fleetforge_telemetry/
metrics.rs1use once_cell::sync::Lazy;
2use opentelemetry::{
3 global,
4 metrics::{Counter, Histogram, Meter},
5 KeyValue,
6};
7
8struct 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
68pub 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
98pub 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
106pub 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}