1use std::collections::HashMap;
28use std::time::{SystemTime, UNIX_EPOCH};
29
30type MetricBatchEntry = (String, MetricValue, Vec<(String, String)>);
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ExportFormat {
36 StatsD,
38 InfluxDB,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq)]
44pub enum MetricValue {
45 Counter(u64),
47 Gauge(i64),
49 Timing(u64),
51 Histogram(f64),
53}
54
55impl MetricValue {
56 #[must_use]
58 #[inline]
59 const fn statsd_type(&self) -> &'static str {
60 match self {
61 Self::Counter(_) => "c",
62 Self::Gauge(_) => "g",
63 Self::Timing(_) => "ms",
64 Self::Histogram(_) => "h",
65 }
66 }
67
68 #[must_use]
70 #[inline]
71 fn value_string(&self) -> String {
72 match self {
73 Self::Counter(v) => v.to_string(),
74 Self::Gauge(v) => v.to_string(),
75 Self::Timing(v) => v.to_string(),
76 Self::Histogram(v) => v.to_string(),
77 }
78 }
79}
80
81pub struct MetricsExporter {
83 format: ExportFormat,
84 default_tags: HashMap<String, String>,
85}
86
87impl MetricsExporter {
88 #[must_use]
90 pub fn new(format: ExportFormat) -> Self {
91 Self {
92 format,
93 default_tags: HashMap::new(),
94 }
95 }
96
97 #[must_use]
99 pub fn with_tags(format: ExportFormat, tags: &[(&str, &str)]) -> Self {
100 let default_tags: HashMap<String, String> = tags
101 .iter()
102 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
103 .collect();
104
105 Self {
106 format,
107 default_tags,
108 }
109 }
110
111 pub fn add_default_tag(&mut self, key: String, value: String) {
113 self.default_tags.insert(key, value);
114 }
115
116 #[must_use]
118 pub fn export_counter(&self, name: &str, value: u64, tags: &[(&str, &str)]) -> String {
119 self.export_metric(name, MetricValue::Counter(value), tags)
120 }
121
122 #[must_use]
124 pub fn export_gauge(&self, name: &str, value: i64, tags: &[(&str, &str)]) -> String {
125 self.export_metric(name, MetricValue::Gauge(value), tags)
126 }
127
128 #[must_use]
130 pub fn export_timing(&self, name: &str, duration_ms: u64, tags: &[(&str, &str)]) -> String {
131 self.export_metric(name, MetricValue::Timing(duration_ms), tags)
132 }
133
134 #[must_use]
136 pub fn export_histogram(&self, name: &str, value: f64, tags: &[(&str, &str)]) -> String {
137 self.export_metric(name, MetricValue::Histogram(value), tags)
138 }
139
140 #[must_use]
142 pub fn export_metric(&self, name: &str, value: MetricValue, tags: &[(&str, &str)]) -> String {
143 match self.format {
144 ExportFormat::StatsD => self.format_statsd(name, value, tags),
145 ExportFormat::InfluxDB => self.format_influxdb(name, value, tags),
146 }
147 }
148
149 #[must_use]
151 fn format_statsd(&self, name: &str, value: MetricValue, tags: &[(&str, &str)]) -> String {
152 let mut parts = vec![format!("{}:{}", name, value.value_string())];
153 parts.push(value.statsd_type().to_string());
154
155 let all_tags = self.merge_tags(tags);
157 if !all_tags.is_empty() {
158 let tag_str: Vec<String> = all_tags
159 .iter()
160 .map(|(k, v)| format!("{}:{}", k, v))
161 .collect();
162 parts.push(format!("#{}", tag_str.join(",")));
163 }
164
165 parts.join("|")
166 }
167
168 #[must_use]
170 fn format_influxdb(&self, name: &str, value: MetricValue, tags: &[(&str, &str)]) -> String {
171 let all_tags = self.merge_tags(tags);
172
173 let mut measurement = name.to_string();
175 if !all_tags.is_empty() {
176 let tag_str: Vec<String> = all_tags
177 .iter()
178 .map(|(k, v)| format!("{}={}", escape_influx_key(k), escape_influx_value(v)))
179 .collect();
180 measurement.push(',');
181 measurement.push_str(&tag_str.join(","));
182 }
183
184 let field_name = "value";
186 let field_value = value.value_string();
187
188 let timestamp = SystemTime::now()
190 .duration_since(UNIX_EPOCH)
191 .unwrap_or_default()
192 .as_nanos();
193
194 format!(
195 "{} {}={} {}",
196 measurement, field_name, field_value, timestamp
197 )
198 }
199
200 #[must_use]
202 fn merge_tags(&self, tags: &[(&str, &str)]) -> HashMap<String, String> {
203 let mut all_tags = self.default_tags.clone();
204 for (k, v) in tags {
205 all_tags.insert((*k).to_string(), (*v).to_string());
206 }
207 all_tags
208 }
209
210 #[must_use]
212 pub fn export_batch(&self, metrics: &[MetricBatchEntry]) -> Vec<String> {
213 metrics
214 .iter()
215 .map(|(name, value, tags)| {
216 let tag_refs: Vec<(&str, &str)> =
217 tags.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
218 self.export_metric(name, *value, &tag_refs)
219 })
220 .collect()
221 }
222}
223
224#[must_use]
226#[inline]
227fn escape_influx_key(s: &str) -> String {
228 s.replace(',', "\\,")
229 .replace('=', "\\=")
230 .replace(' ', "\\ ")
231}
232
233#[must_use]
235#[inline]
236fn escape_influx_value(s: &str) -> String {
237 s.replace(',', "\\,")
238 .replace('=', "\\=")
239 .replace(' ', "\\ ")
240}
241
242pub struct MetricsBatch {
244 metrics: Vec<MetricBatchEntry>,
245}
246
247impl Default for MetricsBatch {
248 fn default() -> Self {
249 Self::new()
250 }
251}
252
253impl MetricsBatch {
254 #[must_use]
256 pub fn new() -> Self {
257 Self {
258 metrics: Vec::new(),
259 }
260 }
261
262 pub fn add_counter(mut self, name: String, value: u64, tags: Vec<(String, String)>) -> Self {
264 self.metrics.push((name, MetricValue::Counter(value), tags));
265 self
266 }
267
268 pub fn add_gauge(mut self, name: String, value: i64, tags: Vec<(String, String)>) -> Self {
270 self.metrics.push((name, MetricValue::Gauge(value), tags));
271 self
272 }
273
274 pub fn add_timing(
276 mut self,
277 name: String,
278 duration_ms: u64,
279 tags: Vec<(String, String)>,
280 ) -> Self {
281 self.metrics
282 .push((name, MetricValue::Timing(duration_ms), tags));
283 self
284 }
285
286 pub fn add_histogram(mut self, name: String, value: f64, tags: Vec<(String, String)>) -> Self {
288 self.metrics
289 .push((name, MetricValue::Histogram(value), tags));
290 self
291 }
292
293 #[must_use]
295 pub fn export(&self, exporter: &MetricsExporter) -> Vec<String> {
296 exporter.export_batch(&self.metrics)
297 }
298
299 #[must_use]
301 #[inline]
302 pub fn len(&self) -> usize {
303 self.metrics.len()
304 }
305
306 #[must_use]
308 #[inline]
309 pub fn is_empty(&self) -> bool {
310 self.metrics.is_empty()
311 }
312}
313
314pub struct CommonMetrics;
316
317impl CommonMetrics {
318 #[must_use]
320 pub fn storage_metrics(
321 exporter: &MetricsExporter,
322 used_bytes: u64,
323 total_bytes: u64,
324 chunk_count: u64,
325 ) -> Vec<String> {
326 vec![
327 exporter.export_gauge("chie.storage.used_bytes", used_bytes as i64, &[]),
328 exporter.export_gauge("chie.storage.total_bytes", total_bytes as i64, &[]),
329 exporter.export_gauge("chie.storage.chunks_count", chunk_count as i64, &[]),
330 ]
331 }
332
333 #[must_use]
335 pub fn bandwidth_metrics(
336 exporter: &MetricsExporter,
337 bytes_sent: u64,
338 bytes_received: u64,
339 requests_served: u64,
340 ) -> Vec<String> {
341 vec![
342 exporter.export_counter("chie.bandwidth.bytes_sent", bytes_sent, &[]),
343 exporter.export_counter("chie.bandwidth.bytes_received", bytes_received, &[]),
344 exporter.export_counter("chie.bandwidth.requests_served", requests_served, &[]),
345 ]
346 }
347
348 #[must_use]
350 pub fn performance_metrics(
351 exporter: &MetricsExporter,
352 avg_latency_ms: u64,
353 p95_latency_ms: u64,
354 p99_latency_ms: u64,
355 ) -> Vec<String> {
356 vec![
357 exporter.export_timing("chie.performance.latency.avg", avg_latency_ms, &[]),
358 exporter.export_timing("chie.performance.latency.p95", p95_latency_ms, &[]),
359 exporter.export_timing("chie.performance.latency.p99", p99_latency_ms, &[]),
360 ]
361 }
362
363 #[must_use]
365 pub fn cache_metrics(
366 exporter: &MetricsExporter,
367 hits: u64,
368 misses: u64,
369 evictions: u64,
370 ) -> Vec<String> {
371 vec![
372 exporter.export_counter("chie.cache.hits", hits, &[]),
373 exporter.export_counter("chie.cache.misses", misses, &[]),
374 exporter.export_counter("chie.cache.evictions", evictions, &[]),
375 ]
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_statsd_counter() {
385 let exporter = MetricsExporter::new(ExportFormat::StatsD);
386 let output = exporter.export_counter("test.counter", 42, &[]);
387 assert_eq!(output, "test.counter:42|c");
388 }
389
390 #[test]
391 fn test_statsd_gauge() {
392 let exporter = MetricsExporter::new(ExportFormat::StatsD);
393 let output = exporter.export_gauge("test.gauge", -10, &[]);
394 assert_eq!(output, "test.gauge:-10|g");
395 }
396
397 #[test]
398 fn test_statsd_timing() {
399 let exporter = MetricsExporter::new(ExportFormat::StatsD);
400 let output = exporter.export_timing("test.timing", 250, &[]);
401 assert_eq!(output, "test.timing:250|ms");
402 }
403
404 #[test]
405 fn test_statsd_with_tags() {
406 let exporter = MetricsExporter::new(ExportFormat::StatsD);
407 let output =
408 exporter.export_counter("test.counter", 1, &[("host", "server1"), ("env", "prod")]);
409 assert!(output.contains("test.counter:1|c|#"));
410 assert!(output.contains("host:server1"));
411 assert!(output.contains("env:prod"));
412 }
413
414 #[test]
415 fn test_influxdb_format() {
416 let exporter = MetricsExporter::new(ExportFormat::InfluxDB);
417 let output = exporter.export_counter("test_counter", 42, &[("host", "server1")]);
418 assert!(output.contains("test_counter,host=server1"));
419 assert!(output.contains("value=42"));
420 }
421
422 #[test]
423 fn test_default_tags() {
424 let exporter = MetricsExporter::with_tags(
425 ExportFormat::StatsD,
426 &[("app", "chie"), ("version", "0.1.0")],
427 );
428 let output = exporter.export_counter("test.counter", 1, &[]);
429 assert!(output.contains("app:chie"));
430 assert!(output.contains("version:0.1.0"));
431 }
432
433 #[test]
434 fn test_metrics_batch() {
435 let batch = MetricsBatch::new()
436 .add_counter("counter".to_string(), 10, vec![])
437 .add_gauge("gauge".to_string(), -5, vec![])
438 .add_timing("timing".to_string(), 100, vec![]);
439
440 assert_eq!(batch.len(), 3);
441 assert!(!batch.is_empty());
442
443 let exporter = MetricsExporter::new(ExportFormat::StatsD);
444 let output = batch.export(&exporter);
445 assert_eq!(output.len(), 3);
446 }
447
448 #[test]
449 fn test_common_storage_metrics() {
450 let exporter = MetricsExporter::new(ExportFormat::StatsD);
451 let metrics = CommonMetrics::storage_metrics(&exporter, 1024, 2048, 10);
452 assert_eq!(metrics.len(), 3);
453 assert!(metrics[0].contains("chie.storage.used_bytes"));
454 assert!(metrics[1].contains("chie.storage.total_bytes"));
455 assert!(metrics[2].contains("chie.storage.chunks_count"));
456 }
457
458 #[test]
459 fn test_influx_escaping() {
460 assert_eq!(escape_influx_key("test,key"), "test\\,key");
461 assert_eq!(escape_influx_key("test=key"), "test\\=key");
462 assert_eq!(escape_influx_key("test key"), "test\\ key");
463 }
464
465 #[test]
466 fn test_metric_value_types() {
467 assert_eq!(MetricValue::Counter(1).statsd_type(), "c");
468 assert_eq!(MetricValue::Gauge(1).statsd_type(), "g");
469 assert_eq!(MetricValue::Timing(1).statsd_type(), "ms");
470 assert_eq!(MetricValue::Histogram(1.0).statsd_type(), "h");
471 }
472}