Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1//! `GET /metrics` — Prometheus exposition (spec §7, §10).
Matt W2//!
Matt W3//! > `GET /metrics` — **bind to loopback only**.
Matt W4//!
Matt W5//! Dogfood serves one port, so "bind to loopback only" is enforced per request
Matt W6//! rather than per socket: the peer address must be a loopback address, or the
Matt W7//! response is the same 404 any unknown path gets. A 403 would confirm the
Matt W8//! endpoint exists, and the metrics say how many private repositories there are.
Matt W9//!
Matt W10//! The peer address comes from `ConnectInfo` — the real TCP peer — and never
Matt W11//! from `X-Forwarded-For`, which the client controls. Behind the edge proxy that
Matt W12//! means metrics are unreachable from outside the host, which is the point;
Matt W13//! scrape it with `docker exec` or over the compose network.
Matt W14//!
Matt W15//! The metric set is the one §10 says to alert on: job queue depth, indexer
Matt W16//! failures, and push latency. Everything else here is cheap context.
Matt W17
Matt W18use axum::extract::State;
Matt W19use axum::response::{IntoResponse, Response};
Matt W20
Matt W21use crate::error::{AppError, AppResult};
Matt W22use crate::state::{AppState, PeerAddr};
Matt W23
Matt W24pub async fn metrics(State(state): State<AppState>, peer: PeerAddr) -> AppResult<Response> {
Matt W25 // A peer that cannot be proven local is treated exactly like a remote one.
Matt W26 // Defaulting the other way would serve metrics to anybody the moment the
Matt W27 // server was reconfigured without connect info.
Matt W28 if !peer.is_loopback() {
Matt W29 tracing::debug!(peer = ?peer.ip(), "refusing /metrics: not a loopback peer");
Matt W30 return Err(AppError::NotFound);
Matt W31 }
Matt W32
Matt W33 let mut out = String::with_capacity(2048);
Matt W34
Matt W35 // ── the three §10 alerting signals ───────────────────────────────────────
Matt W36
Matt W37 let (ready, failed, oldest): (i64, i64, Option<f64>) = sqlx::query_as(
Matt W38 "SELECT
Matt W39 count(*) FILTER (WHERE locked_at IS NULL AND attempts < max_attempts),
Matt W40 count(*) FILTER (WHERE attempts >= max_attempts),
Matt W41 EXTRACT(EPOCH FROM (now() - min(run_at)))::float8
Matt W42 FILTER (WHERE locked_at IS NULL)
Matt W43 FROM jobs",
Matt W44 )
Matt W45 .fetch_one(&state.db)
Matt W46 .await
Matt W47 .unwrap_or((0, 0, None));
Matt W48
Matt W49 gauge(&mut out, "dogfood_jobs_queued", "Jobs waiting to run.", ready as f64);
Matt W50 gauge(
Matt W51 &mut out,
Matt W52 "dogfood_jobs_failed",
Matt W53 "Jobs that exhausted their retries. Alert on any increase.",
Matt W54 failed as f64,
Matt W55 );
Matt W56 gauge(
Matt W57 &mut out,
Matt W58 "dogfood_jobs_oldest_seconds",
Matt W59 "Age of the oldest queued job. The queue-depth alert that actually matters.",
Matt W60 oldest.unwrap_or(0.0),
Matt W61 );
Matt W62
Matt W63 // Push latency, as the lag between a push landing and its index job being
Matt W64 // picked up. Measured from the rows rather than instrumented in-process so
Matt W65 // it survives a restart of either service.
Matt W66 let push_lag: Option<f64> = sqlx::query_scalar(
Matt W67 "SELECT EXTRACT(EPOCH FROM (now() - max(pushed_at)))::float8 FROM revisions",
Matt W68 )
Matt W69 .fetch_one(&state.db)
Matt W70 .await
Matt W71 .unwrap_or(None);
Matt W72 gauge(
Matt W73 &mut out,
Matt W74 "dogfood_seconds_since_last_push",
Matt W75 "Seconds since the most recent indexed revision.",
Matt W76 push_lag.unwrap_or(-1.0),
Matt W77 );
Matt W78
Matt W79 // ── inventory ────────────────────────────────────────────────────────────
Matt W80
Matt W81 for (metric, help, sql) in [
Matt W82 ("dogfood_users", "Provisioned users.", "SELECT count(*) FROM users"),
Matt W83 ("dogfood_repos", "Repositories.", "SELECT count(*) FROM repos"),
Matt W84 (
Matt W85 "dogfood_repos_private",
Matt W86 "Private repositories.",
Matt W87 "SELECT count(*) FROM repos WHERE visibility = 'private'",
Matt W88 ),
Matt W89 ("dogfood_changes", "Changes indexed.", "SELECT count(*) FROM changes"),
Matt W90 (
Matt W91 "dogfood_changes_conflicted",
Matt W92 "Changes whose head revision is conflicted.",
Matt W93 "SELECT count(*) FROM changes WHERE conflicted",
Matt W94 ),
Matt W95 ("dogfood_revisions", "Revisions indexed.", "SELECT count(*) FROM revisions"),
Matt W96 ("dogfood_issues_open", "Open issues.", "SELECT count(*) FROM issues WHERE state = 'open'"),
Matt W97 (
Matt W98 "dogfood_comments_orphaned",
Matt W99 "Inline comments whose anchor no longer exists.",
Matt W100 "SELECT count(*) FROM comments WHERE anchor_state = 'orphaned'",
Matt W101 ),
Matt W102 (
Matt W103 "dogfood_highlight_cache_entries",
Matt W104 "Cached syntax-highlighted blobs.",
Matt W105 "SELECT count(*) FROM highlight_cache",
Matt W106 ),
Matt W107 ] {
Matt W108 let n: i64 = sqlx::query_scalar(sql).fetch_one(&state.db).await.unwrap_or(-1);
Matt W109 gauge(&mut out, metric, help, n as f64);
Matt W110 }
Matt W111
Matt W112 // Reachability of the database is already the substance of every query
Matt W113 // above; a separate probe would only be able to disagree with them.
Matt W114 gauge(&mut out, "dogfood_up", "Always 1 when the process is serving.", 1.0);
Matt W115
Matt W116 Ok((
Matt W117 [(
Matt W118 axum::http::header::CONTENT_TYPE,
Matt W119 "text/plain; version=0.0.4; charset=utf-8",
Matt W120 )],
Matt W121 out,
Matt W122 )
Matt W123 .into_response())
Matt W124}
Matt W125
Matt W126fn gauge(out: &mut String, name: &str, help: &str, value: f64) {
Matt W127 use std::fmt::Write as _;
Matt W128 let _ = writeln!(out, "# HELP {name} {help}");
Matt W129 let _ = writeln!(out, "# TYPE {name} gauge");
Matt W130 let _ = writeln!(out, "{name} {value}");
Matt W131}
Matt W132
Matt W133#[cfg(test)]
Matt W134mod tests {
Matt W135 use super::gauge;
Matt W136
Matt W137 #[test]
Matt W138 fn exposition_format_is_well_formed() {
Matt W139 let mut s = String::new();
Matt W140 gauge(&mut s, "dogfood_x", "A thing.", 3.0);
Matt W141 assert_eq!(
Matt W142 s,
Matt W143 "# HELP dogfood_x A thing.\n# TYPE dogfood_x gauge\ndogfood_x 3\n"
Matt W144 );
Matt W145 }
Matt W146
Matt W147 #[test]
Matt W148 fn loopback_detection_covers_both_families() {
Matt W149 use std::net::IpAddr;
Matt W150 assert!("127.0.0.1".parse::<IpAddr>().unwrap().is_loopback());
Matt W151 assert!("::1".parse::<IpAddr>().unwrap().is_loopback());
Matt W152 // The container's own bridge address is *not* loopback, which is what
Matt W153 // keeps /metrics off the compose network by default.
Matt W154 assert!(!"172.17.0.2".parse::<IpAddr>().unwrap().is_loopback());
Matt W155 assert!(!"10.0.0.1".parse::<IpAddr>().unwrap().is_loopback());
Matt W156 }
Matt W157}

157 lines · Rust