Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e796927
feat(codecs): support encoding native Vector metrics in OTLP serializer
petere-datadog Jul 1, 2026
1e9b90e
update changelog
petere-datadog Jul 1, 2026
b54d48c
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 7, 2026
d3ede6e
handle encoding tagvalueset as OTLP arrayvalue
petere-datadog Jul 7, 2026
becd15c
feat(codecs): encode namespace and reject pre-epoch timestamps in OTL…
petere-datadog Jul 7, 2026
d2c4684
fix fmt
petere-datadog Jul 7, 2026
fa8df16
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 7, 2026
81da2e0
fixed the conversion of icnremental gauges, also did some refactoring…
petere-datadog Jul 8, 2026
d548dec
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 8, 2026
5ccc161
fix test
petere-datadog Jul 8, 2026
f7ee8b3
fix more validation issues
petere-datadog Jul 8, 2026
2af64d1
add duplicatees validation as well as update is_monotonic check
petere-datadog Jul 8, 2026
4fd4726
update test
petere-datadog Jul 8, 2026
01431d0
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 9, 2026
80c6805
do some cleanup, remove unneeded functions, avoid unnecessary cloning
petere-datadog Jul 9, 2026
dc23a53
reject invalid sum in summary metric. Avoid cloning attrs
petere-datadog Jul 9, 2026
aec7ac7
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 10, 2026
7c8be33
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 13, 2026
97f95f9
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 14, 2026
c9bbbd8
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 15, 2026
d22c3bd
Merge branch 'master' of github.com:vectordotdev/vector into peter.eh…
petere-datadog Jul 20, 2026
d36187f
add NaN checks. Fix issue with shifting time interval for delta metrics
petere-datadog Jul 20, 2026
7533cd5
reject negative summary sum
petere-datadog Jul 20, 2026
77df65f
do not create empty key attributes
petere-datadog Jul 21, 2026
74c3981
reject negative counter absolute values
petere-datadog Jul 21, 2026
1e00bbd
Update lib/opentelemetry-proto/src/metrics.rs
petere-datadog Jul 21, 2026
15c22f6
Update lib/codecs/src/encoding/format/otlp.rs
petere-datadog Jul 21, 2026
2dd3804
fix fmt issues from applying last commit
petere-datadog Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions changelog.d/otlp-metric-encoder.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The OTLP codec's serializer now supports native Vector `Metric` events for `Counter`, `Gauge`, `AggregatedHistogram`, and `AggregatedSummary` values, converting them into the OTLP `Sum`, `Gauge`, `Histogram`, and `Summary` protobuf types respectively. Previously, encoding a native metric event with the OTLP serializer always failed.

authors: petere-datadog
169 changes: 163 additions & 6 deletions lib/codecs/src/encoding/format/otlp.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::encoding::ProtobufSerializer;
use bytes::BytesMut;
use opentelemetry_proto::metrics::metric_event_to_export_request;
use opentelemetry_proto::proto::{
DESCRIPTOR_BYTES, LOGS_REQUEST_MESSAGE_TYPE, METRICS_REQUEST_MESSAGE_TYPE,
RESOURCE_LOGS_JSON_FIELD, RESOURCE_METRICS_JSON_FIELD, RESOURCE_SPANS_JSON_FIELD,
TRACES_REQUEST_MESSAGE_TYPE,
};
use prost::Message;
use tokio_util::codec::Encoder;
use vector_config_macros::configurable_component;
use vector_core::{config::DataType, event::Event, schema};
Expand All @@ -25,7 +27,7 @@ impl OtlpSerializerConfig {

/// The data type of events that are accepted by `OtlpSerializer`.
pub fn input_type(&self) -> DataType {
DataType::Log | DataType::Trace
DataType::all_bits()
}

/// The schema required by the serializer.
Expand Down Expand Up @@ -98,9 +100,6 @@ impl Encoder<Event> for OtlpSerializer {
type Error = vector_common::Error;

fn encode(&mut self, event: Event, buffer: &mut BytesMut) -> Result<(), Self::Error> {
// Determine which descriptor to use based on top-level OTLP fields
// This handles events that were decoded with use_otlp_decoding enabled
// The deserializer uses use_json_names: true, so fields are in camelCase
Comment thread
petere-datadog marked this conversation as resolved.
match &event {
Comment thread
petere-datadog marked this conversation as resolved.
Event::Log(log) => {
if log.contains(event_path!(RESOURCE_LOGS_JSON_FIELD)) {
Expand All @@ -125,9 +124,167 @@ impl Encoder<Event> for OtlpSerializer {
.into())
}
}
Event::Metric(_) => {
Err("OTLP serializer does not support native Vector metrics yet.".into())
Event::Metric(metric) => {
let request = metric_event_to_export_request(metric)?;
request.encode(buffer)?;
Comment thread
petere-datadog marked this conversation as resolved.
Ok(())
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
use opentelemetry_proto::proto::collector::metrics::v1::ExportMetricsServiceRequest;
use vector_core::event::{Metric, MetricKind, MetricTags, MetricValue, metric::Bucket};

// `into_event_iter` always wraps attributes in `Some(MetricTags)` (via `build_metric_tags`),
// even when there are none, so a tag-less input compares unequal to its round-tripped output
// unless we give it the same empty-but-present tag set up front.
fn with_empty_tags(metric: Metric) -> Metric {
metric.with_tags(Some(MetricTags::default()))
}

fn round_trip_metric(metric: Metric) -> Metric {
let mut serializer = OtlpSerializer::new().unwrap();
let mut buffer = BytesMut::new();
serializer
.encode(Event::Metric(metric), &mut buffer)
.expect("encode should succeed");

let request =
ExportMetricsServiceRequest::decode(buffer.freeze()).expect("decode should succeed");
let mut events: Vec<Event> = request
.resource_metrics
.into_iter()
.flat_map(|rm| rm.into_event_iter())
.collect();

assert_eq!(events.len(), 1);
match events.remove(0) {
Event::Metric(metric) => metric,
other => panic!("expected a metric event, got {other:?}"),
}
}

#[test]
fn round_trip_counter() {
let metric = with_empty_tags(
Metric::new(
"requests",
MetricKind::Incremental,
MetricValue::Counter { value: 42.0 },
)
.with_timestamp(Some(Utc.timestamp_nanos(1_000_000_000))),
);

assert_eq!(metric.clone(), round_trip_metric(metric));
}

#[test]
fn round_trip_gauge() {
let metric = with_empty_tags(
Metric::new(
"cpu_usage",
MetricKind::Absolute,
MetricValue::Gauge { value: 12.5 },
)
.with_timestamp(Some(Utc.timestamp_nanos(1_000_000_000))),
);

assert_eq!(metric.clone(), round_trip_metric(metric));
}

#[test]
fn round_trip_aggregated_histogram() {
let metric = with_empty_tags(
Metric::new(
"latency",
MetricKind::Absolute,
MetricValue::AggregatedHistogram {
buckets: vec![
Bucket {
upper_limit: 1.0,
count: 1,
},
Bucket {
upper_limit: 2.0,
count: 2,
},
Bucket {
upper_limit: f64::INFINITY,
count: 3,
},
],
count: 6,
sum: 10.0,
},
)
.with_timestamp(Some(Utc.timestamp_nanos(1_000_000_000))),
);

assert_eq!(metric.clone(), round_trip_metric(metric));
}

#[test]
fn round_trip_aggregated_summary() {
let metric = with_empty_tags(
Metric::new(
"response_time",
MetricKind::Absolute,
MetricValue::AggregatedSummary {
quantiles: vec![
vector_core::event::metric::Quantile {
quantile: 0.5,
value: 10.0,
},
vector_core::event::metric::Quantile {
quantile: 0.99,
value: 20.0,
},
],
count: 100,
sum: 1000.0,
},
)
.with_timestamp(Some(Utc.timestamp_nanos(1_000_000_000))),
);

assert_eq!(metric.clone(), round_trip_metric(metric));
}

#[test]
fn unsupported_metric_values_return_err() {
let mut serializer = OtlpSerializer::new().unwrap();
let mut buffer = BytesMut::new();

let set_metric = Metric::new(
"unique_users",
MetricKind::Incremental,
MetricValue::Set {
values: std::iter::once("a".to_string()).collect(),
},
);
assert!(
serializer
.encode(Event::Metric(set_metric), &mut buffer)
.is_err()
);

let distribution_metric = Metric::new(
"latencies",
MetricKind::Incremental,
MetricValue::Distribution {
samples: Vec::new(),
statistic: vector_core::event::metric::StatisticKind::Histogram,
},
);
assert!(
serializer
.encode(Event::Metric(distribution_metric), &mut buffer)
.is_err()
);
}
}
1 change: 1 addition & 0 deletions lib/opentelemetry-proto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ ordered-float.workspace = true
prost.workspace = true
tonic.workspace = true
vrl.workspace = true
vector-common = { path = "../vector-common", default-features = false }
vector-core = { path = "../vector-core", default-features = false }
34 changes: 32 additions & 2 deletions lib/opentelemetry-proto/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use bytes::Bytes;
use ordered_float::NotNan;
use vector_core::event::metric::TagValue;
use vector_core::event::metric::{TagValue, TagValueSet};
use vrl::value::{ObjectMap, Value};

use super::proto::common::v1::{KeyValue, any_value::Value as PBValue};
use super::proto::common::v1::{AnyValue, ArrayValue, KeyValue, any_value::Value as PBValue};

impl From<PBValue> for Value {
fn from(av: PBValue) -> Self {
Expand Down Expand Up @@ -37,6 +37,36 @@ impl From<PBValue> for TagValue {
}
}

impl From<&TagValue> for AnyValue {
fn from(tag: &TagValue) -> Self {
match tag {
TagValue::Value(s) => Self {
value: Some(PBValue::StringValue(s.clone())),
},
TagValue::Bare => Self { value: None },
}
}
}

pub fn str_to_key_value(key: &str, val: &TagValue) -> KeyValue {
Comment thread
petere-datadog marked this conversation as resolved.
KeyValue {
key: key.to_string(),
value: Some(val.into()),
}
}

pub fn tag_set_to_any_value(tag_set: &TagValueSet) -> Option<AnyValue> {
Comment thread
petere-datadog marked this conversation as resolved.
match tag_set {
TagValueSet::Empty => None,
TagValueSet::Single(tag) => Some(tag.into()),
TagValueSet::Set(set) => Some(AnyValue {
value: Some(PBValue::ArrayValue(ArrayValue {
values: set.iter().map(Into::into).collect(),
})),
}),
}
}

pub fn kv_list_into_value(arr: Vec<KeyValue>) -> Value {
Value::Object(
arr.into_iter()
Expand Down
Loading
Loading