cqlite_core/observability/
operator_docs.rs1use super::catalog::ALL_METRICS;
31use super::operator_docs_annotations::ANNOTATIONS;
32
33pub const COMMITTED_DOC_REL: &str = "docs/reports/flight-metrics-reference.md";
36
37pub const WEBSITE_DOC_REL: &str =
41 "website/src/content/docs/agents-using/flight-metrics-reference.md";
42
43const 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum MetricKind {
59 Counter,
61 Gauge,
63 Histogram,
65}
66
67impl MetricKind {
68 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#[derive(Debug, Clone)]
80pub struct MetricDoc {
81 pub name: &'static str,
83 pub kind: MetricKind,
85 pub unit: &'static str,
87 pub summary: &'static str,
89 pub attributes: &'static [&'static str],
92 pub interpretation: &'static str,
94 pub round_item: &'static str,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum DocGenError {
102 MissingAnnotation(&'static str),
104 UnknownMetric(&'static str),
106 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
132pub fn operator_metric_docs() -> Result<Vec<MetricDoc>, DocGenError> {
139 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 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
160pub 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 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
246pub 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 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 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 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 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 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 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 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}