wowlab_sentinel/
telemetry.rs1pub(crate) const CHUNKS_ASSIGNED: &str = "sentinel_chunks_assigned_total";
2pub(crate) const CHUNKS_COMPLETED: &str = "sentinel_chunks_completed_total";
3pub(crate) const CHUNKS_FAILED: &str = "sentinel_chunks_failed_total";
4pub(crate) const CHUNKS_PENDING: &str = "sentinel_chunks_pending";
5pub(crate) const CHUNKS_RECLAIMED: &str = "sentinel_chunks_reclaimed_total";
6pub(crate) const CHUNKS_RUNNING: &str = "sentinel_chunks_running";
7pub(crate) const NODES_ONLINE: &str = "sentinel_nodes_online";
10pub(crate) const NODES_MARKED_OFFLINE: &str = "sentinel_nodes_marked_offline_total";
11pub(crate) const PRESENCE_POLLS: &str = "sentinel_presence_polls_total";
12const WEBHOOKS_RECEIVED: &str = "sentinel_webhooks_received_total";
13pub(crate) const UPTIME_SECONDS: &str = "sentinel_uptime_seconds";
14const MCP_REQUESTS: &str = "sentinel_mcp_requests_total";
15const MCP_REQUEST_DURATION: &str = "sentinel_mcp_request_duration_seconds";
16const MCP_QUERY_ROWS: &str = "sentinel_mcp_query_rows_total";
17const MCP_ERRORS: &str = "sentinel_mcp_errors_total";
18
19use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
20
21#[derive(Clone, Copy, Debug, EnumIter, IntoStaticStr)]
22#[non_exhaustive]
23pub enum DeployEvent {
24 #[strum(serialize = "succeeded")]
25 Succeeded,
26 #[strum(serialize = "failed")]
27 Failed,
28 #[strum(serialize = "building")]
29 Building,
30}
31
32#[derive(Clone, Copy, Debug, EnumIter, IntoStaticStr)]
33#[non_exhaustive]
34pub enum McpTool {
35 #[strum(serialize = "get_schema")]
36 GetSchema,
37 #[strum(serialize = "list_tables")]
38 ListTables,
39 #[strum(serialize = "query")]
40 Query,
41 #[strum(serialize = "query_batch")]
42 QueryBatch,
43 #[strum(serialize = "decode_loadout")]
44 DecodeLoadout,
45 #[strum(serialize = "resolve_effects")]
46 ResolveEffects,
47 #[strum(serialize = "get_docs")]
48 GetDocs,
49 #[strum(serialize = "query_count")]
50 QueryCount,
51 #[strum(serialize = "get_rotation")]
52 GetRotation,
53 #[strum(serialize = "list_rotations")]
54 ListRotations,
55 #[strum(serialize = "list_spell_labels")]
56 ListSpellLabels,
57 #[strum(serialize = "parse_simc")]
58 ParseSimc,
59}
60
61#[derive(Clone, Copy, Debug, EnumIter, IntoStaticStr)]
62#[non_exhaustive]
63pub enum McpErrorType {
64 #[strum(serialize = "invalid_params")]
65 InvalidParams,
66 #[strum(serialize = "timeout")]
67 Timeout,
68 #[strum(serialize = "database")]
69 Database,
70 #[strum(serialize = "internal")]
71 Internal,
72}
73
74pub(crate) fn record_deploy_webhook(event: DeployEvent) {
75 let event: &'static str = event.into();
76
77 metrics::counter!(WEBHOOKS_RECEIVED, "source" => "deploy", "event" => event).increment(1);
78}
79
80pub(crate) fn record_mcp_request(tool: McpTool) {
81 let tool: &'static str = tool.into();
82
83 metrics::counter!(MCP_REQUESTS, "tool" => tool).increment(1);
84}
85
86pub(crate) fn record_mcp_duration(tool: McpTool, duration_secs: f64) {
87 let tool: &'static str = tool.into();
88
89 metrics::histogram!(MCP_REQUEST_DURATION, "tool" => tool).record(duration_secs);
90}
91
92pub(crate) fn record_mcp_query_rows(count: u64) {
93 metrics::counter!(MCP_QUERY_ROWS).increment(count);
94}
95
96pub(crate) fn record_mcp_error(tool: McpTool, error_type: McpErrorType) {
97 let tool: &'static str = tool.into();
98 let error_type: &'static str = error_type.into();
99
100 metrics::counter!(MCP_ERRORS, "tool" => tool, "error_type" => error_type).increment(1);
101}
102
103pub(crate) fn init() {
105 init_gauges();
106 init_counters();
107 init_webhook_counters();
108 init_mcp_counters();
109}
110
111pub(crate) async fn init_running_chunks_gauge(db: &sqlx::PgPool) {
112 struct CountRow {
113 count: Option<i64>,
114 }
115
116 let running: Result<CountRow, _> =
117 sqlx::query_file_as!(CountRow, "queries/telemetry_init_running_gauge.sql")
118 .fetch_one(db)
119 .await;
120
121 if let Ok(row) = running {
122 let count = row.count.unwrap_or(0);
123 #[expect(
124 clippy::cast_precision_loss,
125 reason = "metrics gauges accept f64 and running chunk counts remain exactly representable"
126 )]
127 let count_sample = count as f64;
128
129 metrics::gauge!(CHUNKS_RUNNING).set(count_sample);
130 tracing::info!(count, "Initialized CHUNKS_RUNNING gauge from database");
131 }
132}
133
134fn init_gauges() {
135 for name in [CHUNKS_PENDING, CHUNKS_RUNNING, NODES_ONLINE, UPTIME_SECONDS] {
136 metrics::gauge!(name).set(0.0);
137 }
138}
139
140fn init_counters() {
141 for name in [
142 CHUNKS_ASSIGNED,
143 CHUNKS_COMPLETED,
144 CHUNKS_FAILED,
145 CHUNKS_RECLAIMED,
146 NODES_MARKED_OFFLINE,
147 PRESENCE_POLLS,
148 ] {
149 metrics::counter!(name).absolute(0);
150 }
151}
152
153fn init_webhook_counters() {
154 for event in DeployEvent::iter() {
155 let event: &'static str = event.into();
156
157 metrics::counter!(WEBHOOKS_RECEIVED, "source" => "deploy", "event" => event).absolute(0);
158 }
159}
160
161fn init_mcp_counters() {
162 for tool in McpTool::iter() {
163 let tool_label: &'static str = tool.into();
164
165 metrics::counter!(MCP_REQUESTS, "tool" => tool_label).absolute(0);
166 }
167
168 for tool in McpTool::iter() {
169 for error_type in McpErrorType::iter() {
170 let tool_label: &'static str = tool.into();
171 let error_label: &'static str = error_type.into();
172
173 metrics::counter!(MCP_ERRORS, "tool" => tool_label, "error_type" => error_label)
174 .absolute(0);
175 }
176 }
177
178 metrics::counter!(MCP_QUERY_ROWS).absolute(0);
179}