1use std::collections::BTreeMap;
2use std::error::Error;
3
4use sentry_core::protocol::{Event, Exception, Mechanism, Value};
5#[cfg(feature = "logs")]
6use sentry_core::protocol::{Log, LogAttribute, LogLevel};
7use sentry_core::{event_from_error, Breadcrumb, Level, TransactionOrSpan};
8#[cfg(feature = "logs")]
9use std::time::SystemTime;
10use tracing_core::field::{Field, Visit};
11use tracing_core::Subscriber;
12use tracing_subscriber::layer::Context;
13use tracing_subscriber::registry::LookupSpan;
14
15use super::layer::SentrySpanData;
16use crate::TAGS_PREFIX;
17
18fn level_to_sentry_level(level: &tracing_core::Level) -> Level {
20 match *level {
21 tracing_core::Level::TRACE | tracing_core::Level::DEBUG => Level::Debug,
22 tracing_core::Level::INFO => Level::Info,
23 tracing_core::Level::WARN => Level::Warning,
24 tracing_core::Level::ERROR => Level::Error,
25 }
26}
27
28#[cfg(feature = "logs")]
30fn level_to_log_level(level: &tracing_core::Level) -> LogLevel {
31 match *level {
32 tracing_core::Level::TRACE => LogLevel::Trace,
33 tracing_core::Level::DEBUG => LogLevel::Debug,
34 tracing_core::Level::INFO => LogLevel::Info,
35 tracing_core::Level::WARN => LogLevel::Warn,
36 tracing_core::Level::ERROR => LogLevel::Error,
37 }
38}
39
40#[cfg(feature = "backtrace")]
42fn level_to_exception_type(level: &tracing_core::Level) -> &'static str {
43 match *level {
44 tracing_core::Level::TRACE => "tracing::trace!",
45 tracing_core::Level::DEBUG => "tracing::debug!",
46 tracing_core::Level::INFO => "tracing::info!",
47 tracing_core::Level::WARN => "tracing::warn!",
48 tracing_core::Level::ERROR => "tracing::error!",
49 }
50}
51
52fn extract_event_data(
54 event: &tracing_core::Event,
55 store_errors_in_values: bool,
56) -> (Option<String>, FieldVisitor) {
57 let mut visitor = FieldVisitor {
59 store_errors_in_values,
60 ..Default::default()
61 };
62 event.record(&mut visitor);
63 let message = visitor
64 .json_values
65 .remove("message")
66 .or_else(|| visitor.json_values.remove("error"))
69 .and_then(|v| match v {
70 Value::String(s) => Some(s),
71 _ => None,
72 });
73
74 (message, visitor)
75}
76
77fn extract_event_data_with_context<S>(
79 event: &tracing_core::Event,
80 ctx: Option<&Context<S>>,
81 store_errors_in_values: bool,
82) -> (Option<String>, FieldVisitor)
83where
84 S: Subscriber + for<'a> LookupSpan<'a>,
85{
86 let (message, mut visitor) = extract_event_data(event, store_errors_in_values);
87
88 let current_span = ctx.as_ref().and_then(|ctx| {
90 event
91 .parent()
92 .and_then(|id| ctx.span(id))
93 .or_else(|| ctx.lookup_current())
94 });
95 if let Some(span) = current_span {
96 for span in span.scope() {
97 let name = span.name();
98 let ext = span.extensions();
99
100 if let Some(span_data) = ext.get::<SentrySpanData>() {
101 match &span_data.sentry_span {
102 TransactionOrSpan::Span(span) => {
103 for (key, value) in span.data().iter() {
104 if is_sentry_span_attribute(key) {
105 continue;
106 }
107 if key != "message" {
108 let key = format!("{name}:{key}");
109 visitor.json_values.insert(key, value.clone());
110 }
111 }
112 }
113 TransactionOrSpan::Transaction(transaction) => {
114 for (key, value) in transaction.data().iter() {
115 if is_sentry_span_attribute(key) {
116 continue;
117 }
118 if key != "message" {
119 let key = format!("{name}:{key}");
120 visitor.json_values.insert(key, value.clone());
121 }
122 }
123 }
124 }
125 }
126 }
127 }
128
129 (message, visitor)
130}
131
132fn is_sentry_span_attribute(name: &str) -> bool {
135 matches!(
136 name,
137 "sentry.tracing.target" | "code.module.name" | "code.file.path" | "code.line.number"
138 )
139}
140
141#[derive(Default)]
143pub(crate) struct FieldVisitor {
144 pub(crate) json_values: BTreeMap<String, Value>,
145 pub(crate) exceptions: Vec<Exception>,
146 store_errors_in_values: bool,
149}
150
151impl FieldVisitor {
152 fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
153 self.json_values
154 .insert(field.name().to_owned(), value.into());
155 }
156}
157
158impl Visit for FieldVisitor {
159 fn record_i64(&mut self, field: &Field, value: i64) {
160 self.record(field, value);
161 }
162
163 fn record_u64(&mut self, field: &Field, value: u64) {
164 self.record(field, value);
165 }
166
167 fn record_bool(&mut self, field: &Field, value: bool) {
168 self.record(field, value);
169 }
170
171 fn record_f64(&mut self, field: &Field, value: f64) {
172 self.record(field, value);
173 }
174
175 fn record_str(&mut self, field: &Field, value: &str) {
176 self.record(field, value);
177 }
178
179 fn record_error(&mut self, field: &Field, value: &(dyn Error + 'static)) {
180 let event = event_from_error(value);
181 if self.store_errors_in_values {
182 let error_chain = event
183 .exception
184 .iter()
185 .rev()
186 .filter_map(|x| x.value.as_ref().map(|v| format!("{}: {}", x.ty, *v)))
187 .collect::<Vec<String>>();
188 self.record(field, error_chain);
189 } else {
190 for exception in event.exception {
191 self.exceptions.push(exception);
192 }
193 }
194 }
195
196 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
197 self.record(field, format!("{value:?}"));
198 }
199}
200
201#[expect(clippy::impl_trait_in_params, reason = "existed before lint enabled")]
203pub fn breadcrumb_from_event<'context, S>(
204 event: &tracing_core::Event,
205 ctx: impl Into<Option<&'context Context<'context, S>>>,
206) -> Breadcrumb
207where
208 S: Subscriber + for<'a> LookupSpan<'a>,
209{
210 let (message, visitor) = extract_event_data_with_context(event, ctx.into(), true);
211
212 Breadcrumb {
213 category: Some(event.metadata().target().to_owned()),
214 ty: "log".into(),
215 level: level_to_sentry_level(event.metadata().level()),
216 message,
217 data: visitor.json_values,
218 ..Default::default()
219 }
220}
221
222fn extract_and_remove_tags(fields: &mut BTreeMap<String, Value>) -> BTreeMap<String, String> {
224 let mut tags = BTreeMap::new();
225
226 fields.retain(|key, value| {
227 let Some(key) = key.strip_prefix(TAGS_PREFIX) else {
228 return true;
229 };
230 let string = match value {
231 Value::Bool(b) => b.to_string(),
232 Value::Number(n) => n.to_string(),
233 Value::String(s) => std::mem::take(s),
234 Value::Null => return false,
236 Value::Array(_) | Value::Object(_) => return true,
238 };
239
240 tags.insert(key.to_owned(), string);
241
242 false
243 });
244
245 tags
246}
247
248fn contexts_from_event(
250 event: &tracing_core::Event,
251 fields: BTreeMap<String, Value>,
252) -> BTreeMap<String, sentry_core::protocol::Context> {
253 let event_meta = event.metadata();
254 let mut location_map = BTreeMap::new();
255 if let Some(module_path) = event_meta.module_path() {
256 location_map.insert("module_path".to_string(), module_path.into());
257 }
258 if let Some(file) = event_meta.file() {
259 location_map.insert("file".to_string(), file.into());
260 }
261 if let Some(line) = event_meta.line() {
262 location_map.insert("line".to_string(), line.into());
263 }
264
265 let mut context = BTreeMap::new();
266 if !fields.is_empty() {
267 context.insert(
268 "Rust Tracing Fields".to_string(),
269 sentry_core::protocol::Context::Other(fields),
270 );
271 }
272 if !location_map.is_empty() {
273 context.insert(
274 "Rust Tracing Location".to_string(),
275 sentry_core::protocol::Context::Other(location_map),
276 );
277 }
278 context
279}
280
281#[expect(clippy::impl_trait_in_params, reason = "existed before lint enabled")]
283pub fn event_from_event<'context, S>(
284 event: &tracing_core::Event,
285 ctx: impl Into<Option<&'context Context<'context, S>>>,
286) -> Event<'static>
287where
288 S: Subscriber + for<'a> LookupSpan<'a>,
289{
290 let (message, visitor) = extract_event_data_with_context(event, ctx.into(), false);
295 let FieldVisitor {
296 mut exceptions,
297 mut json_values,
298 store_errors_in_values: _,
299 } = visitor;
300
301 #[cfg(feature = "backtrace")]
307 let message = {
308 let mut message = message;
309 if !exceptions.is_empty() && message.is_some() {
310 if let Some(client) = sentry_core::Hub::current().client() {
311 if client.options().attach_stacktrace {
312 let thread = sentry_backtrace::current_thread(true);
313 let exception = Exception {
314 ty: level_to_exception_type(event.metadata().level()).to_owned(),
315 value: message.take(),
316 module: event.metadata().module_path().map(str::to_owned),
317 stacktrace: thread.stacktrace,
318 raw_stacktrace: thread.raw_stacktrace,
319 thread_id: thread.id,
320 mechanism: Some(Mechanism {
321 synthetic: Some(true),
322 ..Mechanism::default()
323 }),
324 };
325 exceptions.push(exception)
326 }
327 }
328 }
329 message
330 };
331
332 if let Some(exception) = exceptions.last_mut() {
333 "tracing".clone_into(
334 &mut exception
335 .mechanism
336 .get_or_insert_with(Mechanism::default)
337 .ty,
338 );
339 }
340
341 Event {
342 logger: Some(event.metadata().target().to_owned()),
343 level: level_to_sentry_level(event.metadata().level()),
344 message,
345 exception: exceptions.into(),
346 tags: extract_and_remove_tags(&mut json_values),
347 contexts: contexts_from_event(event, json_values),
348 ..Default::default()
349 }
350}
351
352#[cfg(feature = "logs")]
354#[expect(clippy::impl_trait_in_params, reason = "existed before lint enabled")]
355pub fn log_from_event<'context, S>(
356 event: &tracing_core::Event,
357 ctx: impl Into<Option<&'context Context<'context, S>>>,
358) -> Log
359where
360 S: Subscriber + for<'a> LookupSpan<'a>,
361{
362 let (message, visitor) = extract_event_data_with_context(event, ctx.into(), true);
363
364 let mut attributes: BTreeMap<String, LogAttribute> = visitor
365 .json_values
366 .into_iter()
367 .map(|(key, val)| (key, val.into()))
368 .collect();
369
370 let event_meta = event.metadata();
371 if let Some(module_path) = event_meta.module_path() {
372 attributes.insert("code.module.name".to_owned(), module_path.into());
373 }
374 if let Some(file) = event_meta.file() {
375 attributes.insert("code.file.path".to_owned(), file.into());
376 }
377 if let Some(line) = event_meta.line() {
378 attributes.insert("code.line.number".to_owned(), line.into());
379 }
380
381 attributes.insert("sentry.origin".to_owned(), "auto.tracing".into());
382
383 Log {
384 level: level_to_log_level(event.metadata().level()),
385 body: message.unwrap_or_default(),
386 trace_id: None,
387 timestamp: SystemTime::now(),
388 severity_number: None,
389 attributes,
390 }
391}