Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### New Features

- 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)).
- Add SDK protocol support for sending trace metric envelope items ([#1022](https://github.com/getsentry/sentry-rust/pull/1022)).
- Expose transport utilities ([#949](https://github.com/getsentry/sentry-rust/pull/949))

### Fixes
Expand Down
92 changes: 91 additions & 1 deletion sentry-types/src/protocol/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::v7 as protocol;

use protocol::{
Attachment, AttachmentType, ClientSdkInfo, DynamicSamplingContext, Event, Log, MonitorCheckIn,
SessionAggregates, SessionUpdate, Transaction,
SessionAggregates, SessionUpdate, TraceMetric, Transaction,
};

/// Raised if a envelope cannot be parsed from a given input.
Expand Down Expand Up @@ -127,6 +127,9 @@ enum EnvelopeItemType {
/// A container of Log items.
#[serde(rename = "log")]
LogsContainer,
/// A container of TraceMetric items.
#[serde(rename = "trace_metric")]
TraceMetricsContainer,
}

/// An Envelope Item Header.
Expand Down Expand Up @@ -192,6 +195,8 @@ pub enum EnvelopeItem {
pub enum ItemContainer {
/// A list of logs.
Logs(Vec<Log>),
/// A list of trace metrics.
TraceMetrics(Vec<TraceMetric>),
}

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

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

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

impl From<Vec<TraceMetric>> for ItemContainer {
fn from(metrics: Vec<TraceMetric>) -> Self {
Self::TraceMetrics(metrics)
}
}

impl From<Event<'static>> for EnvelopeItem {
fn from(event: Event<'static>) -> Self {
EnvelopeItem::Event(event)
Expand Down Expand Up @@ -283,6 +297,12 @@ impl From<Vec<Log>> for EnvelopeItem {
}
}

impl From<Vec<TraceMetric>> for EnvelopeItem {
fn from(metrics: Vec<TraceMetric>) -> Self {
EnvelopeItem::ItemContainer(metrics.into())
}
}

/// An Iterator over the items of an Envelope.
#[derive(Clone)]
pub struct EnvelopeItemIter<'s> {
Expand Down Expand Up @@ -506,6 +526,12 @@ impl Envelope {
let wrapper = ItemsSerdeWrapper { items: logs.into() };
serde_json::to_writer(&mut item_buf, &wrapper)?
}
ItemContainer::TraceMetrics(metrics) => {
let wrapper = ItemsSerdeWrapper {
items: metrics.into(),
};
serde_json::to_writer(&mut item_buf, &wrapper)?
}
},
EnvelopeItem::Raw => {
continue;
Expand Down Expand Up @@ -677,6 +703,11 @@ impl Envelope {
serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
.map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items.into())))
}
EnvelopeItemType::TraceMetricsContainer => {
serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload).map(|x| {
EnvelopeItem::ItemContainer(ItemContainer::TraceMetrics(x.items.into()))
})
}
}
.map_err(EnvelopeError::InvalidItemPayload)?;

Expand Down Expand Up @@ -708,6 +739,7 @@ mod test {
use std::time::{Duration, SystemTime};

use protocol::Map;
use serde_json::Value;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;

Expand Down Expand Up @@ -1121,6 +1153,49 @@ some content
assert_eq!(expected, serialized.as_bytes());
}

#[test]
fn test_trace_metric_container_header() {
let metrics: EnvelopeItem = vec![TraceMetric {
r#type: protocol::TraceMetricType::Counter,
name: "api.requests".into(),
value: 1.0,
timestamp: timestamp("2026-03-02T13:36:02.000Z"),
trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
span_id: None,
unit: None,
attributes: Map::new(),
}]
.into();

let mut envelope = Envelope::new();
envelope.add_item(metrics);

let expected = [
serde_json::json!({}),
serde_json::json!({
"type": "trace_metric",
"item_count": 1,
"content_type": "application/vnd.sentry.items.trace-metric+json"
}),
serde_json::json!({
"items": [{
"type": "counter",
"name": "api.requests",
"value": 1.0,
"timestamp": 1772458562,
"trace_id": "335e53d614474acc9f89e632b776cc28"
}]
}),
];

let serialized = to_str(envelope);
let actual = serialized
.lines()
.map(|line| serde_json::from_str::<Value>(line).expect("envelope has invalid JSON"));

assert!(actual.eq(expected.into_iter()));
}

// Test all possible item types in a single envelope
#[test]
fn test_deserialize_serialized() {
Expand Down Expand Up @@ -1197,12 +1272,27 @@ some content
]
.into();

let mut metric_attributes = Map::new();
metric_attributes.insert("route".into(), "/users".into());
let trace_metrics: EnvelopeItem = vec![TraceMetric {
r#type: protocol::TraceMetricType::Distribution,
name: "response.time".to_owned(),
value: 123.4,
timestamp: timestamp("2022-07-26T14:51:14.296Z"),
trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
span_id: Some("d42cee9fc3e74f5c".parse().unwrap()),
unit: Some("millisecond".to_owned()),
attributes: metric_attributes,
}]
.into();

let mut envelope: Envelope = Envelope::new();
envelope.add_item(event);
envelope.add_item(transaction);
envelope.add_item(session);
envelope.add_item(attachment);
envelope.add_item(logs);
envelope.add_item(trace_metrics);

let serialized = to_str(envelope);
let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap();
Expand Down
39 changes: 39 additions & 0 deletions sentry-types/src/protocol/v7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2368,6 +2368,45 @@ impl<'de> Deserialize<'de> for LogAttribute {
}
}

/// The type of a [trace metric](https://develop.sentry.dev/sdk/telemetry/metrics/).
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum TraceMetricType {
/// A counter metric that only increments.
Counter,
/// A gauge metric that can go up and down.
Gauge,
/// A distribution metric for statistical spread measurements.
Distribution,
}

/// A single [trace metric](https://develop.sentry.dev/sdk/telemetry/metrics/).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct TraceMetric {
/// The metric type.
pub r#type: TraceMetricType,
/// The metric name. Uses dot separators for hierarchy.
pub name: String,
/// The numeric value.
pub value: f64,
/// The timestamp when recorded.
#[serde(with = "ts_seconds_float")]
pub timestamp: SystemTime,
/// The trace ID this metric is associated with.
pub trace_id: TraceId,
/// The span ID of the active span, if any.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span_id: Option<SpanId>,
/// The measurement unit.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
/// Additional key-value attributes.
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub attributes: Map<String, LogAttribute>,
}

/// An ID that identifies an organization in the Sentry backend.
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct OrganizationId(u64);
Expand Down