use crate::metrics::{CounterDataPoint, Exemplar, ExemplarValue, GaugeDataPoint, MetricSnapshot};
use crate::otlp_trace::{encode_resource, encode_scope, KeyValue};
use crate::proto::*;
fn encode_attr_key_value(buf: &mut Vec<u8>, key: &str, value: &str) {
encode_string_field(buf, 1, key);
encode_message_field_in_place(buf, 2, |buf| {
encode_string_field(buf, 1, value); });
}
fn encode_exemplar(buf: &mut Vec<u8>, exemplar: &Exemplar) {
encode_fixed64_field(buf, 2, exemplar.time_unix_nano);
match exemplar.value {
ExemplarValue::Double(v) => {
encode_fixed64_field_always(buf, 3, v.to_bits());
}
ExemplarValue::Int(v) => {
encode_fixed64_field_always(buf, 6, v as u64);
}
}
encode_bytes_field(buf, 4, &exemplar.span_id);
encode_bytes_field(buf, 5, &exemplar.trace_id);
}
fn encode_counter_data_point(
buf: &mut Vec<u8>,
attrs: &[(String, String)],
start_time_unix_nano: u64,
time_unix_nano: u64,
value: i64,
exemplar: &Option<Exemplar>,
) {
encode_fixed64_field(buf, 2, start_time_unix_nano);
encode_fixed64_field(buf, 3, time_unix_nano);
if let Some(ex) = exemplar {
encode_message_field_in_place(buf, 5, |buf| {
encode_exemplar(buf, ex);
});
}
encode_fixed64_field_always(buf, 6, value as u64);
for (k, v) in attrs {
encode_message_field_in_place(buf, 7, |buf| {
encode_attr_key_value(buf, k, v);
});
}
}
fn encode_gauge_data_point(
buf: &mut Vec<u8>,
attrs: &[(String, String)],
start_time_unix_nano: u64,
time_unix_nano: u64,
value: f64,
exemplar: &Option<Exemplar>,
) {
encode_fixed64_field(buf, 2, start_time_unix_nano);
encode_fixed64_field(buf, 3, time_unix_nano);
encode_fixed64_field_always(buf, 4, value.to_bits());
if let Some(ex) = exemplar {
encode_message_field_in_place(buf, 5, |buf| {
encode_exemplar(buf, ex);
});
}
for (k, v) in attrs {
encode_message_field_in_place(buf, 7, |buf| {
encode_attr_key_value(buf, k, v);
});
}
}
fn encode_sum(buf: &mut Vec<u8>, data_points: &[CounterDataPoint], start_time: u64, time: u64) {
for (attrs, value, exemplar) in data_points {
encode_message_field_in_place(buf, 1, |buf| {
encode_counter_data_point(buf, attrs, start_time, time, *value, exemplar);
});
}
encode_varint_field(buf, 2, 2);
encode_varint_field(buf, 3, 1);
}
fn encode_gauge_msg(buf: &mut Vec<u8>, data_points: &[GaugeDataPoint], start_time: u64, time: u64) {
for (attrs, value, exemplar) in data_points {
encode_message_field_in_place(buf, 1, |buf| {
encode_gauge_data_point(buf, attrs, start_time, time, *value, exemplar);
});
}
}
fn encode_histogram_data_point(
buf: &mut Vec<u8>,
dp: &crate::metrics::HistogramDataPoint,
boundaries: &[f64],
start_time_unix_nano: u64,
time_unix_nano: u64,
) {
encode_fixed64_field(buf, 2, start_time_unix_nano);
encode_fixed64_field(buf, 3, time_unix_nano);
encode_fixed64_field_always(buf, 4, dp.count);
encode_fixed64_field_always(buf, 5, dp.sum.to_bits());
encode_packed_fixed64_field(buf, 6, &dp.bucket_counts);
encode_packed_double_field(buf, 7, boundaries);
for (k, v) in &dp.attrs {
encode_message_field_in_place(buf, 9, |buf| {
encode_attr_key_value(buf, k, v);
});
}
if let Some(ref ex) = dp.exemplar {
encode_message_field_in_place(buf, 8, |buf| {
encode_exemplar(buf, ex);
});
}
encode_fixed64_field_always(buf, 11, dp.min.to_bits());
encode_fixed64_field_always(buf, 12, dp.max.to_bits());
}
fn encode_histogram_msg(
buf: &mut Vec<u8>,
data_points: &[crate::metrics::HistogramDataPoint],
boundaries: &[f64],
start_time: u64,
time: u64,
) {
for dp in data_points {
encode_message_field_in_place(buf, 1, |buf| {
encode_histogram_data_point(buf, dp, boundaries, start_time, time);
});
}
encode_varint_field(buf, 2, 2);
}
fn encode_metric(buf: &mut Vec<u8>, snapshot: &MetricSnapshot, start_time: u64, time: u64) {
match snapshot {
MetricSnapshot::Counter {
name,
description,
data_points,
} => {
encode_string_field(buf, 1, name);
encode_string_field(buf, 2, description);
encode_message_field_in_place(buf, 7, |buf| {
encode_sum(buf, data_points, start_time, time);
});
}
MetricSnapshot::Gauge {
name,
description,
data_points,
} => {
encode_string_field(buf, 1, name);
encode_string_field(buf, 2, description);
encode_message_field_in_place(buf, 5, |buf| {
encode_gauge_msg(buf, data_points, start_time, time);
});
}
MetricSnapshot::Histogram {
name,
description,
boundaries,
data_points,
} => {
encode_string_field(buf, 1, name);
encode_string_field(buf, 2, description);
encode_message_field_in_place(buf, 9, |buf| {
encode_histogram_msg(buf, data_points, boundaries, start_time, time);
});
}
}
}
pub fn encode_export_metrics_request(
resource_attrs: &[KeyValue],
scope_name: &str,
scope_version: &str,
snapshots: &[MetricSnapshot],
start_time_unix_nano: u64,
time_unix_nano: u64,
) -> Vec<u8> {
let mut request_buf = Vec::new();
encode_message_field_in_place(&mut request_buf, 1, |buf| {
encode_message_field_in_place(buf, 1, |buf| {
encode_resource(buf, resource_attrs);
});
encode_message_field_in_place(buf, 2, |buf| {
encode_message_field_in_place(buf, 1, |buf| {
encode_scope(buf, scope_name, scope_version);
});
for snapshot in snapshots {
encode_message_field_in_place(buf, 2, |buf| {
encode_metric(buf, snapshot, start_time_unix_nano, time_unix_nano);
});
}
});
});
request_buf
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_counter_metric_is_nonempty() {
let snapshots = vec![MetricSnapshot::Counter {
name: "http_requests_total".to_string(),
description: "Total HTTP requests".to_string(),
data_points: vec![(
vec![
("method".to_string(), "GET".to_string()),
("status".to_string(), "200".to_string()),
],
42,
None,
)],
}];
let bytes = encode_export_metrics_request(
&[KeyValue {
key: "service.name".to_string(),
value: crate::otlp_trace::AnyValue::String("test-svc".to_string()),
}],
"ro11y",
"0.3.0",
&snapshots,
1_000_000_000,
2_000_000_000,
);
assert!(!bytes.is_empty());
assert_eq!(bytes[0], 0x0A);
let name = b"http_requests_total";
assert!(
bytes.windows(name.len()).any(|w| w == name),
"metric name not found in encoded bytes"
);
}
#[test]
fn encode_gauge_metric_is_nonempty() {
let snapshots = vec![MetricSnapshot::Gauge {
name: "cpu_usage".to_string(),
description: "CPU usage percentage".to_string(),
data_points: vec![(vec![("core".to_string(), "0".to_string())], 75.5, None)],
}];
let bytes = encode_export_metrics_request(
&[KeyValue {
key: "service.name".to_string(),
value: crate::otlp_trace::AnyValue::String("test-svc".to_string()),
}],
"ro11y",
"0.3.0",
&snapshots,
1_000_000_000,
2_000_000_000,
);
assert!(!bytes.is_empty());
let val_bytes = 75.5_f64.to_bits().to_le_bytes();
assert!(
bytes.windows(8).any(|w| w == val_bytes),
"gauge value not found in encoded bytes"
);
}
#[test]
fn encode_counter_value_is_correct() {
let snapshots = vec![MetricSnapshot::Counter {
name: "c".to_string(),
description: String::new(),
data_points: vec![(vec![], 99, None)],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
let val_bytes = (99_i64 as u64).to_le_bytes();
assert!(
bytes.windows(8).any(|w| w == val_bytes),
"counter value 99 not found in encoded bytes"
);
}
#[test]
fn encode_multiple_data_points() {
let snapshots = vec![MetricSnapshot::Counter {
name: "multi".to_string(),
description: String::new(),
data_points: vec![
(vec![("k".to_string(), "a".to_string())], 10, None),
(vec![("k".to_string(), "b".to_string())], 20, None),
],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
let val10 = (10_i64 as u64).to_le_bytes();
let val20 = (20_i64 as u64).to_le_bytes();
assert!(bytes.windows(8).any(|w| w == val10));
assert!(bytes.windows(8).any(|w| w == val20));
}
#[test]
fn encode_counter_has_cumulative_temporality() {
let snapshots = vec![MetricSnapshot::Counter {
name: "c".to_string(),
description: String::new(),
data_points: vec![(vec![], 1, None)],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(
bytes.windows(2).any(|w| w == [0x10, 0x02]),
"CUMULATIVE temporality not found"
);
}
#[test]
fn encode_counter_is_monotonic() {
let snapshots = vec![MetricSnapshot::Counter {
name: "c".to_string(),
description: String::new(),
data_points: vec![(vec![], 1, None)],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(
bytes.windows(2).any(|w| w == [0x18, 0x01]),
"is_monotonic=true not found"
);
}
#[test]
fn encode_mixed_counter_and_gauge() {
let snapshots = vec![
MetricSnapshot::Counter {
name: "requests".to_string(),
description: String::new(),
data_points: vec![(vec![], 100, None)],
},
MetricSnapshot::Gauge {
name: "temperature".to_string(),
description: String::new(),
data_points: vec![(vec![], 36.6, None)],
},
];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(bytes.windows(8).any(|w| w == b"requests"));
assert!(bytes.windows(11).any(|w| w == b"temperature"));
}
#[test]
fn encode_histogram_metric_is_nonempty() {
let snapshots = vec![MetricSnapshot::Histogram {
name: "request_duration".to_string(),
description: "Request duration histogram".to_string(),
boundaries: vec![10.0, 50.0, 100.0],
data_points: vec![crate::metrics::HistogramDataPoint {
attrs: vec![("method".to_string(), "GET".to_string())],
bucket_counts: vec![5, 10, 3, 2],
sum: 1234.5,
count: 20,
min: 1.0,
max: 250.0,
exemplar: None,
}],
}];
let bytes = encode_export_metrics_request(
&[KeyValue {
key: "service.name".to_string(),
value: crate::otlp_trace::AnyValue::String("test-svc".to_string()),
}],
"ro11y",
"0.3.0",
&snapshots,
1_000_000_000,
2_000_000_000,
);
assert!(!bytes.is_empty());
let name = b"request_duration";
assert!(
bytes.windows(name.len()).any(|w| w == name),
"metric name not found in encoded bytes"
);
}
#[test]
fn encode_histogram_has_cumulative_temporality() {
let snapshots = vec![MetricSnapshot::Histogram {
name: "h".to_string(),
description: String::new(),
boundaries: vec![10.0],
data_points: vec![crate::metrics::HistogramDataPoint {
attrs: vec![],
bucket_counts: vec![1, 0],
sum: 5.0,
count: 1,
min: 5.0,
max: 5.0,
exemplar: None,
}],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(
bytes.windows(2).any(|w| w == [0x10, 0x02]),
"CUMULATIVE temporality not found"
);
}
#[test]
fn encode_histogram_bucket_counts_present() {
let snapshots = vec![MetricSnapshot::Histogram {
name: "h".to_string(),
description: String::new(),
boundaries: vec![10.0],
data_points: vec![crate::metrics::HistogramDataPoint {
attrs: vec![],
bucket_counts: vec![3, 7],
sum: 100.0,
count: 10,
min: 1.0,
max: 50.0,
exemplar: None,
}],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
let val3 = 3u64.to_le_bytes();
let val7 = 7u64.to_le_bytes();
assert!(bytes.windows(8).any(|w| w == val3));
assert!(bytes.windows(8).any(|w| w == val7));
}
#[test]
fn encode_histogram_attributes_present() {
let snapshots = vec![MetricSnapshot::Histogram {
name: "h".to_string(),
description: String::new(),
boundaries: vec![10.0],
data_points: vec![crate::metrics::HistogramDataPoint {
attrs: vec![("method".to_string(), "GET".to_string())],
bucket_counts: vec![1, 0],
sum: 5.0,
count: 1,
min: 5.0,
max: 5.0,
exemplar: None,
}],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(bytes.windows(6).any(|w| w == b"method"));
assert!(bytes.windows(3).any(|w| w == b"GET"));
}
#[test]
fn encode_mixed_counter_gauge_histogram() {
let snapshots = vec![
MetricSnapshot::Counter {
name: "requests".to_string(),
description: String::new(),
data_points: vec![(vec![], 100, None)],
},
MetricSnapshot::Gauge {
name: "temperature".to_string(),
description: String::new(),
data_points: vec![(vec![], 36.6, None)],
},
MetricSnapshot::Histogram {
name: "latency".to_string(),
description: String::new(),
boundaries: vec![10.0],
data_points: vec![crate::metrics::HistogramDataPoint {
attrs: vec![],
bucket_counts: vec![1, 1],
sum: 15.0,
count: 2,
min: 5.0,
max: 10.0,
exemplar: None,
}],
},
];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(bytes.windows(8).any(|w| w == b"requests"));
assert!(bytes.windows(11).any(|w| w == b"temperature"));
assert!(bytes.windows(7).any(|w| w == b"latency"));
}
#[test]
fn encode_attributes_in_data_point() {
let snapshots = vec![MetricSnapshot::Counter {
name: "c".to_string(),
description: String::new(),
data_points: vec![(vec![("method".to_string(), "GET".to_string())], 1, None)],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(bytes.windows(6).any(|w| w == b"method"));
assert!(bytes.windows(3).any(|w| w == b"GET"));
}
#[test]
fn encode_counter_with_exemplar() {
let exemplar = Some(crate::metrics::Exemplar {
trace_id: [0x01; 16],
span_id: [0x02; 8],
time_unix_nano: 5_000_000_000,
value: crate::metrics::ExemplarValue::Int(42),
});
let snapshots = vec![MetricSnapshot::Counter {
name: "c".to_string(),
description: String::new(),
data_points: vec![(vec![], 42, exemplar)],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(bytes.windows(16).any(|w| w == [0x01; 16]));
assert!(bytes.windows(8).any(|w| w == [0x02; 8]));
}
#[test]
fn encode_histogram_with_exemplar() {
let exemplar = Some(crate::metrics::Exemplar {
trace_id: [0xAA; 16],
span_id: [0xBB; 8],
time_unix_nano: 9_000_000_000,
value: crate::metrics::ExemplarValue::Double(42.5),
});
let snapshots = vec![MetricSnapshot::Histogram {
name: "h".to_string(),
description: String::new(),
boundaries: vec![10.0],
data_points: vec![crate::metrics::HistogramDataPoint {
attrs: vec![],
bucket_counts: vec![1, 0],
sum: 42.5,
count: 1,
min: 42.5,
max: 42.5,
exemplar,
}],
}];
let bytes = encode_export_metrics_request(&[], "ro11y", "0.3.0", &snapshots, 0, 0);
assert!(bytes.windows(16).any(|w| w == [0xAA; 16]));
assert!(bytes.windows(8).any(|w| w == [0xBB; 8]));
}
}