fleetforge_telemetry/
metrics.rs

1use once_cell::sync::Lazy;
2use opentelemetry::{
3    global,
4    metrics::{Counter, Histogram, Meter},
5    KeyValue,
6};
7
8/// Lazily initialised OpenTelemetry instruments for GenAI observability.
9struct GenAiMetrics {
10    prompt_tokens: Counter<u64>,
11    completion_tokens: Counter<u64>,
12    total_tokens: Counter<u64>,
13    cost_usd: Counter<f64>,
14    duration_ms: Histogram<f64>,
15    policy_events: Counter<u64>,
16}
17
18static GENAI_METRICS: Lazy<GenAiMetrics> = Lazy::new(GenAiMetrics::new);
19
20impl GenAiMetrics {
21    fn new() -> Self {
22        let meter: Meter = global::meter("fleetforge.genai");
23        let prompt_tokens = meter
24            .u64_counter("gen_ai.prompt.tokens")
25            .with_description("Total prompt tokens processed by FleetForge agents")
26            .init();
27        let completion_tokens = meter
28            .u64_counter("gen_ai.completion.tokens")
29            .with_description("Total completion tokens returned by FleetForge agents")
30            .init();
31        let total_tokens = meter
32            .u64_counter("gen_ai.tokens.total")
33            .with_description("Total tokens (prompt + completion) recorded per request")
34            .init();
35        let cost_usd = meter
36            .f64_counter("gen_ai.cost.usd")
37            .with_description("Total USD cost reported by providers per request")
38            .init();
39        let duration_ms = meter
40            .f64_histogram("gen_ai.request.duration")
41            .with_description("Latency of GenAI requests executed by FleetForge (milliseconds)")
42            .init();
43        let policy_events = meter
44            .u64_counter("fleetforge.policy.events")
45            .with_description("Count of policy decisions emitted by guardrails/packs")
46            .init();
47
48        Self {
49            prompt_tokens,
50            completion_tokens,
51            total_tokens,
52            cost_usd,
53            duration_ms,
54            policy_events,
55        }
56    }
57}
58
59fn attributes(system: &str, model: &str) -> Vec<KeyValue> {
60    let mut attrs = Vec::with_capacity(2);
61    attrs.push(KeyValue::new("gen_ai.system", system.to_string()));
62    if !model.is_empty() {
63        attrs.push(KeyValue::new("gen_ai.request.model", model.to_string()));
64    }
65    attrs
66}
67
68/// Record prompt/completion token usage and optional cost for a GenAI request.
69pub fn record_genai_usage(
70    system: &str,
71    model: &str,
72    prompt_tokens: Option<i64>,
73    completion_tokens: Option<i64>,
74    total_tokens: Option<i64>,
75    cost_usd: Option<f64>,
76) {
77    let attrs = attributes(system, model);
78    if let Some(value) = prompt_tokens.and_then(|v| (v >= 0).then_some(v as u64)) {
79        GENAI_METRICS.prompt_tokens.add(value, &attrs);
80    }
81    if let Some(value) = completion_tokens.and_then(|v| (v >= 0).then_some(v as u64)) {
82        GENAI_METRICS.completion_tokens.add(value, &attrs);
83    }
84    if let Some(value) = total_tokens
85        .or_else(|| match (prompt_tokens, completion_tokens) {
86            (Some(p), Some(c)) => Some(p + c),
87            _ => None,
88        })
89        .and_then(|v| (v >= 0).then_some(v as u64))
90    {
91        GENAI_METRICS.total_tokens.add(value, &attrs);
92    }
93    if let Some(cost) = cost_usd {
94        GENAI_METRICS.cost_usd.add(cost.max(0.0), &attrs);
95    }
96}
97
98/// Record the end-to-end duration for a GenAI request.
99pub fn record_genai_duration(system: &str, model: &str, duration_ms: f64) {
100    let attrs = attributes(system, model);
101    GENAI_METRICS
102        .duration_ms
103        .record(duration_ms.max(0.0), &attrs);
104}
105
106/// Record a policy decision event to support guardrail burst dashboards.
107pub fn record_policy_event(effect: &str, pack: Option<&str>) {
108    let mut attrs = vec![KeyValue::new(
109        "fleetforge.policy.effect",
110        effect.to_string(),
111    )];
112    if let Some(pack_id) = pack {
113        attrs.push(KeyValue::new("fleetforge.policy.pack", pack_id.to_string()));
114    }
115    GENAI_METRICS.policy_events.add(1, &attrs);
116}