WarpMetricExporter

class WarpMetricExporter(replica: ReplicaId, store: DurableStore, maxMetrics: Int = DEFAULT_MAX_METRICS, bufferPolicy: MetricBufferPolicy = MetricBufferPolicy.DROP_OLDEST, histogramPrototype: DDSketch = DDSketch.empty(relativeAccuracy = alphaForOtlpScale(DEFAULT_OTLP_HISTOGRAM_SCALE)))

A CRDT-backed metric exporter for cumulative sums, gauges, and cardinality estimates.

Each metric kind is backed by a different CRDT chosen to match its semantics:

  • Sum — a GCounter per MetricKey. Cumulative, monotonically increasing. Two replicas that independently saw N and M events report N+M after merge. Re-exporting the same increment is not idempotent in the counter sense (each call to incrementSum adds to the total), but the GCounter lattice guarantees that merging the same remote snapshot twice does not inflate the total — so merge under retry is safe.

  • Gauge — a LWWRegister<Double> per MetricKey. Last-writer-wins by (timestamp, replicaId) tiebreak. A later timestamp from any replica wins; tie-breaking on ReplicaId is deterministic regardless of arrival order.

  • Cardinality — a HyperLogLog per MetricKey. Estimates distinct element counts (~0.81% relative error at default precision p=14). The join is element-wise max of register arrays: two replicas that independently added overlapping sets produce the same merged estimate — no double-counting.

  • Exponential histogram — a DDSketch per MetricKey. Answers quantile queries ("p99 latency?") within a relative accuracy α; the join is lossless, so merged replicas equal the sketch of the combined stream. Exports as an OTLP ExponentialHistogramDataPoint, which requires α to align with an integer OTLP scale — configure via alphaForOtlpScale (the default histogramPrototype is scale-DEFAULT_OTLP_HISTOGRAM_SCALE-aligned, α ≈ 1.08%). Like HyperLogLog precision, the sketch configuration is a cluster-wide constant: sketches merge only when it matches exactly.

Key inversion

incrementSum, setGauge, and addCardinality all return MetricExportResult.Success the moment the updated CRDT is durably written to DurableStore — not when it is delivered to any backend. Delivery is asynchronous and eventually consistent.

Buffer cap

The total number of distinct MetricKeys across all kinds is bounded by maxMetrics. When the cap is exceeded, the bufferPolicy selects a series to evict. Every eviction is logged — the metric name and kind are emitted at WARN so an operator can detect label-cardinality explosions.

Thread safety

An explicit reentrantLock guards all mutable state. Suspend calls (store reads/writes) are performed outside the lock section. This is correct under a genuinely multi-threaded dispatcher — limitedParallelism(1) confinement is explicitly banned per repo policy.

Honest limits

  • Clock skew. Gauge timestamps are the producer's local clock. An offline device with a slow clock may have its gauge silently overwritten by a peer with a faster clock even if the slow-clock value is "newer" in wall time. An HLC offset could be estimated on reconnect but is not yet implemented.

  • Cardinality bound. HyperLogLog precision is fixed at p=14 (~0.81% error, 12 KB per series). Very small cardinalities (< ~5 distinct elements) have higher relative error; the linear-counting correction reduces but does not eliminate this.

  • Histogram α is OTLP-gated. Only OTLP-aligned sketch accuracies are accepted (alphaForOtlpScale); a free-form α has no integer OTLP scale, and re-bucketing would break the accuracy guarantee, so ingestion rejects it up front rather than letting an OTLP drain fail later.

Parameters

replica

Stable unique identity for this device/process (use a UUID).

store

Durable persistence backend. InMemoryDurableStore in tests.

maxMetrics

Maximum number of distinct MetricKeys across all kinds.

bufferPolicy

Eviction strategy when maxMetrics is exceeded.

histogramPrototype

The empty DDSketch every new histogram series starts from — its (relativeAccuracy, minIndexedValue, maxIndexedValue) is the cluster-wide histogram configuration. Must be empty and OTLP-aligned; defaults to scale DEFAULT_OTLP_HISTOGRAM_SCALE (α ≈ 1.08%) over the DDSketch default range.

Samples

val replica = us.tractat.kuilt.crdt.ReplicaId("device-uuid-abc123")
val exporter = WarpMetricExporter(replica = replica, store = InMemoryDurableStore())

// Recover persisted metrics from a previous session.
exporter.recover()

// Sum: count server requests — idempotent merge means no double-count under retry.
val requests = MetricKey("server.requests", MetricKind.SUM, mapOf("handler" to "/api/v1"))
exporter.incrementSum(requests, by = 1L)
check(exporter.sumValue(requests) == 1L)

// Gauge: snapshot the current CPU load — last writer (by timestamp) wins across replicas.
val cpu = MetricKey("cpu.usage", MetricKind.GAUGE)
exporter.setGauge(cpu, value = 0.72, timestamp = 1_000_000L)
check(exporter.gaugeValue(cpu) == 0.72)

// Cardinality: count distinct users — same user added twice is still 1.
val users = MetricKey("unique.users", MetricKind.CARDINALITY)
exporter.addCardinality(users, "user-abc")
exporter.addCardinality(users, "user-abc") // idempotent — no double-count
check(exporter.cardinalityEstimate(users) > 0L)

// Exponential histogram: record latencies — any quantile within ~1% relative accuracy.
// Exports as an OTLP ExponentialHistogramDataPoint (see alphaForOtlpScale).
val latency = MetricKey("request.latency.ms", MetricKind.EXPONENTIAL_HISTOGRAM)
exporter.recordHistogram(latency, 12.5)
exporter.recordHistogram(latency, 480.0)
check(exporter.histogramQuantile(latency, 0.5) != null)

Constructors

Link copied to clipboard
constructor(replica: ReplicaId, store: DurableStore, maxMetrics: Int = DEFAULT_MAX_METRICS, bufferPolicy: MetricBufferPolicy = MetricBufferPolicy.DROP_OLDEST, histogramPrototype: DDSketch = DDSketch.empty(relativeAccuracy = alphaForOtlpScale(DEFAULT_OTLP_HISTOGRAM_SCALE)))

Functions

Link copied to clipboard
suspend fun addCardinality(key: MetricKey, element: String): MetricExportResult

Add element to the HyperLogLog sketch for key. Elements are hashed with MurmurHash3 — the element string is the canonical identifier (e.g. a user id, session id, or request id).

Link copied to clipboard

Return the current distinct-element estimate for key, or 0 if no elements have been added.

Link copied to clipboard

Return a snapshot of the HyperLogLog for key (for gossip/anti-entropy).

Link copied to clipboard
suspend fun clear(): MetricExportResult

Drop every metric series this exporter holds and delete its five persisted keys (#2208).

Link copied to clipboard

Return a snapshot of the GCounterDouble for key (for gossip/anti-entropy).

Link copied to clipboard

Read the current double-sum value for key, or 0.0 if the key has never been incremented.

Link copied to clipboard

Return a snapshot of the LWWRegister for key (for gossip/anti-entropy).

Link copied to clipboard

Read the current gauge value for key, or null if no value has been set.

Link copied to clipboard

Estimate the q-quantile (q in [0, 1]) of all values recorded for key, within the sketch's relative accuracy — or null if the series holds no values.

Link copied to clipboard

Return a snapshot of the DDSketch for key (for gossip/anti-entropy).

Link copied to clipboard
suspend fun incrementSum(key: MetricKey, by: Long = 1): MetricExportResult

Increment the cumulative sum for key by by on this replica. Returns MetricExportResult.Success after the durable write.

Link copied to clipboard

Increment the exact-precision cumulative sum for key by by on this replica. The double-precision sibling of incrementSum; a monotonic OTLP DOUBLE_SUM routes here, keeping full precision (no truncation, no fixed-point scaling). Returns MetricExportResult.Success after the durable write.

Link copied to clipboard

Merge a remote HyperLogLog snapshot into this exporter's sketch for key.

Link copied to clipboard

Merge a remote LWWRegister snapshot into this exporter's gauge for key.

Link copied to clipboard

Merge a remote DDSketch snapshot into this exporter's histogram for key.

Link copied to clipboard
suspend fun mergeSum(key: MetricKey, remote: GCounter): MetricExportResult

Merge a remote GCounter snapshot into this exporter's sum for key.

Link copied to clipboard

Merge a remote GCounterDouble snapshot into this exporter's double-sum for key.

Link copied to clipboard

Total number of distinct MetricKeys tracked across all kinds.

Link copied to clipboard

Record one measurement value into the histogram for key. A new series starts from histogramPrototype, so every series shares the cluster-wide configuration. Returns MetricExportResult.Success after the durable write.

Link copied to clipboard
suspend fun recover()

Reload persisted metric state from store. Call once at startup before any calls to the mutating methods. Idempotent: a second call re-reads and re-decodes the same bytes.

Link copied to clipboard
suspend fun setGauge(key: MetricKey, value: Double, timestamp: Long): MetricExportResult

Record the current value of a gauge for key. The (timestamp, replica) pair determines which write wins across replicas — a higher timestamp always wins; equal timestamps break on ReplicaId lexicographic order.

Link copied to clipboard

A converged snapshot of every metric series across all five kinds, as one replicable MetricCatalog. The metric analogue of the log buffer's snapshot(); the tap host offers this value to a joining puller.

Link copied to clipboard

Return a snapshot of the GCounter for key (for gossip/anti-entropy).

Link copied to clipboard

Read the current sum value for key, or 0 if the key has never been incremented.