use std::time::UNIX_EPOCH;
use prost::Message;
use crate::model::log::{LogEvent, Severity};
use crate::model::metric::MetricEvent;
use crate::{EncoderError, SondaError};
use super::Encoder;
#[derive(Clone, PartialEq, prost::Message)]
pub struct KeyValue {
#[prost(string, tag = "1")]
pub key: String,
#[prost(message, optional, tag = "2")]
pub value: Option<AnyValue>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct AnyValue {
#[prost(oneof = "any_value::Value", tags = "1, 2, 3, 4")]
pub value: Option<any_value::Value>,
}
pub mod any_value {
#[derive(Clone, PartialEq, prost::Oneof)]
pub enum Value {
#[prost(string, tag = "1")]
StringValue(String),
#[prost(bool, tag = "2")]
BoolValue(bool),
#[prost(int64, tag = "3")]
IntValue(i64),
#[prost(double, tag = "4")]
DoubleValue(f64),
}
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct Resource {
#[prost(message, repeated, tag = "1")]
pub attributes: Vec<KeyValue>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct InstrumentationScope {
#[prost(string, tag = "1")]
pub name: String,
#[prost(string, tag = "2")]
pub version: String,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct ExportMetricsServiceRequest {
#[prost(message, repeated, tag = "1")]
pub resource_metrics: Vec<ResourceMetrics>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct ExportMetricsServiceResponse {}
#[derive(Clone, PartialEq, prost::Message)]
pub struct ResourceMetrics {
#[prost(message, optional, tag = "1")]
pub resource: Option<Resource>,
#[prost(message, repeated, tag = "2")]
pub scope_metrics: Vec<ScopeMetrics>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct ScopeMetrics {
#[prost(message, optional, tag = "1")]
pub scope: Option<InstrumentationScope>,
#[prost(message, repeated, tag = "2")]
pub metrics: Vec<Metric>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct Metric {
#[prost(string, tag = "1")]
pub name: String,
#[prost(string, tag = "2")]
pub description: String,
#[prost(string, tag = "3")]
pub unit: String,
#[prost(oneof = "metric::Data", tags = "5")]
pub data: Option<metric::Data>,
}
pub mod metric {
#[derive(Clone, PartialEq, prost::Oneof)]
pub enum Data {
#[prost(message, tag = "5")]
Gauge(super::Gauge),
}
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct Gauge {
#[prost(message, repeated, tag = "1")]
pub data_points: Vec<NumberDataPoint>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct NumberDataPoint {
#[prost(message, repeated, tag = "7")]
pub attributes: Vec<KeyValue>,
#[prost(fixed64, tag = "3")]
pub time_unix_nano: u64,
#[prost(oneof = "number_data_point::Value", tags = "4, 6")]
pub value: Option<number_data_point::Value>,
}
pub mod number_data_point {
#[derive(Clone, PartialEq, prost::Oneof)]
pub enum Value {
#[prost(double, tag = "4")]
AsDouble(f64),
#[prost(sfixed64, tag = "6")]
AsInt(i64),
}
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct ExportLogsServiceRequest {
#[prost(message, repeated, tag = "1")]
pub resource_logs: Vec<ResourceLogs>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct ExportLogsServiceResponse {}
#[derive(Clone, PartialEq, prost::Message)]
pub struct ResourceLogs {
#[prost(message, optional, tag = "1")]
pub resource: Option<Resource>,
#[prost(message, repeated, tag = "2")]
pub scope_logs: Vec<ScopeLogs>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct ScopeLogs {
#[prost(message, optional, tag = "1")]
pub scope: Option<InstrumentationScope>,
#[prost(message, repeated, tag = "2")]
pub log_records: Vec<LogRecord>,
}
#[derive(Clone, PartialEq, prost::Message)]
pub struct LogRecord {
#[prost(fixed64, tag = "1")]
pub time_unix_nano: u64,
#[prost(int32, tag = "2")]
pub severity_number: i32,
#[prost(string, tag = "3")]
pub severity_text: String,
#[prost(message, optional, tag = "5")]
pub body: Option<AnyValue>,
#[prost(message, repeated, tag = "6")]
pub attributes: Vec<KeyValue>,
}
pub fn severity_to_number(severity: Severity) -> i32 {
match severity {
Severity::Trace => 1, Severity::Debug => 5, Severity::Info => 9, Severity::Warn => 13, Severity::Error => 17, Severity::Fatal => 21, }
}
fn severity_to_text(severity: Severity) -> &'static str {
match severity {
Severity::Trace => "TRACE",
Severity::Debug => "DEBUG",
Severity::Info => "INFO",
Severity::Warn => "WARN",
Severity::Error => "ERROR",
Severity::Fatal => "FATAL",
}
}
fn timestamp_nanos(ts: std::time::SystemTime) -> Result<u64, SondaError> {
let duration = ts
.duration_since(UNIX_EPOCH)
.map_err(|e| SondaError::Encoder(EncoderError::TimestampBeforeEpoch(e)))?;
Ok(duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos()))
}
fn string_kv(key: &str, value: &str) -> KeyValue {
KeyValue {
key: key.to_string(),
value: Some(AnyValue {
value: Some(any_value::Value::StringValue(value.to_string())),
}),
}
}
pub struct OtlpEncoder;
impl OtlpEncoder {
pub fn new() -> Self {
Self
}
}
impl Default for OtlpEncoder {
fn default() -> Self {
Self::new()
}
}
impl Encoder for OtlpEncoder {
fn encode_metric(&self, event: &MetricEvent, buf: &mut Vec<u8>) -> Result<(), SondaError> {
let time_unix_nano = timestamp_nanos(event.timestamp)?;
let attributes: Vec<KeyValue> = event.labels.iter().map(|(k, v)| string_kv(k, v)).collect();
let data_point = NumberDataPoint {
attributes,
time_unix_nano,
value: Some(number_data_point::Value::AsDouble(event.value)),
};
let metric = Metric {
name: event.name.to_string(),
description: String::new(),
unit: String::new(),
data: Some(metric::Data::Gauge(Gauge {
data_points: vec![data_point],
})),
};
let encoded_len = metric.encoded_len();
let mut proto_bytes = Vec::with_capacity(encoded_len);
metric.encode(&mut proto_bytes).map_err(|e| {
SondaError::Encoder(EncoderError::Other(format!("protobuf encode error: {e}")))
})?;
let len = proto_bytes.len() as u32;
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(&proto_bytes);
Ok(())
}
fn encode_log(&self, event: &LogEvent, buf: &mut Vec<u8>) -> Result<(), SondaError> {
let time_unix_nano = timestamp_nanos(event.timestamp)?;
let mut attributes: Vec<KeyValue> =
Vec::with_capacity(event.labels.len() + event.fields.len());
for (k, v) in event.labels.iter() {
attributes.push(string_kv(k, v));
}
for (k, v) in &event.fields {
attributes.push(string_kv(k, v));
}
let log_record = LogRecord {
time_unix_nano,
severity_number: severity_to_number(event.severity),
severity_text: severity_to_text(event.severity).to_string(),
body: Some(AnyValue {
value: Some(any_value::Value::StringValue(event.message.clone())),
}),
attributes,
};
let encoded_len = log_record.encoded_len();
let mut proto_bytes = Vec::with_capacity(encoded_len);
log_record.encode(&mut proto_bytes).map_err(|e| {
SondaError::Encoder(EncoderError::Other(format!("protobuf encode error: {e}")))
})?;
let len = proto_bytes.len() as u32;
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(&proto_bytes);
Ok(())
}
}
pub fn parse_length_prefixed_metrics(data: &[u8]) -> Result<Vec<Metric>, SondaError> {
parse_length_prefixed(data, "Metric")
}
pub fn parse_length_prefixed_log_records(data: &[u8]) -> Result<Vec<LogRecord>, SondaError> {
parse_length_prefixed(data, "LogRecord")
}
fn parse_length_prefixed<T: Message + Default>(
data: &[u8],
type_name: &str,
) -> Result<Vec<T>, SondaError> {
let mut result = Vec::new();
let mut offset = 0;
while offset < data.len() {
if offset + 4 > data.len() {
return Err(SondaError::Encoder(EncoderError::Other(format!(
"truncated length prefix in {type_name} buffer"
))));
}
let len = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
offset += 4;
if offset + len > data.len() {
return Err(SondaError::Encoder(EncoderError::Other(format!(
"truncated {type_name} protobuf: expected {len} bytes, got {}",
data.len() - offset
))));
}
let msg = T::decode(&data[offset..offset + len]).map_err(|e| {
SondaError::Encoder(EncoderError::Other(format!("protobuf decode error: {e}")))
})?;
result.push(msg);
offset += len;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::log::LogEvent;
use crate::model::metric::{Labels, MetricEvent};
use std::collections::BTreeMap;
use std::time::{Duration, UNIX_EPOCH};
fn make_metric(
name: &str,
value: f64,
label_pairs: &[(&str, &str)],
timestamp_ms: u64,
) -> MetricEvent {
let labels = Labels::from_pairs(label_pairs).expect("valid labels");
let ts = UNIX_EPOCH + Duration::from_millis(timestamp_ms);
MetricEvent::with_timestamp(name.to_string(), value, labels, ts).expect("valid metric name")
}
fn make_log(
severity: Severity,
message: &str,
label_pairs: &[(&str, &str)],
fields: &[(&str, &str)],
timestamp_ms: u64,
) -> LogEvent {
let labels = Labels::from_pairs(label_pairs).expect("valid labels");
let mut field_map = BTreeMap::new();
for (k, v) in fields {
field_map.insert(k.to_string(), v.to_string());
}
let ts = UNIX_EPOCH + Duration::from_millis(timestamp_ms);
LogEvent::with_timestamp(ts, severity, message.to_string(), labels, field_map)
}
fn decode_first_metric(buf: &[u8]) -> Metric {
assert!(buf.len() >= 4, "buffer must contain at least length prefix");
let len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
let proto_bytes = &buf[4..4 + len];
Metric::decode(proto_bytes).expect("protobuf decode")
}
fn decode_first_log_record(buf: &[u8]) -> LogRecord {
assert!(buf.len() >= 4, "buffer must contain at least length prefix");
let len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
let proto_bytes = &buf[4..4 + len];
LogRecord::decode(proto_bytes).expect("protobuf decode")
}
#[test]
fn encode_metric_produces_nonempty_bytes() {
let encoder = OtlpEncoder::new();
let event = make_metric("cpu_usage", 42.5, &[("host", "server1")], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
assert!(
buf.len() > 4,
"encoded output must contain length prefix + protobuf"
);
}
#[test]
fn length_prefix_matches_protobuf_length() {
let encoder = OtlpEncoder::new();
let event = make_metric("test_metric", 99.9, &[("env", "prod")], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
assert_eq!(
buf.len(),
4 + len,
"total buffer length must equal 4 (prefix) + declared protobuf length"
);
}
#[test]
fn metric_name_is_set_correctly() {
let encoder = OtlpEncoder::new();
let event = make_metric("http_requests_total", 100.0, &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let metric = decode_first_metric(&buf);
assert_eq!(metric.name, "http_requests_total");
}
#[test]
fn metric_value_is_stored_as_double() {
let encoder = OtlpEncoder::new();
let event = make_metric("gauge", 3.14, &[], 1_700_000_000_500);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let metric = decode_first_metric(&buf);
let gauge = match metric.data {
Some(metric::Data::Gauge(g)) => g,
other => panic!("expected Gauge data, got {other:?}"),
};
assert_eq!(gauge.data_points.len(), 1);
let dp = &gauge.data_points[0];
match &dp.value {
Some(number_data_point::Value::AsDouble(v)) => {
assert!(
(v - 3.14).abs() < f64::EPSILON,
"value must be 3.14, got {v}"
);
}
other => panic!("expected AsDouble, got {other:?}"),
}
}
#[test]
fn metric_timestamp_is_nanoseconds() {
let encoder = OtlpEncoder::new();
let event = make_metric("ts_test", 1.0, &[], 1_700_000_000_500);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let metric = decode_first_metric(&buf);
let gauge = match metric.data {
Some(metric::Data::Gauge(g)) => g,
other => panic!("expected Gauge, got {other:?}"),
};
let dp = &gauge.data_points[0];
assert_eq!(dp.time_unix_nano, 1_700_000_000_500_000_000);
}
#[test]
fn metric_labels_become_data_point_attributes() {
let encoder = OtlpEncoder::new();
let event = make_metric(
"my_metric",
1.0,
&[("zone", "eu1"), ("env", "prod"), ("host", "server1")],
1_700_000_000_000,
);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let metric = decode_first_metric(&buf);
let gauge = match metric.data {
Some(metric::Data::Gauge(g)) => g,
other => panic!("expected Gauge, got {other:?}"),
};
let dp = &gauge.data_points[0];
assert_eq!(dp.attributes.len(), 3, "must have 3 attributes");
let attr_map: std::collections::HashMap<&str, &str> = dp
.attributes
.iter()
.map(|kv| {
let val = match &kv.value {
Some(AnyValue {
value: Some(any_value::Value::StringValue(s)),
}) => s.as_str(),
_ => "",
};
(kv.key.as_str(), val)
})
.collect();
assert_eq!(attr_map.get("env"), Some(&"prod"));
assert_eq!(attr_map.get("host"), Some(&"server1"));
assert_eq!(attr_map.get("zone"), Some(&"eu1"));
}
#[test]
fn metric_empty_labels_produces_no_attributes() {
let encoder = OtlpEncoder::new();
let event = make_metric("bare_metric", 0.0, &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let metric = decode_first_metric(&buf);
let gauge = match metric.data {
Some(metric::Data::Gauge(g)) => g,
other => panic!("expected Gauge, got {other:?}"),
};
let dp = &gauge.data_points[0];
assert!(dp.attributes.is_empty(), "no labels means no attributes");
}
#[test]
fn metric_zero_value_is_preserved() {
let encoder = OtlpEncoder::new();
let event = make_metric("zero", 0.0, &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let metric = decode_first_metric(&buf);
let gauge = match metric.data {
Some(metric::Data::Gauge(g)) => g,
other => panic!("expected Gauge, got {other:?}"),
};
let dp = &gauge.data_points[0];
match &dp.value {
Some(number_data_point::Value::AsDouble(v)) => {
assert!(*v == 0.0, "zero value must be preserved, got {v}");
}
other => panic!("expected AsDouble, got {other:?}"),
}
}
#[test]
fn metric_large_float_is_preserved() {
let encoder = OtlpEncoder::new();
let event = make_metric("big", f64::MAX, &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let metric = decode_first_metric(&buf);
let gauge = match metric.data {
Some(metric::Data::Gauge(g)) => g,
other => panic!("expected Gauge, got {other:?}"),
};
let dp = &gauge.data_points[0];
match &dp.value {
Some(number_data_point::Value::AsDouble(v)) => {
assert_eq!(*v, f64::MAX, "f64::MAX must be preserved");
}
other => panic!("expected AsDouble, got {other:?}"),
}
}
#[test]
fn metric_timestamp_at_epoch_zero() {
let encoder = OtlpEncoder::new();
let event = make_metric("epoch_test", 1.0, &[], 0);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
let metric = decode_first_metric(&buf);
let gauge = match metric.data {
Some(metric::Data::Gauge(g)) => g,
other => panic!("expected Gauge, got {other:?}"),
};
let dp = &gauge.data_points[0];
assert_eq!(dp.time_unix_nano, 0, "timestamp at epoch should be 0 ns");
}
#[test]
fn multiple_metric_encodes_append_to_buffer() {
let encoder = OtlpEncoder::new();
let e1 = make_metric("metric_a", 1.0, &[], 1_700_000_000_000);
let e2 = make_metric("metric_b", 2.0, &[], 1_700_000_001_000);
let mut buf = Vec::new();
encoder.encode_metric(&e1, &mut buf).expect("encode 1");
let len_after_first = buf.len();
encoder.encode_metric(&e2, &mut buf).expect("encode 2");
assert!(buf.len() > len_after_first, "second encode should append");
let metrics = parse_length_prefixed_metrics(&buf).expect("parse ok");
assert_eq!(metrics.len(), 2);
assert_eq!(metrics[0].name, "metric_a");
assert_eq!(metrics[1].name, "metric_b");
}
#[test]
fn encode_log_produces_nonempty_bytes() {
let encoder = OtlpEncoder::new();
let event = make_log(Severity::Info, "hello", &[], &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_log(&event, &mut buf).expect("encode ok");
assert!(buf.len() > 4, "encoded log must contain prefix + protobuf");
}
#[test]
fn log_message_body_is_set() {
let encoder = OtlpEncoder::new();
let event = make_log(
Severity::Info,
"request processed",
&[],
&[],
1_700_000_000_000,
);
let mut buf = Vec::new();
encoder.encode_log(&event, &mut buf).expect("encode ok");
let rec = decode_first_log_record(&buf);
match rec.body {
Some(AnyValue {
value: Some(any_value::Value::StringValue(ref s)),
}) => assert_eq!(s, "request processed"),
other => panic!("expected string body, got {other:?}"),
}
}
#[test]
fn log_severity_number_is_correct() {
let encoder = OtlpEncoder::new();
let cases = [
(Severity::Trace, 1),
(Severity::Debug, 5),
(Severity::Info, 9),
(Severity::Warn, 13),
(Severity::Error, 17),
(Severity::Fatal, 21),
];
for (severity, expected_num) in cases {
let event = make_log(severity, "test", &[], &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_log(&event, &mut buf).expect("encode ok");
let rec = decode_first_log_record(&buf);
assert_eq!(
rec.severity_number, expected_num,
"severity {:?} should map to {}",
severity, expected_num
);
}
}
#[test]
fn log_severity_text_is_correct() {
let encoder = OtlpEncoder::new();
let event = make_log(Severity::Warn, "watch out", &[], &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_log(&event, &mut buf).expect("encode ok");
let rec = decode_first_log_record(&buf);
assert_eq!(rec.severity_text, "WARN");
}
#[test]
fn log_timestamp_is_nanoseconds() {
let encoder = OtlpEncoder::new();
let event = make_log(Severity::Info, "ts", &[], &[], 1_700_000_000_500);
let mut buf = Vec::new();
encoder.encode_log(&event, &mut buf).expect("encode ok");
let rec = decode_first_log_record(&buf);
assert_eq!(rec.time_unix_nano, 1_700_000_000_500_000_000);
}
#[test]
fn log_labels_and_fields_become_attributes() {
let encoder = OtlpEncoder::new();
let event = make_log(
Severity::Info,
"msg",
&[("host", "server1")],
&[("latency", "50ms")],
1_700_000_000_000,
);
let mut buf = Vec::new();
encoder.encode_log(&event, &mut buf).expect("encode ok");
let rec = decode_first_log_record(&buf);
assert_eq!(rec.attributes.len(), 2, "1 label + 1 field = 2 attributes");
let attr_map: std::collections::HashMap<&str, &str> = rec
.attributes
.iter()
.map(|kv| {
let val = match &kv.value {
Some(AnyValue {
value: Some(any_value::Value::StringValue(s)),
}) => s.as_str(),
_ => "",
};
(kv.key.as_str(), val)
})
.collect();
assert_eq!(attr_map.get("host"), Some(&"server1"));
assert_eq!(attr_map.get("latency"), Some(&"50ms"));
}
#[test]
fn log_empty_labels_and_fields_produces_no_attributes() {
let encoder = OtlpEncoder::new();
let event = make_log(Severity::Info, "bare", &[], &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_log(&event, &mut buf).expect("encode ok");
let rec = decode_first_log_record(&buf);
assert!(rec.attributes.is_empty());
}
#[test]
fn multiple_log_encodes_append_to_buffer() {
let encoder = OtlpEncoder::new();
let e1 = make_log(Severity::Info, "first", &[], &[], 1_700_000_000_000);
let e2 = make_log(Severity::Error, "second", &[], &[], 1_700_000_001_000);
let mut buf = Vec::new();
encoder.encode_log(&e1, &mut buf).expect("encode 1");
let len_after_first = buf.len();
encoder.encode_log(&e2, &mut buf).expect("encode 2");
assert!(buf.len() > len_after_first, "second encode should append");
let records = parse_length_prefixed_log_records(&buf).expect("parse ok");
assert_eq!(records.len(), 2);
assert_eq!(records[0].severity_text, "INFO");
assert_eq!(records[1].severity_text, "ERROR");
}
#[test]
fn parse_metrics_empty_buffer_returns_empty_vec() {
let result = parse_length_prefixed_metrics(&[]).expect("empty is ok");
assert!(result.is_empty());
}
#[test]
fn parse_log_records_empty_buffer_returns_empty_vec() {
let result = parse_length_prefixed_log_records(&[]).expect("empty is ok");
assert!(result.is_empty());
}
#[test]
fn parse_metrics_truncated_prefix_returns_error() {
let result = parse_length_prefixed_metrics(&[0x01, 0x02]);
assert!(result.is_err(), "truncated prefix should be an error");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("truncated"),
"error should mention truncation: {msg}"
);
}
#[test]
fn parse_metrics_truncated_body_returns_error() {
let mut data = vec![100, 0, 0, 0];
data.extend_from_slice(&[0x0A, 0x0B]);
let result = parse_length_prefixed_metrics(&data);
assert!(result.is_err(), "truncated body should be an error");
}
#[test]
fn export_metrics_service_request_roundtrips() {
let req = ExportMetricsServiceRequest {
resource_metrics: vec![ResourceMetrics {
resource: Some(Resource {
attributes: vec![string_kv("service.name", "sonda")],
}),
scope_metrics: vec![ScopeMetrics {
scope: Some(InstrumentationScope {
name: "sonda".to_string(),
version: "0.4.0".to_string(),
}),
metrics: vec![Metric {
name: "test_gauge".to_string(),
description: String::new(),
unit: String::new(),
data: Some(metric::Data::Gauge(Gauge {
data_points: vec![NumberDataPoint {
attributes: vec![],
time_unix_nano: 1_700_000_000_000_000_000,
value: Some(number_data_point::Value::AsDouble(42.0)),
}],
})),
}],
}],
}],
};
let mut encoded = Vec::new();
req.encode(&mut encoded).expect("encode");
let decoded = ExportMetricsServiceRequest::decode(encoded.as_slice()).expect("decode");
assert_eq!(req, decoded);
}
#[test]
fn export_logs_service_request_roundtrips() {
let req = ExportLogsServiceRequest {
resource_logs: vec![ResourceLogs {
resource: Some(Resource {
attributes: vec![string_kv("service.name", "sonda")],
}),
scope_logs: vec![ScopeLogs {
scope: Some(InstrumentationScope {
name: "sonda".to_string(),
version: "0.4.0".to_string(),
}),
log_records: vec![LogRecord {
time_unix_nano: 1_700_000_000_000_000_000,
severity_number: 9,
severity_text: "INFO".to_string(),
body: Some(AnyValue {
value: Some(any_value::Value::StringValue("test".to_string())),
}),
attributes: vec![],
}],
}],
}],
};
let mut encoded = Vec::new();
req.encode(&mut encoded).expect("encode");
let decoded = ExportLogsServiceRequest::decode(encoded.as_slice()).expect("decode");
assert_eq!(req, decoded);
}
#[test]
fn severity_to_number_maps_all_variants() {
assert_eq!(severity_to_number(Severity::Trace), 1);
assert_eq!(severity_to_number(Severity::Debug), 5);
assert_eq!(severity_to_number(Severity::Info), 9);
assert_eq!(severity_to_number(Severity::Warn), 13);
assert_eq!(severity_to_number(Severity::Error), 17);
assert_eq!(severity_to_number(Severity::Fatal), 21);
}
#[test]
fn otlp_encoder_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<OtlpEncoder>();
}
#[test]
fn default_creates_valid_encoder() {
let encoder = OtlpEncoder::default();
let event = make_metric("test", 1.0, &[], 1_700_000_000_000);
let mut buf = Vec::new();
encoder.encode_metric(&event, &mut buf).expect("encode ok");
assert!(!buf.is_empty());
}
}