Skip to content

feat(logs): add log protocol types #821

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jun 6, 2025
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
- This information is used as a fallback when capturing an event with tracing disabled or otherwise no ongoing span, to still allow related events to be linked by a trace.
- A new API `Scope::iter_trace_propagation_headers` has been provided that will use the fallback tracing information if there is no current `Span` on the `Scope`.

### Features

- feat(logs): add log protocol types (#821) by @lcian
- Basic types for [Sentry structured logs](https://docs.sentry.io/product/explore/logs/) have been added.
- It's possible (but not recommended) to use them to send logs to Sentry by directly constructing an `Envelope` containing an `ItemContainer::Logs` envelope item and sending it through `Client::send_envelope`.
- A high-level API and integrations will come soon.

## 0.38.1

### Fixes
Expand Down
141 changes: 130 additions & 11 deletions sentry-types/src/protocol/envelope.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use std::{io::Write, path::Path};

use serde::Deserialize;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;

use super::v7 as protocol;

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

Expand Down Expand Up @@ -61,20 +61,24 @@ enum EnvelopeItemType {
/// An Attachment Item type.
#[serde(rename = "attachment")]
Attachment,
/// A Monitor Check In Item Type
/// A Monitor Check In Item Type.
#[serde(rename = "check_in")]
MonitorCheckIn,
/// A container of Log items.
#[serde(rename = "log")]
LogsContainer,
}

/// An Envelope Item Header.
#[derive(Clone, Debug, Deserialize)]
struct EnvelopeItemHeader {
r#type: EnvelopeItemType,
length: Option<usize>,
// Fields below apply only to Attachment Item type
// Applies both to Attachment and ItemContainer Item type
content_type: Option<String>,
// Fields below apply only to Attachment Item types
filename: Option<String>,
attachment_type: Option<AttachmentType>,
content_type: Option<String>,
}

/// An Envelope Item.
Expand Down Expand Up @@ -112,12 +116,64 @@ pub enum EnvelopeItem {
Attachment(Attachment),
/// A MonitorCheckIn item.
MonitorCheckIn(MonitorCheckIn),
/// A container for a list of multiple items.
ItemContainer(ItemContainer),
/// This is a sentinel item used to `filter` raw envelopes.
Raw,
// TODO:
// etc…
}

/// A container for a list of multiple items.
/// It's considered a single envelope item, with its `type` corresponding to the contained items'
/// `type`.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum ItemContainer {
/// A list of logs.
Logs(Vec<Log>),
}

#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")]
impl ItemContainer {
/// The number of items in this item container.
pub fn len(&self) -> usize {
match self {
Self::Logs(logs) => logs.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",
}
}

/// 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",
}
}
}

impl From<Vec<Log>> for ItemContainer {
fn from(logs: Vec<Log>) -> Self {
Self::Logs(logs)
}
}

#[derive(Serialize)]
struct LogsSerializationWrapper<'a> {
items: &'a [Log],
}

#[derive(Deserialize)]
struct LogsDeserializationWrapper {
items: Vec<Log>,
}

impl From<Event<'static>> for EnvelopeItem {
fn from(event: Event<'static>) -> Self {
EnvelopeItem::Event(event)
Expand Down Expand Up @@ -154,6 +210,18 @@ impl From<MonitorCheckIn> for EnvelopeItem {
}
}

impl From<ItemContainer> for EnvelopeItem {
fn from(container: ItemContainer) -> Self {
EnvelopeItem::ItemContainer(container)
}
}

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

/// An Iterator over the items of an Envelope.
#[derive(Clone)]
pub struct EnvelopeItemIter<'s> {
Expand Down Expand Up @@ -352,6 +420,12 @@ impl Envelope {
EnvelopeItem::MonitorCheckIn(check_in) => {
serde_json::to_writer(&mut item_buf, check_in)?
}
EnvelopeItem::ItemContainer(container) => match container {
ItemContainer::Logs(logs) => {
let wrapper = LogsSerializationWrapper { items: logs };
serde_json::to_writer(&mut item_buf, &wrapper)?
}
},
EnvelopeItem::Raw => {
continue;
}
Expand All @@ -362,14 +436,26 @@ impl Envelope {
EnvelopeItem::SessionAggregates(_) => "sessions",
EnvelopeItem::Transaction(_) => "transaction",
EnvelopeItem::MonitorCheckIn(_) => "check_in",
EnvelopeItem::ItemContainer(container) => container.ty(),
EnvelopeItem::Attachment(_) | EnvelopeItem::Raw => unreachable!(),
};
writeln!(
writer,
r#"{{"type":"{}","length":{}}}"#,
item_type,
item_buf.len()
)?;

if let EnvelopeItem::ItemContainer(container) = item {
writeln!(
writer,
r#"{{"type":"{}","item_count":{},"content_type":"{}"}}"#,
item_type,
container.len(),
container.content_type()
)?;
} else {
writeln!(
writer,
r#"{{"type":"{}","length":{}}}"#,
item_type,
item_buf.len()
)?;
}
writer.write_all(&item_buf)?;
writeln!(writer)?;
item_buf.clear();
Expand Down Expand Up @@ -506,6 +592,10 @@ impl Envelope {
EnvelopeItemType::MonitorCheckIn => {
serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn)
}
EnvelopeItemType::LogsContainer => {
serde_json::from_slice::<LogsDeserializationWrapper>(payload)
.map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items)))
}
}
.map_err(EnvelopeError::InvalidItemPayload)?;

Expand Down Expand Up @@ -536,6 +626,7 @@ mod test {
use std::str::FromStr;
use std::time::{Duration, SystemTime};

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

Expand Down Expand Up @@ -969,12 +1060,40 @@ some content
..Default::default()
};

let mut attributes = Map::new();
attributes.insert("key".into(), "value".into());
attributes.insert("num".into(), 10.into());
attributes.insert("val".into(), 10.2.into());
attributes.insert("bool".into(), false.into());
let mut attributes_2 = attributes.clone();
attributes_2.insert("more".into(), true.into());
let logs: EnvelopeItem = vec![
Log {
level: protocol::LogLevel::Warn,
body: "test".to_owned(),
trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
timestamp: timestamp("2022-07-25T14:51:14.296Z"),
severity_number: Some(1.try_into().unwrap()),
attributes,
},
Log {
level: protocol::LogLevel::Error,
body: "a body".to_owned(),
trace_id: "332253d614472a2c9f89e232b7762c28".parse().unwrap(),
timestamp: timestamp("2021-07-21T14:51:14.296Z"),
severity_number: Some(1.try_into().unwrap()),
attributes: attributes_2,
},
]
.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);

let serialized = to_str(envelope);
let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap();
Expand Down
Loading