use fast_telemetry::{
Counter, ExportMetrics, LabelEnum, LabeledCounter, LabeledGauge, LabeledHistogram,
};
#[derive(Copy, Clone, Debug, PartialEq)]
enum HttpMethod {
Get,
Post,
Put,
}
impl LabelEnum for HttpMethod {
const CARDINALITY: usize = 3;
const LABEL_NAME: &'static str = "method";
fn as_index(self) -> usize {
self as usize
}
fn from_index(index: usize) -> Self {
match index {
0 => Self::Get,
1 => Self::Post,
_ => Self::Put,
}
}
fn variant_name(self) -> &'static str {
match self {
Self::Get => "get",
Self::Post => "post",
Self::Put => "put",
}
}
}
#[derive(ExportMetrics)]
#[metric_prefix = "api"]
struct ApiMetrics {
#[help = "Total API requests"]
requests_total: Counter,
#[help = "API requests by HTTP method"]
requests_by_method: LabeledCounter<HttpMethod>,
#[help = "Queue depth per method"]
queue_depth: LabeledGauge<HttpMethod>,
#[help = "Request latency by method"]
latency_by_method: LabeledHistogram<HttpMethod>,
}
impl ApiMetrics {
fn new() -> Self {
Self {
requests_total: Counter::new(4),
requests_by_method: LabeledCounter::new(4),
queue_depth: LabeledGauge::new(),
latency_by_method: LabeledHistogram::new(&[100, 1000], 4),
}
}
}
#[test]
fn test_labeled_counter_export() {
let metrics = ApiMetrics::new();
metrics.requests_by_method.inc(HttpMethod::Get);
metrics.requests_by_method.inc(HttpMethod::Get);
metrics.requests_by_method.add(HttpMethod::Post, 5);
let mut output = String::new();
metrics.export_prometheus(&mut output);
assert!(output.contains("# HELP api_requests_by_method API requests by HTTP method"));
assert!(output.contains("# TYPE api_requests_by_method counter"));
assert!(output.contains("api_requests_by_method{method=\"get\"} 2"));
assert!(output.contains("api_requests_by_method{method=\"post\"} 5"));
assert!(output.contains("api_requests_by_method{method=\"put\"} 0"));
}
#[test]
fn test_labeled_gauge_export() {
let metrics = ApiMetrics::new();
metrics.queue_depth.set(HttpMethod::Get, 10);
metrics.queue_depth.set(HttpMethod::Post, 25);
let mut output = String::new();
metrics.export_prometheus(&mut output);
assert!(output.contains("# HELP api_queue_depth Queue depth per method"));
assert!(output.contains("# TYPE api_queue_depth gauge"));
assert!(output.contains("api_queue_depth{method=\"get\"} 10"));
assert!(output.contains("api_queue_depth{method=\"post\"} 25"));
assert!(output.contains("api_queue_depth{method=\"put\"} 0"));
}
#[test]
fn test_labeled_histogram_export() {
let metrics = ApiMetrics::new();
metrics.latency_by_method.record(HttpMethod::Get, 50);
metrics.latency_by_method.record(HttpMethod::Get, 500);
metrics.latency_by_method.record(HttpMethod::Get, 2000);
let mut output = String::new();
metrics.export_prometheus(&mut output);
assert!(output.contains("# HELP api_latency_by_method Request latency by method"));
assert!(output.contains("# TYPE api_latency_by_method histogram"));
assert!(output.contains("api_latency_by_method_bucket{method=\"get\",le=\"100\"} 1"));
assert!(output.contains("api_latency_by_method_bucket{method=\"get\",le=\"1000\"} 2"));
assert!(output.contains("api_latency_by_method_bucket{method=\"get\",le=\"+Inf\"} 3"));
assert!(output.contains("api_latency_by_method_count{method=\"get\"} 3"));
assert!(output.contains("api_latency_by_method_count{method=\"post\"} 0"));
}
#[test]
fn test_mixed_labeled_and_unlabeled() {
let metrics = ApiMetrics::new();
metrics.requests_total.inc();
metrics.requests_total.inc();
metrics.requests_by_method.inc(HttpMethod::Get);
let mut output = String::new();
metrics.export_prometheus(&mut output);
assert!(output.contains("api_requests_total 2"));
assert!(output.contains("api_requests_by_method{method=\"get\"} 1"));
}
#[test]
fn test_dogstatsd_export() {
let metrics = ApiMetrics::new();
metrics.requests_total.inc();
metrics.requests_total.inc();
metrics.requests_by_method.inc(HttpMethod::Get);
metrics.requests_by_method.add(HttpMethod::Post, 5);
metrics.queue_depth.set(HttpMethod::Get, 10);
let mut output = String::new();
metrics.export_dogstatsd(&mut output, &[]);
assert!(output.contains("api.requests_total:2|c\n"));
assert!(output.contains("api.requests_by_method:1|c|#method:get\n"));
assert!(output.contains("api.requests_by_method:5|c|#method:post\n"));
assert!(output.contains("api.queue_depth:10|g|#method:get\n"));
}
#[test]
fn test_dogstatsd_export_with_tags() {
let metrics = ApiMetrics::new();
metrics.requests_total.add(100);
let mut output = String::new();
metrics.export_dogstatsd(&mut output, &[("env", "prod"), ("region", "us-east")]);
assert!(output.contains("api.requests_total:100|c|#env:prod,region:us-east\n"));
}
#[test]
fn test_dogstatsd_labeled_histogram() {
let metrics = ApiMetrics::new();
metrics.latency_by_method.record(HttpMethod::Get, 100);
metrics.latency_by_method.record(HttpMethod::Get, 200);
let mut output = String::new();
metrics.export_dogstatsd(&mut output, &[]);
assert!(output.contains("api.latency_by_method.count:2|c|#method:get\n"));
assert!(output.contains("api.latency_by_method.sum:300|c|#method:get\n"));
}