Skip to content

feat(logs): add ability to capture and send logs #823

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 45 commits into from
Jun 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
33f85f6
feat(logs): add log protocol types
lcian May 26, 2025
fc0dbed
remove unnecessary type
lcian May 26, 2025
a1c5800
lints
lcian May 26, 2025
e57ccab
remove debug prints
lcian May 26, 2025
3c903a0
details
lcian May 26, 2025
f44b001
docstring
lcian May 26, 2025
85bfe34
Merge branch 'master' into lcian/feat/logs-types
lcian May 26, 2025
ad09fab
add full envelope test
lcian May 27, 2025
89b0e16
cargo fmt
lcian May 27, 2025
046f5e4
cargo fmt
lcian May 27, 2025
7a33e36
changelog
lcian May 27, 2025
f67fa67
wip
lcian May 28, 2025
58098ae
refactor
lcian May 28, 2025
8ed9e15
refactor
lcian May 28, 2025
4d8e557
remove file
lcian May 28, 2025
d1fa386
Merge branch 'master' into lcian/feat/logs-types
lcian May 28, 2025
a6ba441
add from
lcian May 28, 2025
51b2470
severity number changes
lcian May 28, 2025
7ef7157
changelog
lcian May 28, 2025
bd6e293
improve
lcian May 28, 2025
2cff09d
improve
lcian May 28, 2025
1a3a088
improve
lcian May 28, 2025
e915747
improve
lcian May 28, 2025
5c4fde5
feat(logs): add basic API
lcian May 28, 2025
855bd1c
honor enable_logs
lcian May 28, 2025
dce7c0a
nit
lcian May 28, 2025
81ac086
move feature to UNSTABLE_logs
lcian May 28, 2025
48c25ec
changelog
lcian May 28, 2025
de8a0df
non-exhaustive
lcian Jun 3, 2025
d851cb8
generic from value
lcian Jun 3, 2025
a36c769
severity_number optional
lcian Jun 3, 2025
b4070c1
simplify test
lcian Jun 4, 2025
85704c0
link to docs in code
lcian Jun 4, 2025
9ce8c2f
merge base and updates
lcian Jun 4, 2025
00af37d
changelog
lcian Jun 4, 2025
31037fe
doc string
lcian Jun 4, 2025
a2c39a9
doc string
lcian Jun 4, 2025
b6f2556
Merge branch 'lcian/feat/logs-types' into lcian/feat/logs-api
lcian Jun 4, 2025
bd5ab3f
update test
lcian Jun 4, 2025
914a414
update test
lcian Jun 4, 2025
ba64c78
changelog
lcian Jun 4, 2025
5f0e3aa
Update sentry-core/src/client.rs
lcian Jun 5, 2025
49a4e0c
address feedback
lcian Jun 5, 2025
313b278
use variable
lcian Jun 6, 2025
ab64707
Merge branch 'master' into lcian/feat/logs-api
lcian Jun 6, 2025
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: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@

- 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.
- feat(logs): add ability to capture and send logs (#823) by @lcian
- A method `capture_log` has been added to the `Hub` to enable sending logs.
- This is gated behind the `UNSTABLE_logs` feature flag (disabled by default).
- Additionally, the new client option `enable_logs` needs to be enabled for logs to be sent to Sentry.
- Please note that breaking changes could occur until the API is finalized.

## 0.38.1

Expand Down
1 change: 1 addition & 0 deletions sentry-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ default = []
client = ["rand"]
test = ["client", "release-health"]
release-health = []
UNSTABLE_logs = []

[dependencies]
log = { version = "0.4.8", optional = true, features = ["std"] }
Expand Down
85 changes: 83 additions & 2 deletions sentry-core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use std::panic::RefUnwindSafe;
use std::sync::{Arc, RwLock};
use std::time::Duration;

use rand::random;
#[cfg(feature = "UNSTABLE_logs")]
use crate::protocol::EnvelopeItem;
#[cfg(feature = "release-health")]
use sentry_types::protocol::v7::SessionUpdate;
use crate::protocol::SessionUpdate;
use rand::random;
use sentry_types::random_uuid;

use crate::constants::SDK_INFO;
Expand All @@ -18,6 +20,8 @@ use crate::types::{Dsn, Uuid};
#[cfg(feature = "release-health")]
use crate::SessionMode;
use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport};
#[cfg(feature = "UNSTABLE_logs")]
use sentry_types::protocol::v7::{Log, LogAttribute};

impl<T: Into<ClientOptions>> From<T> for Client {
fn from(o: T) -> Client {
Expand Down Expand Up @@ -363,6 +367,83 @@ impl Client {
random::<f32>() < rate
}
}

/// Captures a log and sends it to Sentry.
#[cfg(feature = "UNSTABLE_logs")]
pub fn capture_log(&self, log: Log, scope: &Scope) {
if !self.options().enable_logs {
return;
}
if let Some(ref transport) = *self.transport.read().unwrap() {
if let Some(log) = self.prepare_log(log, scope) {
let mut envelope = Envelope::new();
let logs: EnvelopeItem = vec![log].into();
envelope.add_item(logs);
transport.send_envelope(envelope);
}
}
}

/// Prepares a log to be sent, setting the `trace_id` and other default attributes, and
/// processing it through `before_send_log`.
#[cfg(feature = "UNSTABLE_logs")]
fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
scope.apply_to_log(&mut log, self.options.send_default_pii);

self.set_log_default_attributes(&mut log);

if let Some(ref func) = self.options.before_send_log {
log = func(log)?;
}

Some(log)
}

#[cfg(feature = "UNSTABLE_logs")]
fn set_log_default_attributes(&self, log: &mut Log) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is what all other SDKs are doing? IMO it might be a bit wasteful to copy the same fields into the log event for every single log event.

In the likely case that users are not logging any attributes, this will allocate a hashmap, and clone at least 4 * 2 Strings into it. That is quite some overhead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's true. Unfortunately this is what the spec mandates, every single log should have these default attributes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see any way of optimizing it? @Swatinem
Maybe we could pre populate a Map with some of the default attributes and store it somewhere to reuse it, but we would still need to clone it for every log.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about using a more optimized string type (like my own https://docs.rs/smol_buf/latest/smol_buf/) that can avoid clones in favor of refcounting or small string optimization.
That would cut down on memory usage and allocations a bit. Though the hashmap might still be heavyweight, and hard to avoid.

Not sure these things are worth the effort though? Considering that those would appear everywhere in public types, so it would likely cause some pain for users as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't know.
I was also thinking of optimizations using Cows, providing special fields to store the default attributes that would take in a Cow. This way we wouldn't clone.
But the special fields would need to be pub anyway, because the struct lives in sentry-types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can merge it as is for now and we can always think about optimizing in the future.
However I agree it kinda sucks we need to attach this to every single log.

if !log.attributes.contains_key("sentry.environment") {
if let Some(environment) = self.options.environment.as_ref() {
log.attributes.insert(
"sentry.sdk.version".to_owned(),
LogAttribute(environment.clone().into()),
);
}
}

if !log.attributes.contains_key("sentry.release") {
if let Some(release) = self.options.release.as_ref() {
log.attributes.insert(
"sentry.release".to_owned(),
LogAttribute(release.clone().into()),
);
}
}

if !log.attributes.contains_key("sentry.sdk.name") {
log.attributes.insert(
"sentry.sdk.name".to_owned(),
LogAttribute(self.sdk_info.name.to_owned().into()),
);
}

if !log.attributes.contains_key("sentry.sdk.version") {
log.attributes.insert(
"sentry.sdk.version".to_owned(),
LogAttribute(self.sdk_info.version.to_owned().into()),
);
}

// TODO: set OS (and Rust?) context
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot do this at the moment because sentry-contexts adds those through an event processor, which logs don't go through.


if !log.attributes.contains_key("server.address") {
if let Some(server) = &self.options.server_name {
log.attributes.insert(
"server.address".to_owned(),
LogAttribute(server.clone().into()),
);
}
}
}
}

// Make this unwind safe. It's not out of the box because of the
Expand Down
28 changes: 26 additions & 2 deletions sentry-core/src/clientoptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use std::time::Duration;

use crate::constants::USER_AGENT;
use crate::performance::TracesSampler;
#[cfg(feature = "UNSTABLE_logs")]
use crate::protocol::Log;
use crate::protocol::{Breadcrumb, Event};
use crate::types::Dsn;
use crate::{Integration, IntoDsn, TransportFactory};
Expand Down Expand Up @@ -144,6 +146,9 @@ pub struct ClientOptions {
pub before_send: Option<BeforeCallback<Event<'static>>>,
/// Callback that is executed for each Breadcrumb being added.
pub before_breadcrumb: Option<BeforeCallback<Breadcrumb>>,
/// Callback that is executed for each Log being added.
#[cfg(feature = "UNSTABLE_logs")]
pub before_send_log: Option<BeforeCallback<Log>>,
// Transport options
/// The transport to use.
///
Expand All @@ -162,6 +167,12 @@ pub struct ClientOptions {
pub https_proxy: Option<Cow<'static, str>>,
/// The timeout on client drop for draining events on shutdown.
pub shutdown_timeout: Duration,
/// Controls the maximum size of an HTTP request body that can be captured when using HTTP
/// server integrations. Needs `send_default_pii` to be enabled to have any effect.
pub max_request_body_size: MaxRequestBodySize,
/// Determines whether captured structured logs should be sent to Sentry (defaults to false).
#[cfg(feature = "UNSTABLE_logs")]
pub enable_logs: bool,
// Other options not documented in Unified API
/// Disable SSL verification.
///
Expand All @@ -186,8 +197,6 @@ pub struct ClientOptions {
pub trim_backtraces: bool,
/// The user agent that should be reported.
pub user_agent: Cow<'static, str>,
/// Controls how much of request bodies are captured
pub max_request_body_size: MaxRequestBodySize,
}

impl ClientOptions {
Expand Down Expand Up @@ -223,6 +232,12 @@ impl fmt::Debug for ClientOptions {
#[derive(Debug)]
struct BeforeBreadcrumb;
let before_breadcrumb = self.before_breadcrumb.as_ref().map(|_| BeforeBreadcrumb);
#[cfg(feature = "UNSTABLE_logs")]
let before_send_log = {
#[derive(Debug)]
struct BeforeSendLog;
self.before_send_log.as_ref().map(|_| BeforeSendLog)
};
#[derive(Debug)]
struct TransportFactory;

Expand Down Expand Up @@ -264,6 +279,11 @@ impl fmt::Debug for ClientOptions {
.field("auto_session_tracking", &self.auto_session_tracking)
.field("session_mode", &self.session_mode);

#[cfg(feature = "UNSTABLE_logs")]
debug_struct
.field("enable_logs", &self.enable_logs)
.field("before_send_log", &before_send_log);

debug_struct
.field("extra_border_frames", &self.extra_border_frames)
.field("trim_backtraces", &self.trim_backtraces)
Expand Down Expand Up @@ -305,6 +325,10 @@ impl Default for ClientOptions {
trim_backtraces: true,
user_agent: Cow::Borrowed(USER_AGENT),
max_request_body_size: MaxRequestBodySize::Medium,
#[cfg(feature = "UNSTABLE_logs")]
enable_logs: false,
#[cfg(feature = "UNSTABLE_logs")]
before_send_log: None,
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion sentry-core/src/hub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use std::sync::{Arc, RwLock};

use crate::protocol::{Event, Level, SessionStatus};
use crate::protocol::{Event, Level, Log, LogAttribute, LogLevel, Map, SessionStatus};
use crate::types::Uuid;
use crate::{Integration, IntoBreadcrumbs, Scope, ScopeGuard};

Expand Down Expand Up @@ -245,4 +245,14 @@ impl Hub {
})
}}
}

/// Captures a structured log.
#[cfg(feature = "UNSTABLE_logs")]
pub fn capture_log(&self, log: Log) {
with_client_impl! {{
let top = self.inner.with(|stack| stack.top().clone());
let Some(ref client) = top.client else { return };
client.capture_log(log, &top.scope);
}}
}
}
9 changes: 9 additions & 0 deletions sentry-core/src/scope/noop.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::fmt;

#[cfg(feature = "UNSTABLE_logs")]
use crate::protocol::Log;
use crate::protocol::{Context, Event, Level, User, Value};
use crate::TransactionOrSpan;

Expand Down Expand Up @@ -110,6 +112,13 @@ impl Scope {
minimal_unreachable!();
}

/// Applies the contained scoped data to fill a log.
#[cfg(feature = "UNSTABLE_logs")]
pub fn apply_to_log(&self, log: &mut Log) {
let _log = log;
minimal_unreachable!();
}

/// Set the given [`TransactionOrSpan`] as the active span for this scope.
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
let _ = span;
Expand Down
61 changes: 58 additions & 3 deletions sentry-core/src/scope/real.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ use std::fmt;
use std::sync::Mutex;
use std::sync::{Arc, PoisonError, RwLock};

use sentry_types::protocol::v7::TraceContext;

use crate::performance::TransactionOrSpan;
use crate::protocol::{Attachment, Breadcrumb, Context, Event, Level, Transaction, User, Value};
use crate::protocol::{
Attachment, Breadcrumb, Context, Event, Level, TraceContext, Transaction, User, Value,
};
#[cfg(feature = "UNSTABLE_logs")]
use crate::protocol::{Log, LogAttribute};
#[cfg(feature = "release-health")]
use crate::session::Session;
use crate::{Client, SentryTrace, TraceHeader, TraceHeadersIter};
Expand Down Expand Up @@ -346,6 +348,59 @@ impl Scope {
);
}

/// Applies the contained scoped data to a log, setting the `trace_id` and certain default
/// attributes.
#[cfg(feature = "UNSTABLE_logs")]
pub fn apply_to_log(&self, log: &mut Log, send_default_pii: bool) {
if let Some(span) = self.span.as_ref() {
log.trace_id = Some(span.get_trace_context().trace_id);
} else {
log.trace_id = Some(self.propagation_context.trace_id);
}

if !log.attributes.contains_key("sentry.trace.parent_span_id") {
if let Some(span) = self.get_span() {
let span_id = match span {
crate::TransactionOrSpan::Transaction(transaction) => {
transaction.get_trace_context().span_id
}
crate::TransactionOrSpan::Span(span) => span.get_span_id(),
};
log.attributes.insert(
"parent_span_id".to_owned(),
LogAttribute(span_id.to_string().into()),
);
}
}

if send_default_pii {
if let Some(user) = self.user.as_ref() {
if !log.attributes.contains_key("user.id") {
if let Some(id) = user.id.as_ref() {
log.attributes
.insert("user.id".to_owned(), LogAttribute(id.to_owned().into()));
}
}

if !log.attributes.contains_key("user.name") {
if let Some(name) = user.username.as_ref() {
log.attributes
.insert("user.name".to_owned(), LogAttribute(name.to_owned().into()));
}
}

if !log.attributes.contains_key("user.email") {
if let Some(email) = user.email.as_ref() {
log.attributes.insert(
"user.email".to_owned(),
LogAttribute(email.to_owned().into()),
);
}
}
}
}
}

/// Set the given [`TransactionOrSpan`] as the active span for this scope.
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
self.span = Arc::new(span);
Expand Down
4 changes: 2 additions & 2 deletions sentry-types/src/protocol/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1071,15 +1071,15 @@ some content
Log {
level: protocol::LogLevel::Warn,
body: "test".to_owned(),
trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
trace_id: Some("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(),
trace_id: Some("332253d614472a2c9f89e232b7762c28".parse().unwrap()),
timestamp: timestamp("2021-07-21T14:51:14.296Z"),
severity_number: Some(1.try_into().unwrap()),
attributes: attributes_2,
Expand Down
3 changes: 2 additions & 1 deletion sentry-types/src/protocol/v7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2127,7 +2127,8 @@ pub struct Log {
/// The log body/message (required).
pub body: String,
/// The ID of the Trace in which this log happened (required).
pub trace_id: TraceId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace_id: Option<TraceId>,
/// The timestamp of the log (required).
#[serde(with = "ts_seconds_float")]
pub timestamp: SystemTime,
Expand Down
1 change: 1 addition & 0 deletions sentry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ opentelemetry = ["sentry-opentelemetry"]
# other features
test = ["sentry-core/test"]
release-health = ["sentry-core/release-health", "sentry-actix?/release-health"]
UNSTABLE_logs = ["sentry-core/UNSTABLE_logs"]
# transports
transport = ["reqwest", "native-tls"]
reqwest = ["dep:reqwest", "httpdate", "tokio"]
Expand Down
Loading