use crate::metrics::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_counter_data_point(
buf: &mut Vec<u8>,
attrs: &[(String, String)],
start_time_unix_nano: u64,
time_unix_nano: u64,
value: i64,
) {
encode_fixed64_field(buf, 2, start_time_unix_nano);
encode_fixed64_field(buf, 3, time_unix_nano);
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,
) {
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());
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: &[(Vec<(String, String)>, i64)],
start_time: u64,
time: u64,
) {
for (attrs, value) in data_points {
encode_message_field_in_place(buf, 1, |buf| {
encode_counter_data_point(buf, attrs, start_time, time, *value);
});
}
encode_varint_field(buf, 2, 2);
encode_varint_field(buf, 3, 1);
}
fn encode_gauge_msg(
buf: &mut Vec<u8>,
data_points: &[(Vec<(String, String)>, f64)],
start_time: u64,
time: u64,
) {
for (attrs, value) in data_points {
encode_message_field_in_place(buf, 1, |buf| {
encode_gauge_data_point(buf, attrs, start_time, time, *value);
});
}
}
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);
});
}
}
}
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,
)],
}];
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)],
}];
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)],
}];
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),
(vec![("k".to_string(), "b".to_string())], 20),
],
}];
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)],
}];
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)],
}];
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)],
},
MetricSnapshot::Gauge {
name: "temperature".to_string(),
description: String::new(),
data_points: vec![(vec![], 36.6)],
},
];
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_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,
)],
}];
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"));
}
}