Skip to content

Commit d668e7d

Browse files
lcianSwatinem
andauthored
feat(logs): add ability to capture and send logs (#823)
Co-authored-by: Arpad Borsos <[email protected]>
1 parent 7abbb38 commit d668e7d

File tree

11 files changed

+247
-13
lines changed

11 files changed

+247
-13
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818

1919
- feat(logs): add log protocol types (#821) by @lcian
2020
- Basic types for [Sentry structured logs](https://docs.sentry.io/product/explore/logs/) have been added.
21-
- 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`.
22-
- A high-level API and integrations will come soon.
21+
- feat(logs): add ability to capture and send logs (#823) by @lcian
22+
- A method `capture_log` has been added to the `Hub` to enable sending logs.
23+
- This is gated behind the `UNSTABLE_logs` feature flag (disabled by default).
24+
- Additionally, the new client option `enable_logs` needs to be enabled for logs to be sent to Sentry.
25+
- Please note that breaking changes could occur until the API is finalized.
2326

2427
## 0.38.1
2528

sentry-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ default = []
2424
client = ["rand"]
2525
test = ["client", "release-health"]
2626
release-health = []
27+
UNSTABLE_logs = []
2728

2829
[dependencies]
2930
log = { version = "0.4.8", optional = true, features = ["std"] }

sentry-core/src/client.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ use std::panic::RefUnwindSafe;
55
use std::sync::{Arc, RwLock};
66
use std::time::Duration;
77

8-
use rand::random;
8+
#[cfg(feature = "UNSTABLE_logs")]
9+
use crate::protocol::EnvelopeItem;
910
#[cfg(feature = "release-health")]
10-
use sentry_types::protocol::v7::SessionUpdate;
11+
use crate::protocol::SessionUpdate;
12+
use rand::random;
1113
use sentry_types::random_uuid;
1214

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

2226
impl<T: Into<ClientOptions>> From<T> for Client {
2327
fn from(o: T) -> Client {
@@ -363,6 +367,83 @@ impl Client {
363367
random::<f32>() < rate
364368
}
365369
}
370+
371+
/// Captures a log and sends it to Sentry.
372+
#[cfg(feature = "UNSTABLE_logs")]
373+
pub fn capture_log(&self, log: Log, scope: &Scope) {
374+
if !self.options().enable_logs {
375+
return;
376+
}
377+
if let Some(ref transport) = *self.transport.read().unwrap() {
378+
if let Some(log) = self.prepare_log(log, scope) {
379+
let mut envelope = Envelope::new();
380+
let logs: EnvelopeItem = vec![log].into();
381+
envelope.add_item(logs);
382+
transport.send_envelope(envelope);
383+
}
384+
}
385+
}
386+
387+
/// Prepares a log to be sent, setting the `trace_id` and other default attributes, and
388+
/// processing it through `before_send_log`.
389+
#[cfg(feature = "UNSTABLE_logs")]
390+
fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
391+
scope.apply_to_log(&mut log, self.options.send_default_pii);
392+
393+
self.set_log_default_attributes(&mut log);
394+
395+
if let Some(ref func) = self.options.before_send_log {
396+
log = func(log)?;
397+
}
398+
399+
Some(log)
400+
}
401+
402+
#[cfg(feature = "UNSTABLE_logs")]
403+
fn set_log_default_attributes(&self, log: &mut Log) {
404+
if !log.attributes.contains_key("sentry.environment") {
405+
if let Some(environment) = self.options.environment.as_ref() {
406+
log.attributes.insert(
407+
"sentry.sdk.version".to_owned(),
408+
LogAttribute(environment.clone().into()),
409+
);
410+
}
411+
}
412+
413+
if !log.attributes.contains_key("sentry.release") {
414+
if let Some(release) = self.options.release.as_ref() {
415+
log.attributes.insert(
416+
"sentry.release".to_owned(),
417+
LogAttribute(release.clone().into()),
418+
);
419+
}
420+
}
421+
422+
if !log.attributes.contains_key("sentry.sdk.name") {
423+
log.attributes.insert(
424+
"sentry.sdk.name".to_owned(),
425+
LogAttribute(self.sdk_info.name.to_owned().into()),
426+
);
427+
}
428+
429+
if !log.attributes.contains_key("sentry.sdk.version") {
430+
log.attributes.insert(
431+
"sentry.sdk.version".to_owned(),
432+
LogAttribute(self.sdk_info.version.to_owned().into()),
433+
);
434+
}
435+
436+
// TODO: set OS (and Rust?) context
437+
438+
if !log.attributes.contains_key("server.address") {
439+
if let Some(server) = &self.options.server_name {
440+
log.attributes.insert(
441+
"server.address".to_owned(),
442+
LogAttribute(server.clone().into()),
443+
);
444+
}
445+
}
446+
}
366447
}
367448

368449
// Make this unwind safe. It's not out of the box because of the

sentry-core/src/clientoptions.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::time::Duration;
55

66
use crate::constants::USER_AGENT;
77
use crate::performance::TracesSampler;
8+
#[cfg(feature = "UNSTABLE_logs")]
9+
use crate::protocol::Log;
810
use crate::protocol::{Breadcrumb, Event};
911
use crate::types::Dsn;
1012
use crate::{Integration, IntoDsn, TransportFactory};
@@ -144,6 +146,9 @@ pub struct ClientOptions {
144146
pub before_send: Option<BeforeCallback<Event<'static>>>,
145147
/// Callback that is executed for each Breadcrumb being added.
146148
pub before_breadcrumb: Option<BeforeCallback<Breadcrumb>>,
149+
/// Callback that is executed for each Log being added.
150+
#[cfg(feature = "UNSTABLE_logs")]
151+
pub before_send_log: Option<BeforeCallback<Log>>,
147152
// Transport options
148153
/// The transport to use.
149154
///
@@ -162,6 +167,12 @@ pub struct ClientOptions {
162167
pub https_proxy: Option<Cow<'static, str>>,
163168
/// The timeout on client drop for draining events on shutdown.
164169
pub shutdown_timeout: Duration,
170+
/// Controls the maximum size of an HTTP request body that can be captured when using HTTP
171+
/// server integrations. Needs `send_default_pii` to be enabled to have any effect.
172+
pub max_request_body_size: MaxRequestBodySize,
173+
/// Determines whether captured structured logs should be sent to Sentry (defaults to false).
174+
#[cfg(feature = "UNSTABLE_logs")]
175+
pub enable_logs: bool,
165176
// Other options not documented in Unified API
166177
/// Disable SSL verification.
167178
///
@@ -186,8 +197,6 @@ pub struct ClientOptions {
186197
pub trim_backtraces: bool,
187198
/// The user agent that should be reported.
188199
pub user_agent: Cow<'static, str>,
189-
/// Controls how much of request bodies are captured
190-
pub max_request_body_size: MaxRequestBodySize,
191200
}
192201

193202
impl ClientOptions {
@@ -223,6 +232,12 @@ impl fmt::Debug for ClientOptions {
223232
#[derive(Debug)]
224233
struct BeforeBreadcrumb;
225234
let before_breadcrumb = self.before_breadcrumb.as_ref().map(|_| BeforeBreadcrumb);
235+
#[cfg(feature = "UNSTABLE_logs")]
236+
let before_send_log = {
237+
#[derive(Debug)]
238+
struct BeforeSendLog;
239+
self.before_send_log.as_ref().map(|_| BeforeSendLog)
240+
};
226241
#[derive(Debug)]
227242
struct TransportFactory;
228243

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

282+
#[cfg(feature = "UNSTABLE_logs")]
283+
debug_struct
284+
.field("enable_logs", &self.enable_logs)
285+
.field("before_send_log", &before_send_log);
286+
267287
debug_struct
268288
.field("extra_border_frames", &self.extra_border_frames)
269289
.field("trim_backtraces", &self.trim_backtraces)
@@ -305,6 +325,10 @@ impl Default for ClientOptions {
305325
trim_backtraces: true,
306326
user_agent: Cow::Borrowed(USER_AGENT),
307327
max_request_body_size: MaxRequestBodySize::Medium,
328+
#[cfg(feature = "UNSTABLE_logs")]
329+
enable_logs: false,
330+
#[cfg(feature = "UNSTABLE_logs")]
331+
before_send_log: None,
308332
}
309333
}
310334
}

sentry-core/src/hub.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

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

7-
use crate::protocol::{Event, Level, SessionStatus};
7+
use crate::protocol::{Event, Level, Log, LogAttribute, LogLevel, Map, SessionStatus};
88
use crate::types::Uuid;
99
use crate::{Integration, IntoBreadcrumbs, Scope, ScopeGuard};
1010

@@ -245,4 +245,14 @@ impl Hub {
245245
})
246246
}}
247247
}
248+
249+
/// Captures a structured log.
250+
#[cfg(feature = "UNSTABLE_logs")]
251+
pub fn capture_log(&self, log: Log) {
252+
with_client_impl! {{
253+
let top = self.inner.with(|stack| stack.top().clone());
254+
let Some(ref client) = top.client else { return };
255+
client.capture_log(log, &top.scope);
256+
}}
257+
}
248258
}

sentry-core/src/scope/noop.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::fmt;
22

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

@@ -110,6 +112,13 @@ impl Scope {
110112
minimal_unreachable!();
111113
}
112114

115+
/// Applies the contained scoped data to fill a log.
116+
#[cfg(feature = "UNSTABLE_logs")]
117+
pub fn apply_to_log(&self, log: &mut Log) {
118+
let _log = log;
119+
minimal_unreachable!();
120+
}
121+
113122
/// Set the given [`TransactionOrSpan`] as the active span for this scope.
114123
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
115124
let _ = span;

sentry-core/src/scope/real.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ use std::fmt;
55
use std::sync::Mutex;
66
use std::sync::{Arc, PoisonError, RwLock};
77

8-
use sentry_types::protocol::v7::TraceContext;
9-
108
use crate::performance::TransactionOrSpan;
11-
use crate::protocol::{Attachment, Breadcrumb, Context, Event, Level, Transaction, User, Value};
9+
use crate::protocol::{
10+
Attachment, Breadcrumb, Context, Event, Level, TraceContext, Transaction, User, Value,
11+
};
12+
#[cfg(feature = "UNSTABLE_logs")]
13+
use crate::protocol::{Log, LogAttribute};
1214
#[cfg(feature = "release-health")]
1315
use crate::session::Session;
1416
use crate::{Client, SentryTrace, TraceHeader, TraceHeadersIter};
@@ -346,6 +348,59 @@ impl Scope {
346348
);
347349
}
348350

351+
/// Applies the contained scoped data to a log, setting the `trace_id` and certain default
352+
/// attributes.
353+
#[cfg(feature = "UNSTABLE_logs")]
354+
pub fn apply_to_log(&self, log: &mut Log, send_default_pii: bool) {
355+
if let Some(span) = self.span.as_ref() {
356+
log.trace_id = Some(span.get_trace_context().trace_id);
357+
} else {
358+
log.trace_id = Some(self.propagation_context.trace_id);
359+
}
360+
361+
if !log.attributes.contains_key("sentry.trace.parent_span_id") {
362+
if let Some(span) = self.get_span() {
363+
let span_id = match span {
364+
crate::TransactionOrSpan::Transaction(transaction) => {
365+
transaction.get_trace_context().span_id
366+
}
367+
crate::TransactionOrSpan::Span(span) => span.get_span_id(),
368+
};
369+
log.attributes.insert(
370+
"parent_span_id".to_owned(),
371+
LogAttribute(span_id.to_string().into()),
372+
);
373+
}
374+
}
375+
376+
if send_default_pii {
377+
if let Some(user) = self.user.as_ref() {
378+
if !log.attributes.contains_key("user.id") {
379+
if let Some(id) = user.id.as_ref() {
380+
log.attributes
381+
.insert("user.id".to_owned(), LogAttribute(id.to_owned().into()));
382+
}
383+
}
384+
385+
if !log.attributes.contains_key("user.name") {
386+
if let Some(name) = user.username.as_ref() {
387+
log.attributes
388+
.insert("user.name".to_owned(), LogAttribute(name.to_owned().into()));
389+
}
390+
}
391+
392+
if !log.attributes.contains_key("user.email") {
393+
if let Some(email) = user.email.as_ref() {
394+
log.attributes.insert(
395+
"user.email".to_owned(),
396+
LogAttribute(email.to_owned().into()),
397+
);
398+
}
399+
}
400+
}
401+
}
402+
}
403+
349404
/// Set the given [`TransactionOrSpan`] as the active span for this scope.
350405
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
351406
self.span = Arc::new(span);

sentry-types/src/protocol/envelope.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,15 +1071,15 @@ some content
10711071
Log {
10721072
level: protocol::LogLevel::Warn,
10731073
body: "test".to_owned(),
1074-
trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1074+
trace_id: Some("335e53d614474acc9f89e632b776cc28".parse().unwrap()),
10751075
timestamp: timestamp("2022-07-25T14:51:14.296Z"),
10761076
severity_number: Some(1.try_into().unwrap()),
10771077
attributes,
10781078
},
10791079
Log {
10801080
level: protocol::LogLevel::Error,
10811081
body: "a body".to_owned(),
1082-
trace_id: "332253d614472a2c9f89e232b7762c28".parse().unwrap(),
1082+
trace_id: Some("332253d614472a2c9f89e232b7762c28".parse().unwrap()),
10831083
timestamp: timestamp("2021-07-21T14:51:14.296Z"),
10841084
severity_number: Some(1.try_into().unwrap()),
10851085
attributes: attributes_2,

sentry-types/src/protocol/v7.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2127,7 +2127,8 @@ pub struct Log {
21272127
/// The log body/message (required).
21282128
pub body: String,
21292129
/// The ID of the Trace in which this log happened (required).
2130-
pub trace_id: TraceId,
2130+
#[serde(default, skip_serializing_if = "Option::is_none")]
2131+
pub trace_id: Option<TraceId>,
21312132
/// The timestamp of the log (required).
21322133
#[serde(with = "ts_seconds_float")]
21332134
pub timestamp: SystemTime,

sentry/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ opentelemetry = ["sentry-opentelemetry"]
4848
# other features
4949
test = ["sentry-core/test"]
5050
release-health = ["sentry-core/release-health", "sentry-actix?/release-health"]
51+
UNSTABLE_logs = ["sentry-core/UNSTABLE_logs"]
5152
# transports
5253
transport = ["reqwest", "native-tls"]
5354
reqwest = ["dep:reqwest", "httpdate", "tokio"]

0 commit comments

Comments
 (0)