Skip to content

Commit da1a44c

Browse files
szokeasaurusrexjjbayerlcian
committed
feat(sentry-types): Add metric protocol envelope support (#1022)
Add `Metric` 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 da1a44c

File tree

3 files changed

+133
-2
lines changed

3 files changed

+133
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
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)).
13+
- Add `Metric` and `MetricType` types representing [trace metrics](https://develop.sentry.dev/sdk/telemetry/metrics/) ([#1026](https://github.com/getsentry/sentry-rust/pull/1026)).
1214
- Expose transport utilities ([#949](https://github.com/getsentry/sentry-rust/pull/949))
1315

1416
### Fixes

sentry-types/src/protocol/envelope.rs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use crate::Dsn;
1010
use super::v7 as protocol;
1111

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

1717
/// Raised if a envelope cannot be parsed from a given input.
@@ -127,6 +127,10 @@ enum EnvelopeItemType {
127127
/// A container of Log items.
128128
#[serde(rename = "log")]
129129
LogsContainer,
130+
/// A container of Metric items.
131+
/// Serialized to a `trace_metric` envelope item.
132+
#[serde(rename = "trace_metric")]
133+
MetricsContainer,
130134
}
131135

132136
/// An Envelope Item Header.
@@ -192,6 +196,8 @@ pub enum EnvelopeItem {
192196
pub enum ItemContainer {
193197
/// A list of logs.
194198
Logs(Vec<Log>),
199+
/// A list of metrics.
200+
Metrics(Vec<Metric>),
195201
}
196202

197203
#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")]
@@ -200,20 +206,23 @@ impl ItemContainer {
200206
pub fn len(&self) -> usize {
201207
match self {
202208
Self::Logs(logs) => logs.len(),
209+
Self::Metrics(metrics) => metrics.len(),
203210
}
204211
}
205212

206213
/// The `type` of this item container, which corresponds to the `type` of the contained items.
207214
pub fn ty(&self) -> &'static str {
208215
match self {
209216
Self::Logs(_) => "log",
217+
Self::Metrics(_) => "trace_metric",
210218
}
211219
}
212220

213221
/// The `content-type` expected by Relay for this item container.
214222
pub fn content_type(&self) -> &'static str {
215223
match self {
216224
Self::Logs(_) => "application/vnd.sentry.items.log+json",
225+
Self::Metrics(_) => "application/vnd.sentry.items.trace-metric+json",
217226
}
218227
}
219228
}
@@ -235,6 +244,12 @@ struct ItemsSerdeWrapper<'a, T: Clone> {
235244
items: Cow<'a, [T]>,
236245
}
237246

247+
impl From<Vec<Metric>> for ItemContainer {
248+
fn from(metrics: Vec<Metric>) -> Self {
249+
Self::Metrics(metrics)
250+
}
251+
}
252+
238253
impl From<Event<'static>> for EnvelopeItem {
239254
fn from(event: Event<'static>) -> Self {
240255
EnvelopeItem::Event(event)
@@ -283,6 +298,12 @@ impl From<Vec<Log>> for EnvelopeItem {
283298
}
284299
}
285300

301+
impl From<Vec<Metric>> for EnvelopeItem {
302+
fn from(metrics: Vec<Metric>) -> Self {
303+
EnvelopeItem::ItemContainer(metrics.into())
304+
}
305+
}
306+
286307
/// An Iterator over the items of an Envelope.
287308
#[derive(Clone)]
288309
pub struct EnvelopeItemIter<'s> {
@@ -506,6 +527,12 @@ impl Envelope {
506527
let wrapper = ItemsSerdeWrapper { items: logs.into() };
507528
serde_json::to_writer(&mut item_buf, &wrapper)?
508529
}
530+
ItemContainer::Metrics(metrics) => {
531+
let wrapper = ItemsSerdeWrapper {
532+
items: metrics.into(),
533+
};
534+
serde_json::to_writer(&mut item_buf, &wrapper)?
535+
}
509536
},
510537
EnvelopeItem::Raw => {
511538
continue;
@@ -677,6 +704,10 @@ impl Envelope {
677704
serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
678705
.map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items.into())))
679706
}
707+
EnvelopeItemType::MetricsContainer => {
708+
serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
709+
.map(|x| EnvelopeItem::ItemContainer(ItemContainer::Metrics(x.items.into())))
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_metric_container_header() {
1158+
let metrics: EnvelopeItem = vec![Metric {
1159+
r#type: protocol::MetricType::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 metrics: EnvelopeItem = vec![Metric {
1278+
r#type: protocol::MetricType::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(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 [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 MetricType {
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 [metric](https://develop.sentry.dev/sdk/telemetry/metrics/).
2385+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
2386+
#[non_exhaustive]
2387+
pub struct Metric {
2388+
/// The metric type.
2389+
pub r#type: MetricType,
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)