1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
//! The metrics-surface snapshot (#228, RFC 13 §3 *Exporter obligations*,
//! v1.34): what `zenctl export` serves, as one document.
//!
//! The Prometheus exposition is a **pure function of this struct**
//! ([`crate::model::prom::exposition`]), so `--format json` and the text a
//! scraper reads can never disagree about a value, a state or a bound. The
//! three exporter obligations are visible in the shape rather than in
//! prose: every O6 kind is its own counter under [`ObserverCounters`] and is
//! never summed; a series carries a [`SeriesState`] and loses its value —
//! not its labels — when it stopped, so absence and silence are different
//! bytes; the scopes watched and the planes a wildcard cannot reach ride the
//! document, and a payload verdict is three counted populations, the third
//! being *not validated*.
use std::collections::BTreeMap;
use serde::Serialize;
use super::asked::{Asked, u64_is_zero};
use super::doctor::{CheckId, DoctorSeverity};
/// One snapshot of the exporter's ledger, folded at scrape time.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ExportSnapshot {
/// The exact selectors watched — coverage is this list and no wider
/// (RFC 13 §3 O5).
pub scopes: Vec<String>,
/// The planes a wildcard in `scopes` cannot reach, named verbatim; empty
/// when no scope carries a wildcard. Never implied by the selector alone.
pub excluded: Vec<String>,
/// The registry the contract came from: how many producers' slices were
/// loaded. *Not asked* when no registry was loaded at all — then no key
/// refines and every key counts under `unregistered_keys`, which is a
/// statement about the exporter, not about the fleet (O4).
#[serde(skip_serializing_if = "Asked::is_not_asked", default)]
pub registry: Asked<RegistryInfo>,
/// The ledger's own bound on distinct series; overflow is counted under
/// `suppressed["max_series"]`, never silent.
pub max_series: usize,
/// When the observer started watching (unix seconds). A claim about a
/// span longer than `taken_at - started_at` is unobservable.
pub started_at_unix_s: u64,
/// When this fold was taken (unix seconds). Deliberately **not** in the
/// exposition: two scrapes with no traffic between them must be
/// byte-identical, and the scrape time is the scraper's to record.
pub taken_at_unix_s: u64,
/// Every series the ledger holds, in a deterministic order.
pub series: Vec<SeriesRow>,
pub observer: ObserverCounters,
pub contract: ContractCounters,
/// Samples that produced no series, by reason — `cardinality`
/// (over the declared budget), `max_series`, `fields` (past the
/// per-subject field cap), `text` (a declared text kind), `non_numeric`,
/// `undecodable`, `unparsed` (not a v1 data key). Absent when nothing was
/// suppressed.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub suppressed: BTreeMap<String, u64>,
/// Distinct keys the registry does not declare (or, with no registry
/// loaded, every key): counted and never exported — the contract is the
/// registry, and the count keeps the omission visible (O4).
pub unregistered_keys: u64,
/// The last doctor run, when one was asked for (`--doctor-every`).
#[serde(skip_serializing_if = "Asked::is_not_asked", default)]
pub doctor: Asked<DoctorSummary>,
}
/// The loaded registry, in one number.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct RegistryInfo {
pub producers: usize,
}
/// One exported series: a contract-derived identity, its last value, and
/// the state that says whether the value is current.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SeriesRow {
/// The metric name the exposition uses — derived from the registry's
/// producer, pattern, `unit` and `kind`, never from the leaf's spelling.
pub name: String,
/// The concrete wire key this series was last fed from.
pub key: String,
pub origin: String,
pub producer: String,
pub class: String,
/// The declared pattern, e.g. `disk/{mount}/used`.
pub subject: String,
/// The `{var}` bindings by their declared names.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub labels: BTreeMap<String, String>,
/// The top-level field this series reads, when the payload was an object
/// carrying several numerics rather than one leaf value.
#[serde(skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
/// The declared kind token, when the registry declares one.
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
/// The declared unit, verbatim, when the registry declares one.
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
/// The newest value seen. **Absent** when the state says the series
/// stopped — a stopped series exposes its state and its labels, never a
/// stale number that reads as current.
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
/// When the newest sample arrived, unix seconds on the observer's clock
/// — captured at ingest, so it does not move between scrapes.
pub last_seen_unix_s: u64,
pub state: SeriesState,
/// Samples folded into this series since the observer started.
pub samples: u64,
/// Folds during which this series was fed **and** the observer dropped
/// samples — the interval where its value may not be the newest.
#[serde(skip_serializing_if = "u64_is_zero", default)]
pub drop_exposed: u64,
}
/// Why a series is, or is not, current (RFC 13 §3 — "a series that stopped
/// is not a series that went quiet").
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SeriesState {
/// Fed, and nothing has said it stopped. For telemetry, which declares
/// no period, this is all an observer can say — `last_seen_unix_s`
/// carries the fact.
Live,
/// A `state` subject silent past its declared `ttl_s`. Judged only where
/// the registry declares the period; never for telemetry (O4).
Quiet,
/// The observer chose to forget the key at its own bound (O6); the
/// labels are kept so the disappearance is named.
Evicted,
/// The producer's `alive` token went away (RFC 04 §5) after this series
/// was fed.
OriginDown,
/// The producer retired the key with a tombstone (RFC 04 §1.2) — its
/// own statement, which outranks every inference above.
Retired,
}
impl SeriesState {
/// The wire token, exactly as it serializes — the `state` label.
pub fn as_str(self) -> &'static str {
match self {
SeriesState::Live => "live",
SeriesState::Quiet => "quiet",
SeriesState::Evicted => "evicted",
SeriesState::OriginDown => "origin_down",
SeriesState::Retired => "retired",
}
}
/// Whether a value line is exposed in this state.
pub fn exposes_value(self) -> bool {
matches!(self, SeriesState::Live | SeriesState::Quiet)
}
}
/// What the bounded observer cost, by kind — never summed (RFC 13 §3 O6).
/// Every field is always present: a scraper expects a series to exist at
/// zero, and "absent" is the one thing an exporter must not let a series do.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
pub struct ObserverCounters {
/// Samples missed while the ledger's receiver was behind.
pub dropped: u64,
/// Keys retired from the statistics table at its bound.
pub evicted_keys: u64,
/// Retained samples dropped because the byte budget bit.
pub evicted_bytes: u64,
/// Retained samples that aged past the retention window.
pub expired: u64,
/// Keys retired because their watch was released.
pub unwatched: u64,
/// Samples folded into a newer one between two scrapes — the exporter
/// keeps the newest value per series and counts the rest here.
pub coalesced: u64,
/// Samples that carried no HLC — counted, never defaulted to arrival.
pub unstamped: u64,
}
/// Declared-versus-observed, counted (RFC 13 §3 — "a payload verdict is
/// three counted populations").
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
pub struct ContractCounters {
/// Samples whose subject declares a QoS profile this build knows.
pub qos_judged: u64,
/// Of those, samples that did not ride the declared profile.
pub qos_mismatch: u64,
/// The mismatches by declared subject, for the labelled series.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub qos_mismatch_by_subject: Vec<QosMismatchRow>,
pub payload_valid: u64,
pub payload_invalid: u64,
/// The third population: without `--validate` every sample lands here,
/// and so does every sample past the decode budget.
pub payload_not_validated: u64,
}
/// One declared subject's QoS mismatches.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct QosMismatchRow {
pub producer: String,
pub subject: String,
pub n: u64,
}
/// The last doctor run, reduced to what a series can carry.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DoctorSummary {
pub ran_at_unix_s: u64,
pub findings: Vec<DoctorFindingRef>,
}
/// One finding as a series identity: which check, how bad, on what.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DoctorFindingRef {
pub check: CheckId,
pub severity: DoctorSeverity,
pub subject: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
/// The whole document, pinned: a stopped series carries no `value`, an
/// unasked doctor and an unloaded registry are absent, a zero
/// `drop_exposed` is absent, and every observer counter is present at
/// zero.
#[test]
fn an_export_snapshot_is_pinned() {
let snapshot = ExportSnapshot {
scopes: vec!["v1/*/**".into()],
excluded: vec!["@rpc".into(), "service origins".into()],
registry: Asked::Asked(RegistryInfo { producers: 2 }),
max_series: 10_000,
started_at_unix_s: 1_700_000_000,
taken_at_unix_s: 1_700_000_060,
series: vec![
SeriesRow {
name: "zenkey_subject_sysinfo_cpu_usage_percent".into(),
key: "v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu/usage".into(),
origin: "h-3fa9c2d41b7e".into(),
producer: "sysinfo".into(),
class: "telemetry".into(),
subject: "cpu/usage".into(),
labels: BTreeMap::new(),
field: None,
kind: Some("gauge".into()),
unit: Some("percent".into()),
value: Some(12.5),
last_seen_unix_s: 1_700_000_059,
state: SeriesState::Live,
samples: 7,
drop_exposed: 0,
},
SeriesRow {
name: "zenkey_subject_sysinfo_disk_used_bytes".into(),
key: "v1/h-3fa9c2d41b7e/telemetry/sysinfo/disk/var-log/used".into(),
origin: "h-3fa9c2d41b7e".into(),
producer: "sysinfo".into(),
class: "telemetry".into(),
subject: "disk/{mount}/used".into(),
labels: [("mount".to_string(), "var-log".to_string())]
.into_iter()
.collect(),
field: Some("free".into()),
kind: None,
unit: Some("bytes".into()),
value: None,
last_seen_unix_s: 1_700_000_010,
state: SeriesState::OriginDown,
samples: 3,
drop_exposed: 1,
},
],
observer: ObserverCounters::default(),
contract: ContractCounters {
qos_judged: 7,
qos_mismatch: 1,
qos_mismatch_by_subject: vec![QosMismatchRow {
producer: "sysinfo".into(),
subject: "cpu/usage".into(),
n: 1,
}],
payload_valid: 0,
payload_invalid: 0,
payload_not_validated: 10,
},
suppressed: [("cardinality".to_string(), 4u64)].into_iter().collect(),
unregistered_keys: 1,
doctor: Asked::NotAsked,
};
assert_eq!(
serde_json::to_value(&snapshot).unwrap(),
json!({
"scopes": ["v1/*/**"],
"excluded": ["@rpc", "service origins"],
"registry": {"producers": 2},
"max_series": 10000,
"started_at_unix_s": 1_700_000_000,
"taken_at_unix_s": 1_700_000_060,
"series": [
{
"name": "zenkey_subject_sysinfo_cpu_usage_percent",
"key": "v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu/usage",
"origin": "h-3fa9c2d41b7e",
"producer": "sysinfo",
"class": "telemetry",
"subject": "cpu/usage",
"kind": "gauge",
"unit": "percent",
"value": 12.5,
"last_seen_unix_s": 1_700_000_059,
"state": "live",
"samples": 7,
},
{
"name": "zenkey_subject_sysinfo_disk_used_bytes",
"key": "v1/h-3fa9c2d41b7e/telemetry/sysinfo/disk/var-log/used",
"origin": "h-3fa9c2d41b7e",
"producer": "sysinfo",
"class": "telemetry",
"subject": "disk/{mount}/used",
"labels": {"mount": "var-log"},
"field": "free",
"unit": "bytes",
"last_seen_unix_s": 1_700_000_010,
"state": "origin_down",
"samples": 3,
"drop_exposed": 1,
},
],
"observer": {
"dropped": 0,
"evicted_keys": 0,
"evicted_bytes": 0,
"expired": 0,
"unwatched": 0,
"coalesced": 0,
"unstamped": 0,
},
"contract": {
"qos_judged": 7,
"qos_mismatch": 1,
"qos_mismatch_by_subject": [
{"producer": "sysinfo", "subject": "cpu/usage", "n": 1}
],
"payload_valid": 0,
"payload_invalid": 0,
"payload_not_validated": 10,
},
"suppressed": {"cardinality": 4},
"unregistered_keys": 1,
})
);
}
/// The asked poles: a doctor that ran and a registry that was never
/// loaded — one present with its findings, the other absent rather than
/// `{"producers": 0}`.
#[test]
fn a_doctor_run_and_an_unloaded_registry_are_pinned() {
let doctor = DoctorSummary {
ran_at_unix_s: 1_700_000_030,
findings: vec![DoctorFindingRef {
check: CheckId::StaleState,
severity: DoctorSeverity::Warning,
subject: "h-3fa9c2d41b7e/sysinfo".into(),
}],
};
assert_eq!(
serde_json::to_value(&doctor).unwrap(),
json!({
"ran_at_unix_s": 1_700_000_030,
"findings": [
{"check": "stale-state", "severity": "warning", "subject": "h-3fa9c2d41b7e/sysinfo"}
],
})
);
let unloaded: Asked<RegistryInfo> = Asked::NotAsked;
assert!(unloaded.is_not_asked());
}
#[test]
fn a_stopped_state_exposes_no_value() {
for s in [
SeriesState::Evicted,
SeriesState::OriginDown,
SeriesState::Retired,
] {
assert!(!s.exposes_value(), "{}", s.as_str());
}
assert!(SeriesState::Live.exposes_value());
assert!(SeriesState::Quiet.exposes_value());
}
}