1use rocket::http::ContentType;
23use rocket::{Route, State};
24
25use crate::backend::DbPool;
26use crate::frontend::actor::Actor;
27use crate::models::{Corpus, Service, Session, Task, WorkerMetadata};
28
29fn gauge(out: &mut String, name: &str, help: &str, value: impl std::fmt::Display) {
31 out.push_str(&format!(
32 "# HELP {name} {help}\n# TYPE {name} gauge\n{name} {value}\n"
33 ));
34}
35
36#[get("/metrics")]
41pub fn metrics(_caller: Actor, pool: &State<DbPool>) -> (ContentType, String) {
42 let mut out = String::new();
43
44 out.push_str("# HELP cortex_build_info CorTeX build information.\n");
45 out.push_str("# TYPE cortex_build_info gauge\n");
46 out.push_str(&format!(
47 "cortex_build_info{{version=\"{}\"}} 1\n",
48 env!("CARGO_PKG_VERSION")
49 ));
50
51 let state = pool.state();
53 gauge(
54 &mut out,
55 "cortex_pool_max",
56 "Maximum size of the frontend connection pool.",
57 pool.max_size(),
58 );
59 gauge(
60 &mut out,
61 "cortex_pool_connections",
62 "Connections currently established (idle + in use).",
63 state.connections,
64 );
65 gauge(
66 &mut out,
67 "cortex_pool_idle",
68 "Idle, immediately-available pooled connections.",
69 state.idle_connections,
70 );
71 gauge(
72 &mut out,
73 "cortex_pool_in_use",
74 "Pooled connections currently checked out (saturation signal).",
75 state.connections.saturating_sub(state.idle_connections),
76 );
77
78 match pool.get() {
81 Ok(mut connection) => {
82 gauge(
83 &mut out,
84 "cortex_db_reachable",
85 "1 if the database is reachable, else 0.",
86 1,
87 );
88 gauge(
89 &mut out,
90 "cortex_corpora_total",
91 "Registered corpora.",
92 Corpus::all(&mut connection).map_or(0, |corpora| corpora.len()),
93 );
94 gauge(
95 &mut out,
96 "cortex_services_total",
97 "Registered services.",
98 Service::all(&mut connection).map_or(0, |services| services.len()),
99 );
100 gauge(
101 &mut out,
102 "cortex_jobs_active",
103 "Background jobs currently queued or running.",
104 crate::jobs::list_recent(&mut connection, true, 1000).len(),
105 );
106 gauge(
107 &mut out,
108 "cortex_jobs_failed_recent",
109 "Background jobs that ended in `failed` within the last 24h (rolling window).",
110 crate::jobs::count_recent_with_status(&mut connection, "failed", 24),
111 );
112 gauge(
113 &mut out,
114 "cortex_jobs_interrupted_recent",
115 "Background jobs `interrupted` within the last 24h — stale-reaped (W-4) or restart orphans \
116 (rolling window).",
117 crate::jobs::count_recent_with_status(&mut connection, "interrupted", 24),
118 );
119 gauge(
120 &mut out,
121 "cortex_sessions_active",
122 "Active (unexpired) admin sessions.",
123 Session::active(&mut connection).map_or(0, |sessions| sessions.len()),
124 );
125 if let Ok((workers, in_flight)) = WorkerMetadata::fleet_summary(&mut connection) {
126 gauge(
127 &mut out,
128 "cortex_workers_total",
129 "Workers active in the last ~10 minutes (dispatched or returned a task) — the actively-converting fleet, not all registered rows. 0 when no dispatcher is running.",
130 workers,
131 );
132 gauge(
133 &mut out,
134 "cortex_workers_in_flight_total",
138 "Tasks in-flight (dispatched, not yet returned) at the active workers — real current in-flight work, 0 on an idle deployment.",
139 in_flight,
140 );
141 }
142 gauge(
148 &mut out,
149 "cortex_tasks_todo",
150 "Tasks awaiting conversion (status TODO, not yet dispatched) — the pending-work backlog.",
151 Task::count_todo(&mut connection),
152 );
153 },
154 Err(_) => gauge(
155 &mut out,
156 "cortex_db_reachable",
157 "1 if the database is reachable, else 0.",
158 0,
159 ),
160 }
161
162 (ContentType::Plain, out)
163}
164
165pub fn routes() -> Vec<Route> { routes![metrics] }