Skip to content

Commit 95609cf

Browse files
haasonsaasclaude
andcommitted
Add /metrics endpoint for Prometheus scraping
Expose review statistics (total, running, completed, failed, comments, files reviewed, diff bytes, durations, GitHub posts) in Prometheus text format at /metrics on the app port. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2b8c804 commit 95609cf

2 files changed

Lines changed: 156 additions & 0 deletions

File tree

src/server/metrics.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use axum::extract::State;
2+
use axum::response::IntoResponse;
3+
use std::fmt::Write;
4+
use std::sync::Arc;
5+
6+
use super::state::{AppState, ReviewStatus};
7+
8+
pub async fn get_metrics(State(state): State<Arc<AppState>>) -> impl IntoResponse {
9+
let reviews = state.reviews.read().await;
10+
11+
let mut total: u64 = 0;
12+
let mut running: u64 = 0;
13+
let mut completed: u64 = 0;
14+
let mut failed: u64 = 0;
15+
let mut pending: u64 = 0;
16+
let mut total_comments: u64 = 0;
17+
let mut total_duration_ms: u64 = 0;
18+
let mut completed_with_duration: u64 = 0;
19+
let mut total_files_reviewed: u64 = 0;
20+
let mut total_diff_bytes: u64 = 0;
21+
let mut github_posted: u64 = 0;
22+
23+
for session in reviews.values() {
24+
total += 1;
25+
match session.status {
26+
ReviewStatus::Running => running += 1,
27+
ReviewStatus::Complete => completed += 1,
28+
ReviewStatus::Failed => failed += 1,
29+
ReviewStatus::Pending => pending += 1,
30+
}
31+
total_comments += session.comments.len() as u64;
32+
total_files_reviewed += session.files_reviewed as u64;
33+
34+
if let Some(event) = &session.event {
35+
if event.duration_ms > 0 {
36+
total_duration_ms += event.duration_ms;
37+
completed_with_duration += 1;
38+
}
39+
total_diff_bytes += event.diff_bytes as u64;
40+
if event.github_posted {
41+
github_posted += 1;
42+
}
43+
}
44+
}
45+
46+
drop(reviews);
47+
48+
let mut buf = String::with_capacity(2048);
49+
50+
write_metric(
51+
&mut buf,
52+
"diffscope_reviews_total",
53+
"Total number of reviews",
54+
"counter",
55+
total,
56+
);
57+
write_metric(
58+
&mut buf,
59+
"diffscope_reviews_running",
60+
"Number of currently running reviews",
61+
"gauge",
62+
running,
63+
);
64+
write_metric(
65+
&mut buf,
66+
"diffscope_reviews_completed_total",
67+
"Total completed reviews",
68+
"counter",
69+
completed,
70+
);
71+
write_metric(
72+
&mut buf,
73+
"diffscope_reviews_failed_total",
74+
"Total failed reviews",
75+
"counter",
76+
failed,
77+
);
78+
write_metric(
79+
&mut buf,
80+
"diffscope_reviews_pending",
81+
"Number of pending reviews",
82+
"gauge",
83+
pending,
84+
);
85+
write_metric(
86+
&mut buf,
87+
"diffscope_comments_total",
88+
"Total comments generated across all reviews",
89+
"counter",
90+
total_comments,
91+
);
92+
write_metric(
93+
&mut buf,
94+
"diffscope_files_reviewed_total",
95+
"Total files reviewed across all reviews",
96+
"counter",
97+
total_files_reviewed,
98+
);
99+
write_metric(
100+
&mut buf,
101+
"diffscope_diff_bytes_total",
102+
"Total diff bytes processed",
103+
"counter",
104+
total_diff_bytes,
105+
);
106+
write_metric(
107+
&mut buf,
108+
"diffscope_review_duration_ms_total",
109+
"Total review duration in milliseconds",
110+
"counter",
111+
total_duration_ms,
112+
);
113+
write_metric(
114+
&mut buf,
115+
"diffscope_reviews_with_duration_total",
116+
"Number of completed reviews with duration data",
117+
"counter",
118+
completed_with_duration,
119+
);
120+
write_metric(
121+
&mut buf,
122+
"diffscope_github_reviews_posted_total",
123+
"Total reviews posted to GitHub",
124+
"counter",
125+
github_posted,
126+
);
127+
128+
(
129+
[(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")],
130+
buf,
131+
)
132+
}
133+
134+
fn write_metric(buf: &mut String, name: &str, help: &str, metric_type: &str, value: u64) {
135+
let _ = writeln!(buf, "# HELP {name} {help}");
136+
let _ = writeln!(buf, "# TYPE {name} {metric_type}");
137+
let _ = writeln!(buf, "{name} {value}");
138+
}
139+
140+
#[cfg(test)]
141+
mod tests {
142+
use super::*;
143+
144+
#[test]
145+
fn test_write_metric_format() {
146+
let mut buf = String::new();
147+
write_metric(&mut buf, "test_metric", "A test metric", "gauge", 42);
148+
assert!(buf.contains("# HELP test_metric A test metric\n"));
149+
assert!(buf.contains("# TYPE test_metric gauge\n"));
150+
assert!(buf.contains("test_metric 42\n"));
151+
}
152+
}

src/server/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pub mod api;
22
pub mod github;
33
pub mod state;
44

5+
pub mod metrics;
6+
57
use axum::{
68
http::{header, StatusCode},
79
response::{IntoResponse, Response},
@@ -110,6 +112,8 @@ pub async fn start_server(config: Config, host: &str, port: u16) -> anyhow::Resu
110112

111113
let app = Router::new()
112114
.nest("/api", api_routes)
115+
.route("/metrics", get(metrics::get_metrics))
116+
.with_state(state.clone())
113117
.fallback(serve_embedded)
114118
.layer(cors);
115119

0 commit comments

Comments
 (0)