Skip to content

Snapshot options ​

snapshot() returns what a metric is still holding, as rows. The options object filters those rows, collapses them, sorts them and cuts them, in that order.

ts
await httpRequests.snapshot({
  dims: { route: '/checkout' },
  from: Date.now() - 300_000,
  complete: true,
  rollup: 'sum',
  groupBy: ['route'],
  orderBy: 'value',
  direction: 'desc',
  limit: 10,
})

Where they are accepted ​

CallableAcceptsNotes
counter.snapshot()all of them
gauge.snapshot()all of thema rollup merges folds
level.snapshot()all of thema rollup takes the latest value per series, and carried windows are included
timer.snapshot()all of themsame merge as a gauge
event.snapshot()from, to, orderBy, direction, limitthe rest are ignored
log.snapshot()the same as an event
house.snapshot()all of them, plus only and strictapplied to every registered metric

The options an event ignores rather than rejects are the ones that only mean something to a window: dims, complete, rollup and groupBy. One options object can then be handed to a mixed schema without checking what each metric is.

The options ​

OptionTypeDefaultWhat it does
dimspartial declared valuesnoneKeep rows matching these labels
fromnumber or DatenoneLower bound on bucket_ts, inclusive
tonumber or DatenoneUpper bound on bucket_ts, exclusive
completebooleantrueExclude the window still filling
rollup'none' or 'sum''none'Merge every window of a series into one row
groupByarray of dim namesevery dimKeep these labels and merge the rest away
orderBycolumn namenoneSort on this column
direction'asc' or 'desc''desc'Sort direction
limitnumbernoneTake this many, after sorting
onlyarray of metric namesevery metricHouse calls only
strictbooleanfalseHouse calls only. Reject an only naming a metric the house does not hold

dims ​

A partial match on the declared dims. Any subset works, in any order.

ts
await httpRequests.snapshot({ dims: { route: '/checkout' } })
await httpRequests.snapshot({ dims: { route: '/checkout', status: '5xx' } })

Values are compared for equality against the materialised row. A ts() dim matches any Date for the same instant, since the row holds a fresh Date rather than the one you passed.

Naming a dim the metric does not declare throws. A typo would otherwise match nothing and render as an empty chart, which reads like an outage.

http_requests: dims names "pakr", which is not a declared dim. This metric
has [route, status]

from ​

Lower bound on bucket_ts, inclusive. A number is epoch milliseconds, and a Date works too.

ts
await httpRequests.snapshot({ from: Date.now() - 600_000 })

The bound applies to the start of a window, so a window that began before from is left out even when part of it falls inside the range. Round from down to a boundary when you want the window containing it.

An invalid Date, such as new Date('garbage'), throws rather than reading as no bound at all. The same goes for NaN and Infinity. A to that read as no bound would quietly include the window still filling.

to ​

Upper bound on bucket_ts, exclusive.

ts
await httpRequests.snapshot({ from: start, to: end })

to and complete are both upper bounds and both apply. Asking for a to in the future does not waive complete.

complete ​

Leaves out the window that is still accepting writes. Defaults to true.

Four finished buckets with values, and one still filling at 40 percent.
A live read returns the finished windows unless you ask for the open one.

Poll a ten second counter at an arbitrary moment and the current window is on average half full. On a chart that makes every series dip at the right hand edge, and as a rate it produces a sawtooth that looks like real traffic.

Ask for the open window when you want it, and scale it with the two liveness columns every row carries.

ts
const rows = await httpRequests.snapshot({ complete: false })

for (const row of rows) {
  if (row.bucket_open) {
    const fraction = row.bucket_elapsed_ms / httpRequests.resolutionMs
    console.log('projected', row.value / fraction)
  }
}
ColumnMeaning
bucket_openIs this window still accepting writes
bucket_elapsed_msHow many milliseconds of the window have passed

rollup ​

'none', the default, keeps one row per window per series, which is the shape a chart wants. 'sum' merges every window of a series into one row.

ts
await httpRequests.snapshot({ rollup: 'sum' })

Each metric type merges the way its own numbers merge.

TypeHow a rollup merges
countervalues are added
gauge, timersum and count add, min and max take the extreme, last takes the latest window when one series holds it
levelthe latest value per series, then series added together
event, logignored, because records are never merged

A rollup drops id and bucket_ts. Neither survives the merge, because there is no longer one window for them to name.

groupBy ​

Keeps the dims you name and merges the rest away. Windows survive, so this is the option for "per route, over time".

ts
await httpRequests.snapshot({ groupBy: ['route'] })
await httpRequests.snapshot({ rollup: 'sum', groupBy: ['route'] })

bucket_ts survives a groupBy. id survives only on a row where the grouping merged nothing, which is a fact about the data rather than about the options, so the type reports it as optional. An optional dim the group has no value for is left off the row, as it is on an ungrouped row, rather than appearing as a key that holds undefined.

A merged row covers each of its windows once. Three series merged inside one ten second window that is five seconds old report bucket_elapsed_ms: 5000, not 15000, so the projection above still works on a grouped row.

A gauge or timer row that merged several series inside its newest window has no last. A fold does not record which series was observed most recently, so there is no honest answer, and totals() leaves last out for the same reason. A group whose newest window holds a single series keeps it.

Naming a dim the metric does not declare throws, exactly as dims does. groupBy: [] keeps no dims at all and merges every series, one row per window.

orderBy ​

Sorts on one column of the finished rows, before limit applies. Numbers and dates compare by value, and everything else compares as a string.

ts
await httpRequests.snapshot({ orderBy: 'bucket_ts', direction: 'asc' })
await httpRequests.snapshot({ orderBy: 'value', limit: 10 })

The column has to exist on the rows the other options produced, so a rollup that dropped bucket_ts cannot then sort on it. A column some rows have and others do not, such as a gauge's last after a groupBy, is fine: the rows without it go last whichever direction you ask for, so a top ten is ten rows that have the value being ranked.

A name that is not a column of the metric at all throws even when there are no rows to sort, and so does a name such as toString, which every object inherits but no row has.

http_requests: orderBy names "bucket_ts", which is not a column on these rows. They have [route, status, value, bucket_open, bucket_elapsed_ms]

direction ​

'desc' by default, which is what makes an unqualified top ten the top rather than the bottom. Pass 'asc' for a chart in time order. Any other value throws, since one read from configuration gets past TypeScript.

limit ​

Takes this many rows after sorting, so orderBy plus limit is a top K. Without orderBy it takes whatever order the rows arrived in.

ts
await httpRequests.snapshot({ orderBy: 'value', direction: 'desc', limit: 10 })

A negative or fractional limit throws.

only ​

House calls only. Restricts the snapshot to the named metrics.

ts
await house.snapshot({ only: ['http_requests', 'app_log'] })

A name that matches no registered metric reads nothing and leaves no key in the result. That is not an error, since a metric can be registered later with house.register(). Pass strict: true to reject the call instead, before anything is read.

ts
await house.snapshot({ only: ['http_requets'], strict: true })
// Error: house.snapshot: only names "http_requets", which is not a registered
// metric. The registered metrics are [http_requests, app_log]

The order things happen in ​

read -> match dims -> sort by window -> collapse -> stamp liveness -> orderBy -> limit

Two consequences follow from that order:

  • limit is a top K. Sorting happens first, so the ten rows you get back are the ten largest rather than the first ten found.
  • orderBy sees the collapsed rows. A column that a rollup merged away is no longer there to sort on.

How the type changes with the options ​

Rows are typed to the metric that produced them, and the row type follows the options you passed.

ts
const rows = await httpRequests.snapshot()
rows[0].route      // string
rows[0].status     // '2xx' | '3xx' | '4xx' | '5xx'
rows[0].value      // number
rows[0].bucket_ts  // Date

const rolled = await httpRequests.snapshot({ rollup: 'sum' })
rolled[0].bucket_ts
//        ^^^^^^^^^ Type error: this row has no bucket_ts

const byRoute = await httpRequests.snapshot({ groupBy: ['route'] })
byRoute[0].status
//         ^^^^^^ Type error: groupBy merged this column away

Pass the options inline for this to work. An object built in a variable and typed as SnapshotOptions first has nothing left for the type to read, and you get the unrolled shape.

ts
const options: SnapshotOptions = { rollup: 'sum' }
await httpRequests.snapshot(options)   // typed as if nothing was rolled up

Errors ​

MessageCause
dims names "pakr", which is not a declared dimA filter naming an undeclared dim
groupBy names "pakr", which is not a declared dimA groupBy naming an undeclared dim
http_requests: orderBy names "bucket_ts", which is not a column on these rowsSorting on a column the other options removed
limit must be a non-negative integer, got -1A negative or fractional limit
to must be a valid Date or a finite number of millisecondsAn invalid Date, NaN or Infinity for from or to
rollup must be 'none' or 'sum', got "avg"Any other rollup
direction must be 'asc' or 'desc', got "up"Any other direction
house.snapshot: only names "x", which is not a registered metricstrict: true with an only naming a metric the house does not hold

Released under the MIT License.