forge_runtime/observability/
metrics.rs1use opentelemetry::{
2 KeyValue, global,
3 metrics::{Counter, Histogram, UpDownCounter},
4};
5use std::sync::OnceLock;
6
7const METER_NAME: &str = "forge-runtime";
8
9static HTTP_METRICS: OnceLock<HttpMetrics> = OnceLock::new();
10static FN_METRICS: OnceLock<FnMetrics> = OnceLock::new();
11static JOB_METRICS: OnceLock<JobMetrics> = OnceLock::new();
12static CONNECTIONS_GAUGE: OnceLock<ActiveConnectionsGauge> = OnceLock::new();
13
14pub struct HttpMetrics {
15 requests_total: Counter<u64>,
16 request_duration: Histogram<f64>,
17}
18
19impl HttpMetrics {
20 fn new() -> Self {
21 let meter = global::meter(METER_NAME);
22
23 let requests_total = meter
24 .u64_counter("http_requests_total")
25 .with_description("Total number of HTTP requests")
26 .with_unit("requests")
27 .build();
28
29 let request_duration = meter
30 .f64_histogram("http_request_duration_seconds")
31 .with_description("HTTP request duration in seconds")
32 .with_unit("s")
33 .build();
34
35 Self {
36 requests_total,
37 request_duration,
38 }
39 }
40
41 pub fn record(&self, method: &str, path: &str, status: u16, duration_secs: f64) {
42 let attributes = [
43 KeyValue::new("method", method.to_string()),
44 KeyValue::new("path", path.to_string()),
45 KeyValue::new("status", i64::from(status)),
46 ];
47
48 self.requests_total.add(1, &attributes);
49 self.request_duration.record(duration_secs, &attributes);
50 }
51}
52
53pub struct FnMetrics {
54 executions_total: Counter<u64>,
55 duration: Histogram<f64>,
56}
57
58impl FnMetrics {
59 fn new() -> Self {
60 let meter = global::meter(METER_NAME);
61
62 let executions_total = meter
63 .u64_counter("fn.executions_total")
64 .with_description("Total function executions")
65 .with_unit("executions")
66 .build();
67
68 let duration = meter
69 .f64_histogram("fn.duration_seconds")
70 .with_description("Function execution duration")
71 .with_unit("s")
72 .build();
73
74 Self {
75 executions_total,
76 duration,
77 }
78 }
79
80 pub fn record(&self, function: &str, kind: &str, success: bool, duration_secs: f64) {
81 let status = if success { "ok" } else { "error" };
82 let attributes = [
83 KeyValue::new("function", function.to_string()),
84 KeyValue::new("kind", kind.to_string()),
85 KeyValue::new("status", status),
86 ];
87
88 self.executions_total.add(1, &attributes);
89 self.duration.record(duration_secs, &attributes);
90 }
91}
92
93pub struct JobMetrics {
94 executions_total: Counter<u64>,
95 duration: Histogram<f64>,
96}
97
98impl JobMetrics {
99 fn new() -> Self {
100 let meter = global::meter(METER_NAME);
101
102 let executions_total = meter
103 .u64_counter("job_executions_total")
104 .with_description("Total number of job executions")
105 .with_unit("executions")
106 .build();
107
108 let duration = meter
109 .f64_histogram("job_duration_seconds")
110 .with_description("Job execution duration in seconds")
111 .with_unit("s")
112 .build();
113
114 Self {
115 executions_total,
116 duration,
117 }
118 }
119
120 pub fn record(&self, job_type: &str, status: &str, duration_secs: f64) {
121 let attributes = [
122 KeyValue::new("job_type", job_type.to_string()),
123 KeyValue::new("status", status.to_string()),
124 ];
125
126 self.executions_total.add(1, &attributes);
127 self.duration.record(duration_secs, &attributes);
128 }
129}
130
131pub struct ActiveConnectionsGauge {
132 gauge: UpDownCounter<i64>,
133}
134
135impl ActiveConnectionsGauge {
136 fn new() -> Self {
137 let meter = global::meter(METER_NAME);
138
139 let gauge = meter
140 .i64_up_down_counter("active_connections")
141 .with_description("Number of active connections")
142 .with_unit("connections")
143 .build();
144
145 Self { gauge }
146 }
147
148 pub fn increment(&self, connection_type: &str) {
149 self.gauge
150 .add(1, &[KeyValue::new("type", connection_type.to_string())]);
151 }
152
153 pub fn decrement(&self, connection_type: &str) {
154 self.gauge
155 .add(-1, &[KeyValue::new("type", connection_type.to_string())]);
156 }
157
158 pub fn set(&self, connection_type: &str, delta: i64) {
159 self.gauge
160 .add(delta, &[KeyValue::new("type", connection_type.to_string())]);
161 }
162}
163
164fn http_metrics() -> &'static HttpMetrics {
165 HTTP_METRICS.get_or_init(HttpMetrics::new)
166}
167
168fn fn_metrics() -> &'static FnMetrics {
169 FN_METRICS.get_or_init(FnMetrics::new)
170}
171
172fn job_metrics() -> &'static JobMetrics {
173 JOB_METRICS.get_or_init(JobMetrics::new)
174}
175
176fn connections_gauge() -> &'static ActiveConnectionsGauge {
177 CONNECTIONS_GAUGE.get_or_init(ActiveConnectionsGauge::new)
178}
179
180pub fn record_http_request(method: &str, path: &str, status: u16, duration_secs: f64) {
181 http_metrics().record(method, path, status, duration_secs);
182}
183
184pub fn record_fn_execution(function: &str, kind: &str, success: bool, duration_secs: f64) {
185 fn_metrics().record(function, kind, success, duration_secs);
186}
187
188pub fn record_job_execution(job_type: &str, status: &str, duration_secs: f64) {
189 job_metrics().record(job_type, status, duration_secs);
190}
191
192pub fn set_active_connections(connection_type: &str, delta: i64) {
193 connections_gauge().set(connection_type, delta);
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_http_metrics_creation() {
202 let _metrics = HttpMetrics::new();
203 }
204
205 #[test]
206 fn test_job_metrics_creation() {
207 let _metrics = JobMetrics::new();
208 }
209
210 #[test]
211 fn test_connections_gauge_creation() {
212 let _gauge = ActiveConnectionsGauge::new();
213 }
214}