1use once_cell::sync::{Lazy, OnceCell};
8use prometheus::{
9 Counter, Histogram, HistogramOpts, HistogramVec, IntCounterVec, IntGaugeVec, Opts, Registry,
10};
11
12use super::prometheus_names::{frontend_perf, labels, name_prefix};
13use crate::MetricsRegistry;
14
15pub use super::prometheus_names::frontend_perf::{STAGE_DISPATCH, STAGE_PREPROCESS, STAGE_ROUTE};
16
17fn frontend_metric_name(suffix: &str) -> String {
18 format!("{}_{}", name_prefix::FRONTEND, suffix)
19}
20
21pub static STAGE_REQUESTS: Lazy<IntGaugeVec> = Lazy::new(|| {
24 IntGaugeVec::new(
25 Opts::new(
26 frontend_metric_name(frontend_perf::STAGE_REQUESTS),
27 "Number of requests currently in the given pipeline stage",
28 ),
29 &["stage", "phase"],
30 )
31 .expect("failed to create dynamo_frontend_stage_requests gauge")
32});
33
34pub struct StageGuard {
40 gauge: prometheus::IntGauge,
41}
42
43impl StageGuard {
44 pub fn new(stage: &str, phase: &str) -> Self {
51 let gauge = STAGE_REQUESTS.with_label_values(&[stage, phase]);
52 gauge.inc();
53 Self { gauge }
54 }
55}
56
57impl Drop for StageGuard {
58 fn drop(&mut self) {
59 self.gauge.dec();
60 }
61}
62
63pub static STAGE_DURATION_SECONDS: Lazy<HistogramVec> = Lazy::new(|| {
65 HistogramVec::new(
66 HistogramOpts::new(
67 frontend_metric_name(frontend_perf::STAGE_DURATION_SECONDS),
68 "Pipeline stage duration (seconds)",
69 )
70 .buckets(vec![
71 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0,
72 ]),
73 &["stage"],
74 )
75 .expect("stage_duration_seconds histogram vec")
76});
77
78pub static TOKENIZE_SECONDS: Lazy<Histogram> = Lazy::new(|| {
80 Histogram::with_opts(
81 HistogramOpts::new(
82 frontend_metric_name(frontend_perf::TOKENIZE_SECONDS),
83 "Tokenization time in preprocessor (seconds)",
84 )
85 .buckets(vec![
86 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0,
87 ]),
88 )
89 .expect("tokenize_seconds histogram")
90});
91
92pub static TEMPLATE_SECONDS: Lazy<Histogram> = Lazy::new(|| {
94 Histogram::with_opts(
95 HistogramOpts::new(
96 frontend_metric_name(frontend_perf::TEMPLATE_SECONDS),
97 "Template application time in preprocessor (seconds)",
98 )
99 .buckets(vec![
100 0.00001, 0.00005, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05,
101 ]),
102 )
103 .expect("template_seconds histogram")
104});
105
106pub static DETOKENIZE_TOTAL_US: Lazy<Counter> = Lazy::new(|| {
109 Counter::with_opts(Opts::new(
110 frontend_metric_name(frontend_perf::DETOKENIZE_TOTAL_US),
111 "Cumulative detokenization time (microseconds)",
112 ))
113 .expect("detokenize_total_us counter")
114});
115
116pub static DETOKENIZE_TOKEN_COUNT: Lazy<Counter> = Lazy::new(|| {
118 Counter::with_opts(Opts::new(
119 frontend_metric_name(frontend_perf::DETOKENIZE_TOKEN_COUNT),
120 "Total tokens detokenized",
121 ))
122 .expect("detokenize_token_count counter")
123});
124
125pub static TOKENIZER_CACHE_HITS_TOTAL: Lazy<Counter> = Lazy::new(|| {
127 Counter::with_opts(Opts::new(
128 frontend_metric_name(frontend_perf::TOKENIZER_CACHE_HITS_TOTAL),
129 "Cumulative L1 tokenizer prefix-cache hits",
130 ))
131 .expect("tokenizer_cache_hits_total counter")
132});
133
134pub static TOKENIZER_CACHE_MISSES_TOTAL: Lazy<Counter> = Lazy::new(|| {
136 Counter::with_opts(Opts::new(
137 frontend_metric_name(frontend_perf::TOKENIZER_CACHE_MISSES_TOTAL),
138 "Cumulative L1 tokenizer prefix-cache misses",
139 ))
140 .expect("tokenizer_cache_misses_total counter")
141});
142
143pub static TOKENIZER_CACHE_CACHED_TOKENS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
145 IntCounterVec::new(
146 Opts::new(
147 frontend_metric_name(frontend_perf::TOKENIZER_CACHE_CACHED_TOKENS_TOTAL),
148 "Total tokens returned from the L1 tokenizer prefix cache",
149 ),
150 &[labels::MODEL],
151 )
152 .expect("tokenizer_cache_cached_tokens_total counter vec")
153});
154
155pub static TOKENIZER_CACHE_UNCACHED_TOKENS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
157 IntCounterVec::new(
158 Opts::new(
159 frontend_metric_name(frontend_perf::TOKENIZER_CACHE_UNCACHED_TOKENS_TOTAL),
160 "Total tokens freshly encoded after an L1 tokenizer prefix-cache lookup",
161 ),
162 &[labels::MODEL],
163 )
164 .expect("tokenizer_cache_uncached_tokens_total counter vec")
165});
166
167static REGISTERED: OnceCell<()> = OnceCell::new();
169
170static PROMETHEUS_REGISTERED: OnceCell<()> = OnceCell::new();
174
175fn register_frontend_perf_metrics(registry: &MetricsRegistry) {
176 registry.add_metric(Box::new(STAGE_REQUESTS.clone())).ok();
177 registry
178 .add_metric(Box::new(STAGE_DURATION_SECONDS.clone()))
179 .ok();
180 registry.add_metric(Box::new(TOKENIZE_SECONDS.clone())).ok();
181 registry.add_metric(Box::new(TEMPLATE_SECONDS.clone())).ok();
182 registry
183 .add_metric(Box::new(DETOKENIZE_TOTAL_US.clone()))
184 .ok();
185 registry
186 .add_metric(Box::new(DETOKENIZE_TOKEN_COUNT.clone()))
187 .ok();
188 registry
189 .add_metric(Box::new(TOKENIZER_CACHE_HITS_TOTAL.clone()))
190 .ok();
191 registry
192 .add_metric(Box::new(TOKENIZER_CACHE_MISSES_TOTAL.clone()))
193 .ok();
194 registry
195 .add_metric(Box::new(TOKENIZER_CACHE_CACHED_TOKENS_TOTAL.clone()))
196 .ok();
197 registry
198 .add_metric(Box::new(TOKENIZER_CACHE_UNCACHED_TOKENS_TOTAL.clone()))
199 .ok();
200}
201
202fn register_frontend_perf_metrics_prometheus(registry: &Registry) -> Result<(), prometheus::Error> {
203 registry.register(Box::new(STAGE_REQUESTS.clone()))?;
204 registry.register(Box::new(STAGE_DURATION_SECONDS.clone()))?;
205 registry.register(Box::new(TOKENIZE_SECONDS.clone()))?;
206 registry.register(Box::new(TEMPLATE_SECONDS.clone()))?;
207 registry.register(Box::new(DETOKENIZE_TOTAL_US.clone()))?;
208 registry.register(Box::new(DETOKENIZE_TOKEN_COUNT.clone()))?;
209 registry.register(Box::new(TOKENIZER_CACHE_HITS_TOTAL.clone()))?;
210 registry.register(Box::new(TOKENIZER_CACHE_MISSES_TOTAL.clone()))?;
211 registry.register(Box::new(TOKENIZER_CACHE_CACHED_TOKENS_TOTAL.clone()))?;
212 registry.register(Box::new(TOKENIZER_CACHE_UNCACHED_TOKENS_TOTAL.clone()))?;
213 Ok(())
214}
215
216pub fn ensure_frontend_perf_metrics_registered(registry: &MetricsRegistry) {
218 let _ = REGISTERED.get_or_init(|| register_frontend_perf_metrics(registry));
219}
220
221pub fn ensure_frontend_perf_metrics_registered_prometheus(
224 registry: &Registry,
225) -> Result<(), prometheus::Error> {
226 if PROMETHEUS_REGISTERED.get().is_some() {
227 return Ok(());
228 }
229 register_frontend_perf_metrics_prometheus(registry)?;
230 let _ = PROMETHEUS_REGISTERED.set(());
231 Ok(())
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 fn assert_tokenizer_cache_token_metrics_registered(
239 families: &[prometheus::proto::MetricFamily],
240 model: &str,
241 ) {
242 for name in [
243 "dynamo_frontend_tokenizer_cache_cached_tokens_total",
244 "dynamo_frontend_tokenizer_cache_uncached_tokens_total",
245 ] {
246 let family = families
247 .iter()
248 .find(|family| family.name() == name)
249 .unwrap_or_else(|| panic!("missing metric family {name}"));
250 assert!(family.get_metric().iter().any(|metric| {
251 metric
252 .get_label()
253 .iter()
254 .any(|label| label.name() == labels::MODEL && label.value() == model)
255 }));
256 }
257 }
258
259 #[test]
260 fn test_tokenizer_cache_token_metrics_registered_with_model_label() {
261 let model = "frontend-perf-registration-test-model";
262 let _ = TOKENIZER_CACHE_CACHED_TOKENS_TOTAL.with_label_values(&[model]);
263 let _ = TOKENIZER_CACHE_UNCACHED_TOKENS_TOTAL.with_label_values(&[model]);
264
265 let metrics_registry = MetricsRegistry::new();
266 register_frontend_perf_metrics(&metrics_registry);
267 assert_tokenizer_cache_token_metrics_registered(
268 &metrics_registry.get_prometheus_registry().gather(),
269 model,
270 );
271
272 let prometheus_registry = Registry::new();
273 register_frontend_perf_metrics_prometheus(&prometheus_registry).unwrap();
274 assert_tokenizer_cache_token_metrics_registered(&prometheus_registry.gather(), model);
275 }
276
277 #[test]
278 fn test_stage_guard_inc_dec() {
279 let gauge = STAGE_REQUESTS.with_label_values(&["test_stage", "test_phase"]);
280 assert_eq!(gauge.get(), 0);
281
282 {
283 let _guard = StageGuard::new("test_stage", "test_phase");
284 assert_eq!(gauge.get(), 1);
285
286 {
287 let _guard2 = StageGuard::new("test_stage", "test_phase");
288 assert_eq!(gauge.get(), 2);
289 }
290 assert_eq!(gauge.get(), 1);
292 }
293 assert_eq!(gauge.get(), 0);
295 }
296
297 #[test]
298 fn test_stage_guard_different_labels() {
299 let preprocess = STAGE_REQUESTS.with_label_values(&["preprocess_t", ""]);
300 let route_prefill = STAGE_REQUESTS.with_label_values(&["route_t", "prefill"]);
301 let route_decode = STAGE_REQUESTS.with_label_values(&["route_t", "decode"]);
302
303 let _g1 = StageGuard::new("preprocess_t", "");
304 let _g2 = StageGuard::new("route_t", "prefill");
305 let _g3 = StageGuard::new("route_t", "decode");
306
307 assert_eq!(preprocess.get(), 1);
308 assert_eq!(route_prefill.get(), 1);
309 assert_eq!(route_decode.get(), 1);
310
311 drop(_g2);
312 assert_eq!(preprocess.get(), 1);
313 assert_eq!(route_prefill.get(), 0);
314 assert_eq!(route_decode.get(), 1);
315 }
316}