mz_orchestratord/
metrics.rs1use std::collections::BTreeMap;
11use std::time::Duration;
12
13use axum::{Extension, Router, body::Body, routing::get};
14use http::{HeaderMap, Method, Request, Response, StatusCode};
15use prometheus::{Encoder, TextEncoder};
16use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
17use tracing::{Level, Span};
18
19use mz_ore::metric;
20use mz_ore::metrics::{MetricsRegistry, UIntGauge};
21
22#[derive(Debug)]
23pub struct Metrics {
24 pub is_leader: UIntGauge,
25 pub environmentd_needs_update: UIntGauge,
26}
27
28impl Metrics {
29 pub fn register_into(registry: &MetricsRegistry) -> Self {
30 Self {
31 is_leader: registry.register(
32 metric! {
33 name: "orchestratord_is_leader",
34 help: "Whether this operator replica holds the controller leadership lease, and is therefore the replica reconciling. Summed across the replicas this should be 1. A sustained 0 means no replica can take the lease, for instance because the service account lacks permission on leases, and the operator is reconciling nothing.",
35 }),
36 environmentd_needs_update: registry.register(
37 metric! {
38 name: "environmentd_needs_update",
39 help: "Count of organizations in this cluster which are running outdated pod templates. Only the operator replica holding the leadership lease reconciles, so the others report zero.",
40 }),
41 }
42 }
43
44 pub fn leadership_acquired(&self) {
46 self.is_leader.set(1);
47 }
48
49 pub fn leadership_lost(&self) {
58 self.is_leader.set(0);
59 self.environmentd_needs_update.set(0);
60 }
61}
62
63pub fn router(registry: MetricsRegistry) -> Router {
64 add_tracing_layer(
65 Router::new()
66 .route("/metrics", get(metrics))
67 .layer(Extension(registry)),
68 )
69}
70
71#[allow(clippy::unused_async)]
72async fn metrics(Extension(registry): Extension<MetricsRegistry>) -> (StatusCode, Vec<u8>) {
73 let mut buf = vec![];
74 let encoder = TextEncoder::new();
75 let metric_families = registry.gather();
76 encoder.encode(&metric_families, &mut buf).unwrap();
77 (StatusCode::OK, buf)
78}
79
80fn add_tracing_layer<S>(router: Router<S>) -> Router<S>
93where
94 S: Clone + Send + Sync + 'static,
95{
96 router.layer(
97 TraceLayer::new_for_http()
98 .make_span_with(|request: &Request<Body>| {
99 macro_rules! make_span {
105 ($level:expr) => {
106 tracing::span!(
107 $level,
108 "HTTP request",
109 "request.uri" = %request.uri(),
110 "request.version" = ?request.version(),
111 "request.method" = %request.method(),
112 "request.headers" = tracing::field::Empty,
113 "response.status" = tracing::field::Empty,
114 "response.status_code" = tracing::field::Empty,
115 "response.headers" = tracing::field::Empty,
116 )
117 }
118 }
119 let span = if ["/api/health", "/metrics"].contains(&request.uri().path())
120 || request.method() == Method::OPTIONS
121 {
122 make_span!(Level::DEBUG)
123 } else {
124 make_span!(Level::INFO)
125 };
126
127 if let Ok(s) = serde_json::to_string(&display_headers(request.headers().clone())) {
128 span.record("request.headers", s);
129 }
130
131 span
132 })
133 .on_response(|response: &Response<Body>, _latency, span: &Span| {
134 span.record(
135 "response.status",
136 &tracing::field::display(response.status()),
137 );
138 span.record("response.status_code", response.status().as_u16());
139 if let Ok(s) = serde_json::to_string(&display_headers(response.headers().clone())) {
140 span.record("response.headers", s);
141 }
142
143 if span
147 .metadata()
148 .and_then(|m| Some(m.level()))
149 .unwrap_or(&Level::DEBUG)
150 == &Level::DEBUG
151 {
152 tracing::debug!("HTTP response generated");
153 } else {
154 tracing::info!("HTTP response generated");
155 }
156 })
157 .on_failure(
158 |error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
159 tracing::warn!(error = ?error, "HTTP request handling error");
160 },
161 ),
162 )
163}
164
165fn display_headers(mut headers: HeaderMap) -> BTreeMap<String, String> {
166 _ = headers.remove(http::header::AUTHORIZATION);
168
169 headers
170 .into_iter()
171 .filter_map(|(k, v)| {
172 k.map(|k| {
173 (
174 k.to_string(),
175 String::from_utf8_lossy(v.as_bytes()).to_string(),
176 )
177 })
178 })
179 .collect()
180}