Skip to content

Commit d06efa0

Browse files
szokeasaurusrexjjbayerlcian
committed
feat(sentry-types): Add trace metric protocol envelope support (#1022)
Add `TraceMetric` protocol types and `trace_metric` envelope container serialization/deserialization in `sentry-types`. Closes #1008 Closes [RUST-159](https://linear.app/getsentry/issue/RUST-159/add-trace-metric-protocol-models-and-envelope-item-container-support) Co-authored-by: Joris Bayer <joris.bayer@sentry.io> Co-authored-by: Lorenzo Cian <17258265+lcian@users.noreply.github.com>
1 parent d7a6da9 commit d06efa0

3 files changed

Lines changed: 131 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### New Features
1010

1111
- Added a `Envelope::into_items` method, which returns an iterator over owned [`EnvelopeItem`s](https://docs.rs/sentry/0.46.2/sentry/protocol/enum.EnvelopeItem.html) in the [`Envelope`](https://docs.rs/sentry/0.46.2/sentry/struct.Envelope.html) ([#983](https://github.com/getsentry/sentry-rust/pull/983)).
12+
- Add SDK protocol support for sending trace metric envelope items ([#1022](https://github.com/getsentry/sentry-rust/pull/1022)).
1213
- Expose transport utilities ([#949](https://github.com/getsentry/sentry-rust/pull/949))
1314

1415
### Fixes

sentry-types/src/protocol/envelope.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use super::v7 as protocol;
1111

1212
use protocol::{
1313
Attachment, AttachmentType, ClientSdkInfo, DynamicSamplingContext, Event, Log, MonitorCheckIn,
14-
SessionAggregates, SessionUpdate, Transaction,
14+
SessionAggregates, SessionUpdate, TraceMetric, Transaction,
1515
};
1616

1717
/// Raised if a envelope cannot be parsed from a given input.
@@ -127,6 +127,9 @@ enum EnvelopeItemType {
127127
/// A container of Log items.
128128
#[serde(rename = "log")]
129129
LogsContainer,
130+
/// A container of TraceMetric items.
131+
#[serde(rename = "trace_metric")]
132+
TraceMetricsContainer,
130133
}
131134

132135
/// An Envelope Item Header.
@@ -192,6 +195,8 @@ pub enum EnvelopeItem {
192195
pub enum ItemContainer {
193196
/// A list of logs.
194197
Logs(Vec<Log>),
198+
/// A list of trace metrics.
199+
TraceMetrics(Vec<TraceMetric>),
195200
}
196201

197202
#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")]
@@ -200,20 +205,23 @@ impl ItemContainer {
200205
pub fn len(&self) -> usize {
201206
match self {
202207
Self::Logs(logs) => logs.len(),
208+
Self::TraceMetrics(metrics) => metrics.len(),
203209
}
204210
}
205211

206212
/// The `type` of this item container, which corresponds to the `type` of the contained items.
207213
pub fn ty(&self) -> &'static str {
208214
match self {
209215
Self::Logs(_) => "log",
216+
Self::TraceMetrics(_) => "trace_metric",
210217
}
211218
}
212219

213220
/// The `content-type` expected by Relay for this item container.
214221
pub fn content_type(&self) -> &'static str {
215222
match self {
216223
Self::Logs(_) => "application/vnd.sentry.items.log+json",
224+
Self::TraceMetrics(_) => "application/vnd.sentry.items.trace-metric+json",
217225
}
218226
}
219227
}
@@ -235,6 +243,12 @@ struct ItemsSerdeWrapper<'a, T: Clone> {
235243
items: Cow<'a, [T]>,
236244
}
237245

246+
impl From<Vec<TraceMetric>> for ItemContainer {
247+
fn from(metrics: Vec<TraceMetric>) -> Self {
248+
Self::TraceMetrics(metrics)
249+
}
250+
}
251+
238252
impl From<Event<'static>> for EnvelopeItem {
239253
fn from(event: Event<'static>) -> Self {
240254
EnvelopeItem::Event(event)
@@ -283,6 +297,12 @@ impl From<Vec<Log>> for EnvelopeItem {
283297
}
284298
}
285299

300+
impl From<Vec<TraceMetric>> for EnvelopeItem {
301+
fn from(metrics: Vec<TraceMetric>) -> Self {
302+
EnvelopeItem::ItemContainer(metrics.into())
303+
}
304+
}
305+
286306
/// An Iterator over the items of an Envelope.
287307
#[derive(Clone)]
288308
pub struct EnvelopeItemIter<'s> {
@@ -506,6 +526,12 @@ impl Envelope {
506526
let wrapper = ItemsSerdeWrapper { items: logs.into() };
507527
serde_json::to_writer(&mut item_buf, &wrapper)?
508528
}
529+
ItemContainer::TraceMetrics(metrics) => {
530+
let wrapper = ItemsSerdeWrapper {
531+
items: metrics.into(),
532+
};
533+
serde_json::to_writer(&mut item_buf, &wrapper)?
534+
}
509535
},
510536
EnvelopeItem::Raw => {
511537
continue;
@@ -677,6 +703,11 @@ impl Envelope {
677703
serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
678704
.map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items.into())))
679705
}
706+
EnvelopeItemType::TraceMetricsContainer => {
707+
serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload).map(|x| {
708+
EnvelopeItem::ItemContainer(ItemContainer::TraceMetrics(x.items.into()))
709+
})
710+
}
680711
}
681712
.map_err(EnvelopeError::InvalidItemPayload)?;
682713

@@ -708,6 +739,7 @@ mod test {
708739
use std::time::{Duration, SystemTime};
709740

710741
use protocol::Map;
742+
use serde_json::Value;
711743
use time::format_description::well_known::Rfc3339;
712744
use time::OffsetDateTime;
713745

@@ -1121,6 +1153,49 @@ some content
11211153
assert_eq!(expected, serialized.as_bytes());
11221154
}
11231155

1156+
#[test]
1157+
fn test_trace_metric_container_header() {
1158+
let metrics: EnvelopeItem = vec![TraceMetric {
1159+
r#type: protocol::TraceMetricType::Counter,
1160+
name: "api.requests".into(),
1161+
value: 1.0,
1162+
timestamp: timestamp("2026-03-02T13:36:02.000Z"),
1163+
trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1164+
span_id: None,
1165+
unit: None,
1166+
attributes: Map::new(),
1167+
}]
1168+
.into();
1169+
1170+
let mut envelope = Envelope::new();
1171+
envelope.add_item(metrics);
1172+
1173+
let expected = [
1174+
serde_json::json!({}),
1175+
serde_json::json!({
1176+
"type": "trace_metric",
1177+
"item_count": 1,
1178+
"content_type": "application/vnd.sentry.items.trace-metric+json"
1179+
}),
1180+
serde_json::json!({
1181+
"items": [{
1182+
"type": "counter",
1183+
"name": "api.requests",
1184+
"value": 1.0,
1185+
"timestamp": 1772458562,
1186+
"trace_id": "335e53d614474acc9f89e632b776cc28"
1187+
}]
1188+
}),
1189+
];
1190+
1191+
let serialized = to_str(envelope);
1192+
let actual = serialized
1193+
.lines()
1194+
.map(|line| serde_json::from_str::<Value>(line).expect("envelope has invalid JSON"));
1195+
1196+
assert!(actual.eq(expected.into_iter()));
1197+
}
1198+
11241199
// Test all possible item types in a single envelope
11251200
#[test]
11261201
fn test_deserialize_serialized() {
@@ -1197,12 +1272,27 @@ some content
11971272
]
11981273
.into();
11991274

1275+
let mut metric_attributes = Map::new();
1276+
metric_attributes.insert("route".into(), "/users".into());
1277+
let trace_metrics: EnvelopeItem = vec![TraceMetric {
1278+
r#type: protocol::TraceMetricType::Distribution,
1279+
name: "response.time".to_owned(),
1280+
value: 123.4,
1281+
timestamp: timestamp("2022-07-26T14:51:14.296Z"),
1282+
trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1283+
span_id: Some("d42cee9fc3e74f5c".parse().unwrap()),
1284+
unit: Some("millisecond".to_owned()),
1285+
attributes: metric_attributes,
1286+
}]
1287+
.into();
1288+
12001289
let mut envelope: Envelope = Envelope::new();
12011290
envelope.add_item(event);
12021291
envelope.add_item(transaction);
12031292
envelope.add_item(session);
12041293
envelope.add_item(attachment);
12051294
envelope.add_item(logs);
1295+
envelope.add_item(trace_metrics);
12061296

12071297
let serialized = to_str(envelope);
12081298
let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap();

sentry-types/src/protocol/v7.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2368,6 +2368,45 @@ impl<'de> Deserialize<'de> for LogAttribute {
23682368
}
23692369
}
23702370

2371+
/// The type of a [trace metric](https://develop.sentry.dev/sdk/telemetry/metrics/).
2372+
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
2373+
#[serde(rename_all = "lowercase")]
2374+
#[non_exhaustive]
2375+
pub enum TraceMetricType {
2376+
/// A counter metric that only increments.
2377+
Counter,
2378+
/// A gauge metric that can go up and down.
2379+
Gauge,
2380+
/// A distribution metric for statistical spread measurements.
2381+
Distribution,
2382+
}
2383+
2384+
/// A single [trace metric](https://develop.sentry.dev/sdk/telemetry/metrics/).
2385+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
2386+
#[non_exhaustive]
2387+
pub struct TraceMetric {
2388+
/// The metric type.
2389+
pub r#type: TraceMetricType,
2390+
/// The metric name. Uses dot separators for hierarchy.
2391+
pub name: String,
2392+
/// The numeric value.
2393+
pub value: f64,
2394+
/// The timestamp when recorded.
2395+
#[serde(with = "ts_seconds_float")]
2396+
pub timestamp: SystemTime,
2397+
/// The trace ID this metric is associated with.
2398+
pub trace_id: TraceId,
2399+
/// The span ID of the active span, if any.
2400+
#[serde(default, skip_serializing_if = "Option::is_none")]
2401+
pub span_id: Option<SpanId>,
2402+
/// The measurement unit.
2403+
#[serde(default, skip_serializing_if = "Option::is_none")]
2404+
pub unit: Option<String>,
2405+
/// Additional key-value attributes.
2406+
#[serde(default, skip_serializing_if = "Map::is_empty")]
2407+
pub attributes: Map<String, LogAttribute>,
2408+
}
2409+
23712410
/// An ID that identifies an organization in the Sentry backend.
23722411
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
23732412
pub struct OrganizationId(u64);

0 commit comments

Comments
 (0)