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
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 onlyapplied 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

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, so a ts() dim matches on the Date it decodes to.

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.

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
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.

Naming a dim the metric does not declare throws, exactly as dims does.

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.

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.

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'] })

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
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

Released under the MIT License.