Skip to content
Clone

feat(repo): add metrics module and handler middleware

zhenyi committed
parent c016303 commit 1f1aa45 Browse files

3 files changed

+421 −0

File tree 3
Added lib/repo/handler/middleware/metrics.rs View lib/repo/handler/middleware/metrics.rs
@@ -0,0 +1,47 @@
1use axum::extract::{MatchedPath, Request};
2use axum::middleware::Next;
3use axum::response::Response;
4
5/// HTTP-level metrics layer for the `repo` crate.
6///
7/// Emits `repo_http_requests_total{route,method,status_class}` (counter) and
8/// `repo_http_request_duration_seconds{route,method,status_class}` (histogram)
9/// on every request that traverses the router. Uses `MatchedPath` so the
10/// `route` label stays low-cardinality (no repo-id path params).
11pub async fn metrics_layer(req: Request, next: Next) -> Response {
12 crate::metrics::ensure_described();
13
14 let route = req
15 .extensions()
16 .get::<MatchedPath>()
17 .map(|m| m.as_str().to_string())
18 .unwrap_or_else(|| req.uri().path().to_string());
19 let method = req.method().as_str().to_string();
20 let started = std::time::Instant::now();
21
22 let resp = next.run(req).await;
23
24 let class = match resp.status().as_u16() / 100 {
25 2 => "2xx",
26 3 => "3xx",
27 4 => "4xx",
28 5 => "5xx",
29 _ => "other",
30 };
31 metrics::counter!(
32 "repo_http_requests_total",
33 "route" => route.clone(),
34 "method" => method.clone(),
35 "status_class" => class
36 )
37 .increment(1);
38 metrics::histogram!(
39 "repo_http_request_duration_seconds",
40 "route" => route,
41 "method" => method,
42 "status_class" => class
43 )
44 .record(started.elapsed().as_secs_f64());
45
46 resp
47}
No newline at end of file
Added lib/repo/handler/middleware/mod.rs View lib/repo/handler/middleware/mod.rs
@@ -0,0 +1 @@
1pub(crate) mod metrics;
No newline at end of file
Added lib/repo/metrics.rs View lib/repo/metrics.rs
@@ -0,0 +1,373 @@
1//! Tracing layer for the `repo` crate.
2//!
3//! Emits metrics through the `metrics` facade. No exporter is installed here;
4//! the host binary is responsible for binding a recorder. All labels are
5//! bounded to keep cardinality low (no `repo_id`/`user_id`/`branch`).
6
7use std::time::Instant;
8
9use metrics::{counter, describe_counter, describe_histogram, histogram};
10
11use crate::error::RepoError;
12
13// ── Feature constants (one per usecase) ───────────────────────────────────
14
15// archive
16pub const FEATURE_ARCHIVES_GET: &str = "archives_get";
17
18// branch_protection
19pub const FEATURE_BRANCH_PROTECTIONS_LIST: &str = "branch_protections_list";
20pub const FEATURE_BRANCH_PROTECTIONS_CREATE: &str = "branch_protections_create";
21pub const FEATURE_BRANCH_PROTECTIONS_GET: &str = "branch_protections_get";
22pub const FEATURE_BRANCH_PROTECTIONS_EDIT: &str = "branch_protections_edit";
23pub const FEATURE_BRANCH_PROTECTIONS_DELETE: &str = "branch_protections_delete";
24pub const FEATURE_BRANCH_PROTECTIONS_UPDATE_PRIORITIES: &str = "branch_protections_update_priorities";
25
26// branch
27pub const FEATURE_BRANCHES_LIST: &str = "branches_list";
28pub const FEATURE_BRANCHES_GET: &str = "branches_get";
29pub const FEATURE_BRANCHES_CREATE: &str = "branches_create";
30pub const FEATURE_BRANCHES_UPDATE: &str = "branches_update";
31pub const FEATURE_BRANCHES_DELETE: &str = "branches_delete";
32pub const FEATURE_BRANCHES_RENAME: &str = "branches_rename";
33pub const FEATURE_BRANCHES_MERGE_UPSTREAM: &str = "branches_merge_upstream";
34
35// collab
36pub const FEATURE_COLLAB_LIST: &str = "collab_list";
37pub const FEATURE_COLLAB_IS_COLLABORATOR: &str = "collab_is_collaborator";
38pub const FEATURE_COLLAB_ADD: &str = "collab_add";
39pub const FEATURE_COLLAB_DELETE: &str = "collab_delete";
40pub const FEATURE_COLLAB_GET_REPO_PERMISSIONS: &str = "collab_get_repo_permissions";
41
42// commit_status
43pub const FEATURE_COMMIT_STATUSES_LIST: &str = "commit_statuses_list";
44pub const FEATURE_COMMIT_STATUSES_CREATE: &str = "commit_statuses_create";
45pub const FEATURE_COMMIT_STATUSES_LIST_BY_REF: &str = "commit_statuses_list_by_ref";
46pub const FEATURE_COMMIT_STATUSES_COMBINED_BY_REF: &str = "commit_statuses_combined_by_ref";
47
48// compare
49pub const FEATURE_COMPARE: &str = "compare";
50
51// contents
52pub const FEATURE_CONTENTS_LIST: &str = "contents_list";
53pub const FEATURE_CONTENTS_GET: &str = "contents_get";
54pub const FEATURE_CONTENTS_CREATE_FILE: &str = "contents_create_file";
55pub const FEATURE_CONTENTS_UPDATE_FILE: &str = "contents_update_file";
56pub const FEATURE_CONTENTS_DELETE_FILE: &str = "contents_delete_file";
57pub const FEATURE_CONTENTS_CHANGE_FILES: &str = "contents_change_files";
58pub const FEATURE_CONTENTS_APPLY_DIFF_PATCH: &str = "contents_apply_diff_patch";
59
60// deploy_key
61pub const FEATURE_DEPLOY_KEYS_LIST: &str = "deploy_keys_list";
62pub const FEATURE_DEPLOY_KEYS_GET: &str = "deploy_keys_get";
63pub const FEATURE_DEPLOY_KEYS_CREATE: &str = "deploy_keys_create";
64pub const FEATURE_DEPLOY_KEYS_DELETE: &str = "deploy_keys_delete";
65
66// extras
67pub const FEATURE_EXTRAS_SEARCH_REPOS: &str = "extras_search_repos";
68pub const FEATURE_EXTRAS_GET_REPO_BY_ID: &str = "extras_get_repo_by_id";
69pub const FEATURE_EXTRAS_UPDATE_REPO_AVATAR: &str = "extras_update_repo_avatar";
70pub const FEATURE_EXTRAS_DELETE_REPO_AVATAR: &str = "extras_delete_repo_avatar";
71pub const FEATURE_EXTRAS_LIST_ISSUE_TEMPLATES: &str = "extras_list_issue_templates";
72pub const FEATURE_EXTRAS_GET_ISSUE_CONFIG: &str = "extras_get_issue_config";
73pub const FEATURE_EXTRAS_VALIDATE_ISSUE_CONFIG: &str = "extras_validate_issue_config";
74pub const FEATURE_EXTRAS_GET_LANGUAGES: &str = "extras_get_languages";
75pub const FEATURE_EXTRAS_GET_LICENSES: &str = "extras_get_licenses";
76pub const FEATURE_EXTRAS_LIST_STARGAZERS: &str = "extras_list_stargazers";
77pub const FEATURE_EXTRAS_LIST_ASSIGNEES: &str = "extras_list_assignees";
78pub const FEATURE_EXTRAS_CHECK_ASSIGNEE: &str = "extras_check_assignee";
79pub const FEATURE_EXTRAS_LIST_REVIEWERS: &str = "extras_list_reviewers";
80pub const FEATURE_EXTRAS_CHECK_NEW_PIN_ALLOWED: &str = "extras_check_new_pin_allowed";
81pub const FEATURE_EXTRAS_GET_FILE_CONTENTS: &str = "extras_get_file_contents";
82pub const FEATURE_EXTRAS_LIST_COMMITS: &str = "extras_list_commits";
83pub const FEATURE_EXTRAS_GET_COMMIT: &str = "extras_get_commit";
84pub const FEATURE_EXTRAS_GET_COMMIT_DIFF_OR_PATCH: &str = "extras_get_commit_diff_or_patch";
85pub const FEATURE_EXTRAS_GET_COMBINED_STATUS_BY_REF: &str = "extras_get_combined_status_by_ref";
86pub const FEATURE_EXTRAS_GET_STATUSES_BY_REF: &str = "extras_get_statuses_by_ref";
87pub const FEATURE_EXTRAS_COMPARE_COMMITS: &str = "extras_compare_commits";
88pub const FEATURE_EXTRAS_LIST_GIT_REFS: &str = "extras_list_git_refs";
89pub const FEATURE_EXTRAS_GET_TREE: &str = "extras_get_tree";
90pub const FEATURE_EXTRAS_GET_BLOB: &str = "extras_get_blob";
91pub const FEATURE_EXTRAS_GET_NOTE: &str = "extras_get_note";
92pub const FEATURE_EXTRAS_GET_RAW_FILE: &str = "extras_get_raw_file";
93pub const FEATURE_EXTRAS_GET_MEDIA: &str = "extras_get_media";
94pub const FEATURE_EXTRAS_GET_EDITORCONFIG: &str = "extras_get_editorconfig";
95pub const FEATURE_EXTRAS_LIST_FORKS: &str = "extras_list_forks";
96pub const FEATURE_EXTRAS_LIST_RELEASE_ATTACHMENTS: &str = "extras_list_release_attachments";
97pub const FEATURE_EXTRAS_GET_RELEASE_ATTACHMENT: &str = "extras_get_release_attachment";
98pub const FEATURE_EXTRAS_WATCH_REPO: &str = "extras_watch_repo";
99pub const FEATURE_EXTRAS_UNWATCH_REPO: &str = "extras_unwatch_repo";
100pub const FEATURE_EXTRAS_GET_SIGNING_KEY_GPG: &str = "extras_get_signing_key_gpg";
101pub const FEATURE_EXTRAS_GET_SIGNING_KEY_SSH: &str = "extras_get_signing_key_ssh";
102
103// git_ref
104pub const FEATURE_GIT_REFS_LIST: &str = "git_refs_list";
105pub const FEATURE_GIT_REFS_FILTER: &str = "git_refs_filter";
106pub const FEATURE_GIT_REFS_GET_COMMIT: &str = "git_refs_get_commit";
107pub const FEATURE_GIT_REFS_GET_COMMIT_DIFF_OR_PATCH: &str = "git_refs_get_commit_diff_or_patch";
108pub const FEATURE_GIT_REFS_GET_TREE: &str = "git_refs_get_tree";
109pub const FEATURE_GIT_REFS_GET_BLOB: &str = "git_refs_get_blob";
110pub const FEATURE_GIT_REFS_GET_NOTE: &str = "git_refs_get_note";
111
112// pull_request
113pub const FEATURE_PULL_REQUESTS_LIST: &str = "pull_requests_list";
114pub const FEATURE_PULL_REQUESTS_GET: &str = "pull_requests_get";
115pub const FEATURE_PULL_REQUESTS_CREATE: &str = "pull_requests_create";
116pub const FEATURE_PULL_REQUESTS_UPDATE: &str = "pull_requests_update";
117pub const FEATURE_PULL_REQUESTS_CLOSE: &str = "pull_requests_close";
118pub const FEATURE_PULL_REQUESTS_REOPEN: &str = "pull_requests_reopen";
119pub const FEATURE_PULL_REQUESTS_LOCK: &str = "pull_requests_lock";
120pub const FEATURE_PULL_REQUESTS_UNLOCK: &str = "pull_requests_unlock";
121pub const FEATURE_PULL_REQUESTS_CREATE_COMMENT: &str = "pull_requests_create_comment";
122pub const FEATURE_PULL_REQUESTS_LIST_COMMENTS: &str = "pull_requests_list_comments";
123pub const FEATURE_PULL_REQUESTS_SUBMIT_REVIEW: &str = "pull_requests_submit_review";
124pub const FEATURE_PULL_REQUESTS_LIST_REVIEWS: &str = "pull_requests_list_reviews";
125pub const FEATURE_PULL_REQUESTS_LIST_EVENTS: &str = "pull_requests_list_events";
126pub const FEATURE_PULL_REQUESTS_MERGE: &str = "pull_requests_merge";
127
128// release
129pub const FEATURE_RELEASES_LIST: &str = "releases_list";
130pub const FEATURE_RELEASES_GET: &str = "releases_get";
131pub const FEATURE_RELEASES_GET_LATEST: &str = "releases_get_latest";
132pub const FEATURE_RELEASES_GET_BY_TAG: &str = "releases_get_by_tag";
133pub const FEATURE_RELEASES_CREATE: &str = "releases_create";
134pub const FEATURE_RELEASES_EDIT: &str = "releases_edit";
135pub const FEATURE_RELEASES_DELETE: &str = "releases_delete";
136pub const FEATURE_RELEASES_DELETE_BY_TAG: &str = "releases_delete_by_tag";
137
138// subscriber
139pub const FEATURE_SUBSCRIBERS_LIST: &str = "subscribers_list";
140
141// sync
142pub const FEATURE_SYNC_GET_STATUS: &str = "sync_get_status";
143pub const FEATURE_SYNC_REPO_METADATA: &str = "sync_repo_metadata";
144
145// tag_protection
146pub const FEATURE_TAG_PROTECTIONS_LIST: &str = "tag_protections_list";
147pub const FEATURE_TAG_PROTECTIONS_CREATE: &str = "tag_protections_create";
148pub const FEATURE_TAG_PROTECTIONS_GET: &str = "tag_protections_get";
149pub const FEATURE_TAG_PROTECTIONS_EDIT: &str = "tag_protections_edit";
150pub const FEATURE_TAG_PROTECTIONS_DELETE: &str = "tag_protections_delete";
151
152// tag
153pub const FEATURE_TAGS_LIST: &str = "tags_list";
154pub const FEATURE_TAGS_GET: &str = "tags_get";
155pub const FEATURE_TAGS_CREATE: &str = "tags_create";
156pub const FEATURE_TAGS_DELETE: &str = "tags_delete";
157
158// topic
159pub const FEATURE_TOPICS_LIST: &str = "topics_list";
160pub const FEATURE_TOPICS_UPDATE: &str = "topics_update";
161pub const FEATURE_TOPICS_ADD: &str = "topics_add";
162pub const FEATURE_TOPICS_DELETE: &str = "topics_delete";
163pub const FEATURE_TOPICS_SEARCH: &str = "topics_search";
164
165// lifecycle (repos)
166pub const FEATURE_REPOS_CREATE: &str = "repos_create";
167pub const FEATURE_REPOS_GET: &str = "repos_get";
168pub const FEATURE_REPOS_LIST: &str = "repos_list";
169pub const FEATURE_REPOS_UPDATE: &str = "repos_update";
170pub const FEATURE_REPOS_DELETE: &str = "repos_delete";
171pub const FEATURE_REPOS_ARCHIVE: &str = "repos_archive";
172pub const FEATURE_REPOS_RENAME: &str = "repos_rename";
173pub const FEATURE_REPOS_TRANSFER: &str = "repos_transfer";
174pub const FEATURE_REPOS_FORK: &str = "repos_fork";
175pub const FEATURE_REPOS_GENERATE: &str = "repos_generate";
176
177// ── Outcome labels (bounded enums) ────────────────────────────────────────
178
179pub const OUTCOME_SUCCESS: &str = "success";
180
181// ── Metric description registration (Once) ────────────────────────────────
182
183static DESCRIBE_ONCE: std::sync::Once = std::sync::Once::new();
184
185pub fn ensure_described() {
186 DESCRIBE_ONCE.call_once(describe_all);
187}
188
189fn describe_all() {
190 describe_counter!(
191 "repo_http_requests_total",
192 "Total HTTP requests handled by the repo service"
193 );
194 describe_histogram!(
195 "repo_http_request_duration_seconds",
196 metrics::Unit::Seconds,
197 "Repo HTTP request latency"
198 );
199 describe_counter!(
200 "repo_usecase_invocations_total",
201 "Total usecase invocations labeled by feature and outcome"
202 );
203 describe_counter!(
204 "repo_usecase_errors_total",
205 "Total usecase errors labeled by feature and error_kind"
206 );
207 describe_histogram!(
208 "repo_usecase_duration_seconds",
209 metrics::Unit::Seconds,
210 "Usecase execution latency"
211 );
212 describe_counter!(
213 "repo_storage_operations_total",
214 "Object storage operations initiated by the repo service"
215 );
216}
217
218// ── Timer (RAII) ──────────────────────────────────────────────────────────
219
220/// Records the usecase execution duration on drop.
221pub struct Timer {
222 pub feature: &'static str,
223 pub start: Instant,
224}
225
226impl Timer {
227 pub fn start(feature: &'static str) -> Self {
228 ensure_described();
229 Self {
230 feature,
231 start: Instant::now(),
232 }
233 }
234}
235
236impl Drop for Timer {
237 fn drop(&mut self) {
238 let elapsed = self.start.elapsed().as_secs_f64();
239 histogram!(
240 "repo_usecase_duration_seconds",
241 "feature" => self.feature.to_string()
242 )
243 .record(elapsed);
244 }
245}
246
247// ── Generic outcome counter ───────────────────────────────────────────────
248
249/// Bump invocation counter (always) and, on `Err`, an error counter labeled
250/// by `RepoError::error_kind()`. Drop semantics of `_timer` capture latency.
251pub fn record_outcome<T>(feature: &'static str, result: &Result<T, RepoError>) {
252 match result {
253 Ok(_) => {
254 counter!(
255 "repo_usecase_invocations_total",
256 "feature" => feature.to_string(),
257 "outcome" => OUTCOME_SUCCESS
258 )
259 .increment(1);
260 }
261 Err(e) => {
262 let kind = e.error_kind();
263 counter!(
264 "repo_usecase_invocations_total",
265 "feature" => feature.to_string(),
266 "outcome" => kind
267 )
268 .increment(1);
269 counter!(
270 "repo_usecase_errors_total",
271 "feature" => feature.to_string(),
272 "outcome" => kind
273 )
274 .increment(1);
275 }
276 }
277}
278
279// ── Repo create helper ────────────────────────────────────────────────────
280
281#[derive(Debug, Clone, Copy)]
282pub enum RepoCreateOutcome {
283 Success,
284 BadName,
285 Conflict,
286 Forbidden,
287 Other,
288}
289
290impl RepoCreateOutcome {
291 pub fn as_label(self) -> &'static str {
292 match self {
293 RepoCreateOutcome::Success => "success",
294 RepoCreateOutcome::BadName => "bad_name",
295 RepoCreateOutcome::Conflict => "conflict",
296 RepoCreateOutcome::Forbidden => "forbidden",
297 RepoCreateOutcome::Other => "other",
298 }
299 }
300}
301
302pub fn record_repo_create(outcome: RepoCreateOutcome) {
303 counter!(
304 "repo_usecase_invocations_total",
305 "feature" => FEATURE_REPOS_CREATE,
306 "outcome" => outcome.as_label()
307 )
308 .increment(1);
309}
310
311// ── Branch create helper ──────────────────────────────────────────────────
312
313#[derive(Debug, Clone, Copy)]
314pub enum BranchCreateOutcome {
315 Success,
316 BadName,
317 Conflict,
318 Forbidden,
319 Other,
320}
321
322impl BranchCreateOutcome {
323 pub fn as_label(self) -> &'static str {
324 match self {
325 BranchCreateOutcome::Success => "success",
326 BranchCreateOutcome::BadName => "bad_name",
327 BranchCreateOutcome::Conflict => "conflict",
328 BranchCreateOutcome::Forbidden => "forbidden",
329 BranchCreateOutcome::Other => "other",
330 }
331 }
332}
333
334pub fn record_branch_create(outcome: BranchCreateOutcome) {
335 counter!(
336 "repo_usecase_invocations_total",
337 "feature" => FEATURE_BRANCHES_CREATE,
338 "outcome" => outcome.as_label()
339 )
340 .increment(1);
341}
342
343// ── Pull request merge helper ─────────────────────────────────────────────
344
345#[derive(Debug, Clone, Copy)]
346pub enum PullRequestMergeOutcome {
347 Success,
348 NotFound,
349 Conflict,
350 Forbidden,
351 Other,
352}
353
354impl PullRequestMergeOutcome {
355 pub fn as_label(self) -> &'static str {
356 match self {
357 PullRequestMergeOutcome::Success => "success",
358 PullRequestMergeOutcome::NotFound => "not_found",
359 PullRequestMergeOutcome::Conflict => "conflict",
360 PullRequestMergeOutcome::Forbidden => "forbidden",
361 PullRequestMergeOutcome::Other => "other",
362 }
363 }
364}
365
366pub fn record_pull_request_merge(outcome: PullRequestMergeOutcome) {
367 counter!(
368 "repo_usecase_invocations_total",
369 "feature" => FEATURE_PULL_REQUESTS_MERGE,
370 "outcome" => outcome.as_label()
371 )
372 .increment(1);
373}
No newline at end of file