5.2 Series and Samples

The metric store is a single SQLite database, and unlike the event and log stores it is organised around series rather than records. Individual samples are appended to a series that already has an identity.

5.2.1 The series table #

ColumnTypeContents
idINTEGER PRIMARY KEYSeries identifier; the foreign key samples uses.
nameTEXT NOT NULLThe metric name.
labelsTEXT NOT NULLCanonical label representation. Empty string for no labels.
typeINTEGER NOT NULL0 counter, 1 gauge, 2 histogram.
label_hashINTEGER NOT NULLHash of the canonical label string.
boundaries_hashINTEGERHash of the canonical boundary blob. Null for counters and gauges.
boundariesBLOBCanonical boundary blob. Null for counters and gauges.

5.2.1.1 The canonical label string #

Labels are sorted by key in unsigned UTF-8 byte order, each pair written key=value, and the pairs joined with commas: core=0,host=server1. The empty label set is the empty string.

No escaping is performed and none is needed, because ingestion rejects = and , inside a key or a value (PSPU §3.10). That prohibition exists precisely to make this encoding unambiguous, and it is the reason the constraint binds the producer rather than being handled internally.

5.2.1.2 The boundaries blob #

A fixed binary encoding, not MessagePack:

  1. boundary_count, u32 little-endian
  2. that many IEEE-754 f64 values, each little-endian, in the validated order the producer sent

It exists only to identify histogram series and to resolve hash collisions, and is never returned in a query result.

5.2.1.3 Hashes narrow, they do not decide #

label_hash and boundaries_hash are 64-bit FNV-1a over the exact bytes of the canonical string or blob, with offset basis 0xcbf29ce484222325 and prime 0x100000001b3. The high bit is cleared before storage, hash & 0x7fff_ffff_ffff_ffff, so the value always fits SQLite's signed INTEGER.

A lookup always verifies the full labels string, and for a histogram the full boundaries blob, after narrowing by hash. A hash is an index key, never an identity: two label sets that collide are still two series.

5.2.1.4 Uniqueness #

The table carries UNIQUE(name, labels, boundaries_hash).

For counters and gauges boundaries_hash is null, and SQLite treats nulls as distinct in a unique constraint — so the constraint does not enforce uniqueness for them. What does is the single-writer resolution logic (§5.3), which checks before inserting. The constraint is a defensive backstop against a future change that introduces a second write path, not the primary mechanism.

type is not part of the identity. A record resolving to an existing series with a different type resolves successfully and is then dropped for the mismatch (§5.1).

5.2.2 The samples table #

ColumnTypeContents
idINTEGER PRIMARY KEYInternal row identifier; the tiebreaker for samples sharing a series and timestamp.
series_idINTEGER NOT NULLReferences series(id).
boot_idBLOB NOT NULL16-byte boot ID GUID.
timestampINTEGER NOT NULLNanoseconds since the Unix epoch.
valueREAL NOT NULLThe raw value for counters and gauges. Stores 0 for histograms.
histogram_dataBLOBCanonical MessagePack histogram sample map. Null for counters and gauges.

For a histogram, histogram_data is a canonical MessagePack map (PSPU §3.5) with exactly four keys: boundaries, an array of float64 in the producer's order; counts, an array of unsigned integers; total_count; and sum, a finite float64.

value is a placeholder for histogram rows and is never returned as a metric query value. Storing 0 rather than null keeps the column NOT NULL and keeps the row layout uniform.

Canonical encoding is required here because a stored sample map must be byte-stable: two equal histograms encode identically, which is what makes them comparable without decoding.

boot_id is per sample and is not part of the series identity, so a series stays continuous across a reboot (§3.7).

5.2.3 Ordering #

Query execution order within a series is always (timestamp, id) ascending, never insertion order alone. Duplicate timestamps are permitted and id gives them a stable order.

id is internal. It is never exposed as a query field, a result field, an access-control field, or a reserved label key (PSPU §3.28).

5.2.4 The rollups table #

The database also holds rollups, defined in §5.6. Rollups are boot-agnostic scalar aggregates and carry no boot_id, which is why a boot-filtered metric query is never served from one.

5.2.5 Write-time indexes #

  • idx_samples_series_timestamp on samples(series_id, timestamp, id) — the dominant pattern is "samples for series X over range Y in deterministic order", and this one composite index serves the series lookup, the range filter and the (timestamp, id) ordering in a single scan.
  • idx_series_name on series(name) — name lookups.
  • idx_series_label_hash on series(label_hash) — series resolution on the ingestion path.
  • idx_rollups_series_function_window on rollups(series_id, function, window_seconds, window_start).

5.2.6 Schema version #

A metadata table with the same structure as the other stores' (§3.1). Version 1 comprises series, samples, rollups and metadata; the current value is in §B. eventd checks it at startup, applies the lifecycle rules of §5.4, and does not migrate.

Edit this page