Skip to main content

wowlab_sentinel/
telemetry.rs

1pub(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";
7/// Histogram: chunk duration from assign to complete, in seconds.
8/// Histogram: time waiting on the per-job row lock during chunk completion.
9pub(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
103/// Initialize all sentinel metrics with default zero values so they appear in `/metrics`.
104pub(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}