Skip to main content

cqlite_core/observability/
operator_docs.rs

1//! Operator-facing metric reference — GENERATED source of truth (issue #2426).
2//!
3//! The developer-facing catalog ([`super::catalog`]) owns the metric *names*,
4//! *units*, and cardinality contracts. This module adds the **operator**
5//! annotation for every catalogued instrument (one-sentence meaning, bounded
6//! attribute set, healthy-vs-alarming interpretation, and the #2399 round
7//! scoreboard item it feeds) and renders a deterministic markdown reference so
8//! the AWS field team has a single consumable page that CANNOT drift from the
9//! code.
10//!
11//! # Anti-drift contract (mirrors the #1338 parity-report pattern)
12//!
13//! - The renderer's source of truth is the REAL catalog: it walks
14//!   [`super::catalog::ALL_METRICS`] and references the `catalog::attr::*`
15//!   constants, never a hand-copied name list.
16//! - Generation is **fail-closed**: a metric present in `ALL_METRICS` with no
17//!   operator annotation makes [`operator_metric_docs`] return
18//!   [`DocGenError::MissingAnnotation`] — no undocumented instrument can ship.
19//!   An annotation for a name absent from `ALL_METRICS` is likewise rejected.
20//! - The rendered markdown is committed at [`COMMITTED_DOC_REL`]; a lib test
21//!   (`operator_metrics_doc_is_fresh`) and the `operator-metrics-doc` agent-gate
22//!   component fail if the committed file drifts from a fresh render.
23//! - Regenerate with:
24//!   `cargo run -p cqlite-core --example gen_operator_metrics_doc`.
25//!
26//! Everything here is always compiled (it pulls in no OpenTelemetry types), like
27//! the catalog itself, so the generator and its freshness test build in any
28//! feature configuration.
29
30use super::catalog::ALL_METRICS;
31use super::operator_docs_annotations::ANNOTATIONS;
32
33/// Repository-relative path of the committed operator reference. The generator
34/// example and the freshness test resolve it against `CARGO_MANIFEST_DIR/..`.
35pub const COMMITTED_DOC_REL: &str = "docs/reports/flight-metrics-reference.md";
36
37/// Repository-relative path of the PUBLISHED docs-site page (Astro/Starlight
38/// content). Generated from the same catalog render (with front matter) so the
39/// published operator page cannot drift from the code either (issue #2426).
40pub const WEBSITE_DOC_REL: &str =
41    "website/src/content/docs/agents-using/flight-metrics-reference.md";
42
43/// Astro/Starlight front matter for the published website page. Kept beside the
44/// renderer so the example and the freshness test agree byte-for-byte.
45const WEBSITE_FRONT_MATTER: &str = "\
46---
47title: Flight metrics reference
48description: Operator reference for every cqlite.* instrument CQLite emits over Arrow Flight and the storage/write/compaction paths — generated from the observability catalog so it cannot drift.
49sidebar:
50  label: Flight metrics reference
51  order: 20
52---
53
54";
55
56/// OpenTelemetry instrument kind, as the field team reads it on a dashboard.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum MetricKind {
59    /// Monotonically increasing total.
60    Counter,
61    /// Current up/down value.
62    Gauge,
63    /// Distribution of observed values (durations, sizes, ratios).
64    Histogram,
65}
66
67impl MetricKind {
68    /// Lowercase label used in the rendered reference.
69    pub fn label(self) -> &'static str {
70        match self {
71            MetricKind::Counter => "counter",
72            MetricKind::Gauge => "gauge",
73            MetricKind::Histogram => "histogram",
74        }
75    }
76}
77
78/// One operator annotation for a catalogued instrument.
79#[derive(Debug, Clone)]
80pub struct MetricDoc {
81    /// Metric name (a `catalog::*` constant — never a copied literal).
82    pub name: &'static str,
83    /// Instrument kind.
84    pub kind: MetricKind,
85    /// UCUM unit string.
86    pub unit: &'static str,
87    /// One operator sentence: what this instrument means in the field.
88    pub summary: &'static str,
89    /// Bounded attribute keys this metric may carry (`catalog::attr::*`), empty
90    /// when the metric carries none.
91    pub attributes: &'static [&'static str],
92    /// Healthy value vs the shape that should raise an alarm.
93    pub interpretation: &'static str,
94    /// #2399 round-scoreboard item this metric feeds, or `"—"`.
95    pub round_item: &'static str,
96}
97
98/// Error raised when the operator annotations and the catalog disagree — the
99/// fail-closed guard against an undocumented (or phantom) instrument shipping.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum DocGenError {
102    /// A metric in [`ALL_METRICS`] has no operator annotation.
103    MissingAnnotation(&'static str),
104    /// An annotation names a metric that is not in [`ALL_METRICS`].
105    UnknownMetric(&'static str),
106    /// An annotation names a metric more than once.
107    DuplicateAnnotation(&'static str),
108}
109
110impl std::fmt::Display for DocGenError {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            DocGenError::MissingAnnotation(m) => write!(
114                f,
115                "metric `{m}` is catalogued but has no operator annotation in \
116                 operator_docs::ANNOTATIONS — no undocumented instrument may ship (#2426)"
117            ),
118            DocGenError::UnknownMetric(m) => write!(
119                f,
120                "operator annotation names `{m}`, which is not in catalog::ALL_METRICS \
121                 (stale annotation)"
122            ),
123            DocGenError::DuplicateAnnotation(m) => {
124                write!(f, "operator annotation for `{m}` appears more than once")
125            }
126        }
127    }
128}
129
130impl std::error::Error for DocGenError {}
131
132/// Return every catalogued instrument paired with its operator annotation, in
133/// catalog (`ALL_METRICS`) order.
134///
135/// Fail-closed: returns [`DocGenError`] if any metric lacks an annotation, if an
136/// annotation names a metric absent from `ALL_METRICS`, or if a metric is
137/// annotated twice — so no undocumented (or phantom) instrument can ship.
138pub fn operator_metric_docs() -> Result<Vec<MetricDoc>, DocGenError> {
139    // Reject duplicate annotations first (a duplicate would mask a missing one).
140    let mut seen = std::collections::HashSet::new();
141    for a in ANNOTATIONS {
142        if !seen.insert(a.name) {
143            return Err(DocGenError::DuplicateAnnotation(a.name));
144        }
145        if !ALL_METRICS.contains(&a.name) {
146            return Err(DocGenError::UnknownMetric(a.name));
147        }
148    }
149    // Every catalogued metric must have an annotation, walked in catalog order.
150    let mut out = Vec::with_capacity(ALL_METRICS.len());
151    for name in ALL_METRICS {
152        match ANNOTATIONS.iter().find(|a| a.name == *name) {
153            Some(a) => out.push(a.clone()),
154            None => return Err(DocGenError::MissingAnnotation(name)),
155        }
156    }
157    Ok(out)
158}
159
160/// Render the deterministic operator-facing markdown reference from the catalog.
161///
162/// Output is stable across runs (metrics sorted by name, no timestamps) so a
163/// `--check` regeneration can fail on a stale committed artifact — the #1338
164/// derived-artifact pattern.
165pub fn render_markdown() -> Result<String, DocGenError> {
166    let mut docs = operator_metric_docs()?;
167    docs.sort_by(|a, b| a.name.cmp(b.name));
168
169    let mut s = String::new();
170    let mut line = |t: &str| {
171        s.push_str(t);
172        s.push('\n');
173    };
174
175    line("# CQLite Flight metrics — operator reference");
176    line("");
177    line(
178        "> GENERATED from the observability catalog \
179         (`cqlite-core/src/observability/catalog.rs` + `operator_docs.rs`) by \
180         `cargo run -p cqlite-core --example gen_operator_metrics_doc`. \
181         Do NOT edit by hand — edit the catalog/annotations and regenerate. \
182         The `operator-metrics-doc` agent-gate component fails if this file drifts \
183         from the catalog (issue #2426).",
184    );
185    line("");
186    line(
187        "Operator-facing reference for every `cqlite.*` instrument CQLite emits \
188         over Arrow Flight and the storage/write/compaction paths. Names, units, \
189         and bounded attribute sets come straight from the code, so this page \
190         cannot silently diverge from what a running server exposes.",
191    );
192    line("");
193    line("Related: the Flight/Trino operator docs (`docs/flight-trino/`) and the round scoreboard template (issue #2399) link back to the entries here.");
194    line("");
195    line(&format!("Total instruments: **{}**.", docs.len()));
196    line("");
197
198    line("## All instruments");
199    line("");
200    line("| Metric | Type | Unit | Attributes | Operator meaning | Healthy vs alarming |");
201    line("|---|---|---|---|---|---|");
202    for d in &docs {
203        let attrs = if d.attributes.is_empty() {
204            "_(none)_".to_string()
205        } else {
206            d.attributes
207                .iter()
208                .map(|a| format!("`{a}`"))
209                .collect::<Vec<_>>()
210                .join("<br>")
211        };
212        line(&format!(
213            "| `{}` | {} | `{}` | {} | {} | {} |",
214            d.name,
215            d.kind.label(),
216            d.unit,
217            attrs,
218            d.summary,
219            d.interpretation,
220        ));
221    }
222    line("");
223
224    // Round-scoreboard mapping (#2399): only metrics that feed a template item.
225    line("## Round-scoreboard mapping (issue #2399)");
226    line("");
227    line("Metrics a #2399 round-template scoreboard item consumes. Round handoffs (#2367-style) link the item to its metric here instead of re-explaining it.");
228    line("");
229    line("| Metric | Scoreboard item |");
230    line("|---|---|");
231    let mut any = false;
232    for d in &docs {
233        if d.round_item != "—" {
234            any = true;
235            line(&format!("| `{}` | {} |", d.name, d.round_item));
236        }
237    }
238    if !any {
239        line("| _(none)_ | _(none)_ |");
240    }
241    line("");
242
243    Ok(s)
244}
245
246/// Render the published website (Astro/Starlight) variant: the same catalog
247/// render as [`render_markdown`] with Starlight front matter prepended. Sharing
248/// one render keeps the published page from drifting from the committed report.
249pub fn render_website_markdown() -> Result<String, DocGenError> {
250    Ok(format!("{WEBSITE_FRONT_MATTER}{}", render_markdown()?))
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn every_catalogued_metric_has_an_operator_annotation() {
259        // Fail-closed generation (#2426 AC1): no undocumented instrument may ship.
260        let docs = operator_metric_docs().expect("every ALL_METRICS entry must be annotated");
261        assert_eq!(
262            docs.len(),
263            ALL_METRICS.len(),
264            "operator doc count must equal the catalog metric count"
265        );
266    }
267
268    #[test]
269    fn no_annotation_names_a_metric_absent_from_the_catalog() {
270        // A stale annotation (name removed from the catalog) is rejected.
271        for a in ANNOTATIONS {
272            assert!(
273                ALL_METRICS.contains(&a.name),
274                "annotation names `{}`, absent from ALL_METRICS",
275                a.name
276            );
277        }
278    }
279
280    #[test]
281    fn attributes_are_catalogued_bounded_keys() {
282        // Every attribute an annotation lists must be a real `cqlite.`-namespaced
283        // bounded key, so the reference cannot invent an unbounded dimension.
284        let docs = operator_metric_docs().unwrap();
285        for d in &docs {
286            for a in d.attributes {
287                assert!(
288                    a.starts_with("cqlite."),
289                    "attribute `{a}` on `{}` must be a namespaced bounded key",
290                    d.name
291                );
292            }
293        }
294    }
295
296    #[test]
297    fn render_is_deterministic_and_non_empty() {
298        let a = render_markdown().unwrap();
299        let b = render_markdown().unwrap();
300        assert_eq!(a, b, "render must be byte-stable across runs");
301        assert!(a.contains("# CQLite Flight metrics — operator reference"));
302        // Every metric name appears in the rendered table.
303        for name in ALL_METRICS {
304            assert!(a.contains(name), "rendered doc must mention `{name}`");
305        }
306    }
307
308    #[test]
309    fn committed_operator_metrics_doc_is_fresh() {
310        // Anti-drift guard (#2426 AC2/AC5): the committed reference MUST match a
311        // fresh render of the catalog. If a metric is added/renamed or an
312        // annotation changes without regenerating, this fails — mirroring the
313        // #1338 parity-report freshness check, at the lib-test layer so it runs in
314        // both the full gate's core-tests and the lite gate's cqlite-core --lib.
315        let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
316            .parent()
317            .expect("cqlite-core has a repo-root parent");
318        let path = repo_root.join(COMMITTED_DOC_REL);
319        let committed = std::fs::read_to_string(&path).unwrap_or_else(|e| {
320            panic!(
321                "committed operator metrics doc {} is missing ({e}); regenerate with \
322                 `cargo run -p cqlite-core --example gen_operator_metrics_doc`",
323                path.display()
324            )
325        });
326        let fresh = render_markdown().expect("render must succeed");
327        assert_eq!(
328            committed,
329            fresh,
330            "{} is STALE vs the catalog; regenerate with \
331             `cargo run -p cqlite-core --example gen_operator_metrics_doc`",
332            path.display()
333        );
334
335        // The published website page is the same render with front matter.
336        let web_path = repo_root.join(WEBSITE_DOC_REL);
337        let web_committed = std::fs::read_to_string(&web_path).unwrap_or_else(|e| {
338            panic!(
339                "published website metrics page {} is missing ({e}); regenerate with \
340                 `cargo run -p cqlite-core --example gen_operator_metrics_doc`",
341                web_path.display()
342            )
343        });
344        let web_fresh = render_website_markdown().expect("website render must succeed");
345        assert_eq!(
346            web_committed,
347            web_fresh,
348            "{} is STALE vs the catalog; regenerate with \
349             `cargo run -p cqlite-core --example gen_operator_metrics_doc`",
350            web_path.display()
351        );
352    }
353
354    #[test]
355    fn missing_annotation_is_detected() {
356        // Directly exercise the fail-closed path: a catalog with an unannotated
357        // name must surface MissingAnnotation. We simulate by asserting the error
358        // Display is actionable for the real error variants.
359        let e = DocGenError::MissingAnnotation("cqlite.example.unannotated");
360        assert!(e.to_string().contains("no operator annotation"));
361        let u = DocGenError::UnknownMetric("cqlite.example.stale");
362        assert!(u.to_string().contains("not in catalog::ALL_METRICS"));
363    }
364}