Skip to content

Commit 5c8ab31

Browse files
authored
ref(tracing): rework tracing to Sentry span name/op conversion (#887)
1 parent 045c2e2 commit 5c8ab31

File tree

6 files changed

+257
-27
lines changed

6 files changed

+257
-27
lines changed

CHANGELOG.md

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

55
### Breaking changes
66

7-
- fix(actix): capture only server errors ([#877](https://github.com/getsentry/sentry-rust/pull/877))
7+
- ref(tracing): rework tracing to Sentry span name/op conversion ([#887](https://github.com/getsentry/sentry-rust/pull/887)) by @lcian
8+
- The `tracing` integration now uses the tracing span name as the Sentry span name by default.
9+
- Before this change, the span name would be set based on the `tracing` span target (<module>::<function> when using the `tracing::instrument` macro).
10+
- The `tracing` integration now uses `default` as the default Sentry span op.
11+
- Before this change, the span op would be set based on the `tracing` span name.
12+
- When upgrading, please ensure to adapt any queries, metrics or dashboards to use the new span names/ops.
13+
- fix(actix): capture only server errors ([#877](https://github.com/getsentry/sentry-rust/pull/877)) by @lcian
814
- The Actix integration now properly honors the `capture_server_errors` option (enabled by default), capturing errors returned by middleware only if they are server errors (HTTP status code 5xx).
915
- Previously, if a middleware were to process the request after the Sentry middleware and return an error, our middleware would always capture it and send it to Sentry, regardless if it was a client, server or some other kind of error.
1016
- With this change, we capture errors returned by middleware only if those errors can be classified as server errors.
@@ -17,6 +23,27 @@
1723

1824
### Features
1925

26+
- ref(tracing): rework tracing to Sentry span name/op conversion ([#887](https://github.com/getsentry/sentry-rust/pull/887)) by @lcian
27+
- Additional special fields have been added that allow overriding certain data on the Sentry span:
28+
- `sentry.op`: override the Sentry span op.
29+
- `sentry.name`: override the Sentry span name.
30+
- `sentry.trace`: given a string matching a valid `sentry-trace` header (sent automatically by client SDKs), continues the distributed trace instead of starting a new one. If the value is not a valid `sentry-trace` header or a trace is already started, this value is ignored.
31+
- `sentry.op` and `sentry.name` can also be applied retroactively by declaring fields with value `tracing::field::Empty` and then recorded using `tracing::Span::record`.
32+
- Example usage:
33+
```rust
34+
#[tracing::instrument(skip_all, fields(
35+
sentry.op = "http.server",
36+
sentry.name = "GET /payments",
37+
sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()),
38+
))]
39+
async fn handle_request(headers: std::collections::HashMap<String, String>) {
40+
// ...
41+
}
42+
```
43+
- Additional attributes are sent along with each span by default:
44+
- `sentry.tracing.target`: corresponds to the `tracing` span's `metadata.target()`
45+
- `code.module.name`, `code.file.path`, `code.line.number`
46+
2047
- feat(core): add Response context ([#874](https://github.com/getsentry/sentry-rust/pull/874)) by @lcian
2148
- The `Response` context can now be attached to events, to include information about HTTP responses such as headers, cookies and status code.
2249
- Example:

sentry-core/src/performance.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,22 @@ impl TransactionOrSpan {
465465
}
466466
}
467467

468+
/// Set the operation for this Transaction/Span.
469+
pub fn set_op(&self, op: &str) {
470+
match self {
471+
TransactionOrSpan::Transaction(transaction) => transaction.set_op(op),
472+
TransactionOrSpan::Span(span) => span.set_op(op),
473+
}
474+
}
475+
476+
/// Set the name (description) for this Transaction/Span.
477+
pub fn set_name(&self, name: &str) {
478+
match self {
479+
TransactionOrSpan::Transaction(transaction) => transaction.set_name(name),
480+
TransactionOrSpan::Span(span) => span.set_name(name),
481+
}
482+
}
483+
468484
/// Set the HTTP request information for this Transaction/Span.
469485
pub fn set_request(&self, request: protocol::Request) {
470486
match self {
@@ -781,6 +797,20 @@ impl Transaction {
781797
inner.context.status = Some(status);
782798
}
783799

800+
/// Set the operation of the Transaction.
801+
pub fn set_op(&self, op: &str) {
802+
let mut inner = self.inner.lock().unwrap();
803+
inner.context.op = Some(op.to_string());
804+
}
805+
806+
/// Set the name of the Transaction.
807+
pub fn set_name(&self, name: &str) {
808+
let mut inner = self.inner.lock().unwrap();
809+
if let Some(transaction) = inner.transaction.as_mut() {
810+
transaction.name = Some(name.to_string());
811+
}
812+
}
813+
784814
/// Set the HTTP request information for this Transaction.
785815
pub fn set_request(&self, request: protocol::Request) {
786816
let mut inner = self.inner.lock().unwrap();
@@ -1018,6 +1048,18 @@ impl Span {
10181048
span.status = Some(status);
10191049
}
10201050

1051+
/// Set the operation of the Span.
1052+
pub fn set_op(&self, op: &str) {
1053+
let mut span = self.span.lock().unwrap();
1054+
span.op = Some(op.to_string());
1055+
}
1056+
1057+
/// Set the name (description) of the Span.
1058+
pub fn set_name(&self, name: &str) {
1059+
let mut span = self.span.lock().unwrap();
1060+
span.description = Some(name.to_string());
1061+
}
1062+
10211063
/// Set the HTTP request information for this Span.
10221064
pub fn set_request(&self, request: protocol::Request) {
10231065
let mut span = self.span.lock().unwrap();

sentry-tracing/src/layer.rs

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ use tracing_subscriber::layer::{Context, Layer};
1212
use tracing_subscriber::registry::LookupSpan;
1313

1414
use crate::converters::*;
15+
use crate::SENTRY_NAME_FIELD;
16+
use crate::SENTRY_OP_FIELD;
17+
use crate::SENTRY_TRACE_FIELD;
1518
use crate::TAGS_PREFIX;
1619

1720
bitflags! {
@@ -298,27 +301,26 @@ where
298301
return;
299302
}
300303

301-
let (description, data) = extract_span_data(attrs);
302-
let op = span.name();
303-
304-
// Spans don't always have a description, this ensures our data is not empty,
305-
// therefore the Sentry UI will be a lot more valuable for navigating spans.
306-
let description = description.unwrap_or_else(|| {
307-
let target = span.metadata().target();
308-
if target.is_empty() {
309-
op.to_string()
310-
} else {
311-
format!("{target}::{op}")
312-
}
313-
});
304+
let (data, sentry_name, sentry_op, sentry_trace) = extract_span_data(attrs);
305+
let sentry_name = sentry_name.as_deref().unwrap_or_else(|| span.name());
306+
let sentry_op = sentry_op.as_deref().unwrap_or("default");
314307

315308
let hub = sentry_core::Hub::current();
316309
let parent_sentry_span = hub.configure_scope(|scope| scope.get_span());
317310

318-
let sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span {
319-
Some(parent) => parent.start_child(op, &description).into(),
311+
let mut sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span {
312+
Some(parent) => parent.start_child(sentry_op, sentry_name).into(),
320313
None => {
321-
let ctx = sentry_core::TransactionContext::new(&description, op);
314+
let ctx = if let Some(trace_header) = sentry_trace {
315+
sentry_core::TransactionContext::continue_from_headers(
316+
sentry_name,
317+
sentry_op,
318+
[("sentry-trace", trace_header.as_str())],
319+
)
320+
} else {
321+
sentry_core::TransactionContext::new(sentry_name, sentry_op)
322+
};
323+
322324
let tx = sentry_core::start_transaction(ctx);
323325
tx.set_data("origin", "auto.tracing".into());
324326
tx.into()
@@ -328,6 +330,8 @@ where
328330
// This comes from typically the `fields` in `tracing::instrument`.
329331
record_fields(&sentry_span, data);
330332

333+
set_default_attributes(&mut sentry_span, span.metadata());
334+
331335
let mut extensions = span.extensions_mut();
332336
extensions.insert(SentrySpanData {
333337
sentry_span,
@@ -403,10 +407,52 @@ where
403407
let mut data = FieldVisitor::default();
404408
values.record(&mut data);
405409

410+
let sentry_name = data
411+
.json_values
412+
.remove(SENTRY_NAME_FIELD)
413+
.and_then(|v| match v {
414+
Value::String(s) => Some(s),
415+
_ => None,
416+
});
417+
418+
let sentry_op = data
419+
.json_values
420+
.remove(SENTRY_OP_FIELD)
421+
.and_then(|v| match v {
422+
Value::String(s) => Some(s),
423+
_ => None,
424+
});
425+
426+
// `sentry.trace` cannot be applied retroactively
427+
data.json_values.remove(SENTRY_TRACE_FIELD);
428+
429+
if let Some(name) = sentry_name {
430+
span.set_name(&name);
431+
}
432+
if let Some(op) = sentry_op {
433+
span.set_op(&op);
434+
}
435+
406436
record_fields(span, data.json_values);
407437
}
408438
}
409439

440+
fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) {
441+
span.set_data("sentry.tracing.target", metadata.target().into());
442+
443+
if let Some(module) = metadata.module_path() {
444+
span.set_data("code.module.name", module.into());
445+
}
446+
447+
if let Some(file) = metadata.file() {
448+
span.set_data("code.file.path", file.into());
449+
}
450+
451+
if let Some(line) = metadata.line() {
452+
span.set_data("code.line.number", line.into());
453+
}
454+
}
455+
410456
/// Creates a default Sentry layer
411457
pub fn layer<S>() -> SentryLayer<S>
412458
where
@@ -415,8 +461,16 @@ where
415461
Default::default()
416462
}
417463

418-
/// Extracts the message and attributes from a span
419-
fn extract_span_data(attrs: &span::Attributes) -> (Option<String>, BTreeMap<&'static str, Value>) {
464+
/// Extracts the attributes from a span,
465+
/// returning the values of SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD separately
466+
fn extract_span_data(
467+
attrs: &span::Attributes,
468+
) -> (
469+
BTreeMap<&'static str, Value>,
470+
Option<String>,
471+
Option<String>,
472+
Option<String>,
473+
) {
420474
let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| {
421475
let mut visitor = SpanFieldVisitor {
422476
debug_buffer,
@@ -426,13 +480,24 @@ fn extract_span_data(attrs: &span::Attributes) -> (Option<String>, BTreeMap<&'st
426480
visitor.json_values
427481
});
428482

429-
// Find message of the span, if any
430-
let message = json_values.remove("message").and_then(|v| match v {
483+
let name = json_values.remove(SENTRY_NAME_FIELD).and_then(|v| match v {
431484
Value::String(s) => Some(s),
432485
_ => None,
433486
});
434487

435-
(message, json_values)
488+
let op = json_values.remove(SENTRY_OP_FIELD).and_then(|v| match v {
489+
Value::String(s) => Some(s),
490+
_ => None,
491+
});
492+
493+
let sentry_trace = json_values
494+
.remove(SENTRY_TRACE_FIELD)
495+
.and_then(|v| match v {
496+
Value::String(s) => Some(s),
497+
_ => None,
498+
});
499+
500+
(json_values, name, op, sentry_trace)
436501
}
437502

438503
thread_local! {

sentry-tracing/src/lib.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@
169169
//! # Tracing Spans
170170
//!
171171
//! The integration automatically tracks `tracing` spans as spans in Sentry. A convenient way to do
172-
//! this is with the `#[instrument]` attribute macro, which creates a transaction for the function
172+
//! this is with the `#[instrument]` attribute macro, which creates a span/transaction for the function
173173
//! in Sentry.
174174
//!
175175
//! Function arguments are added as context fields automatically, which can be configured through
@@ -180,8 +180,8 @@
180180
//!
181181
//! use tracing_subscriber::prelude::*;
182182
//!
183-
//! // Functions instrumented by tracing automatically report
184-
//! // their span as transactions.
183+
//! // Functions instrumented by tracing automatically
184+
//! // create spans/transactions around their execution.
185185
//! #[tracing::instrument]
186186
//! async fn outer() {
187187
//! for i in 0..10 {
@@ -198,6 +198,42 @@
198198
//! tokio::time::sleep(Duration::from_millis(100)).await;
199199
//! }
200200
//! ```
201+
//!
202+
//! By default, the name of the span sent to Sentry matches the name of the `tracing` span, which
203+
//! is the name of the function when using `tracing::instrument`, or the name passed to the
204+
//! `tracing::<level>_span` macros.
205+
//!
206+
//! By default, the `op` of the span sent to Sentry is `default`.
207+
//!
208+
//! ## Special Span Fields
209+
//!
210+
//! Some fields on spans are treated specially by the Sentry tracing integration:
211+
//! - `sentry.name`: overrides the span name sent to Sentry.
212+
//! This is useful to customize the span name when using `#[tracing::instrument]`, or to update
213+
//! it retroactively (using `span.record`) after the span has been created.
214+
//! - `sentry.op`: overrides the span `op` sent to Sentry.
215+
//! - `sentry.trace`: in Sentry, the `sentry-trace` header is sent with HTTP requests to achieve distributed tracing.
216+
//! If the value of this field is set to the value of a valid `sentry-trace` header, which
217+
//! other Sentry SDKs send automatically with outgoing requests, then the SDK will continue the trace using the given distributed tracing information.
218+
//! This is useful to achieve distributed tracing at service boundaries by using only the
219+
//! `tracing` API.
220+
//! Note that `sentry.trace` will only be effective on span creation (it cannot be applied retroactively)
221+
//! and requires the span it's applied to to be a root span, i.e. no span should active upon its
222+
//! creation.
223+
//!
224+
//!
225+
//! Example:
226+
//!
227+
//! ```
228+
//! #[tracing::instrument(skip_all, fields(
229+
//! sentry.name = "GET /payments",
230+
//! sentry.op = "http.server",
231+
//! sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()),
232+
//! ))]
233+
//! async fn handle_request(headers: std::collections::HashMap<String, String>) {
234+
//! // ...
235+
//! }
236+
//! ```
201237
202238
#![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")]
203239
#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]
@@ -210,3 +246,6 @@ pub use converters::*;
210246
pub use layer::*;
211247

212248
const TAGS_PREFIX: &str = "tags.";
249+
const SENTRY_OP_FIELD: &str = "sentry.op";
250+
const SENTRY_NAME_FIELD: &str = "sentry.name";
251+
const SENTRY_TRACE_FIELD: &str = "sentry.trace";
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
mod shared;
2+
3+
#[tracing::instrument(fields(
4+
some = "value",
5+
sentry.name = "updated name",
6+
sentry.op = "updated op",
7+
))]
8+
fn test_fun_record_on_creation() {}
9+
10+
#[tracing::instrument(fields(
11+
some = "value",
12+
sentry.name = tracing::field::Empty,
13+
sentry.op = tracing::field::Empty,
14+
))]
15+
fn test_fun_record_later() {
16+
tracing::Span::current().record("sentry.name", "updated name");
17+
tracing::Span::current().record("sentry.op", "updated op");
18+
}
19+
20+
#[test]
21+
fn should_update_sentry_op_and_name_based_on_fields() {
22+
let transport = shared::init_sentry(1.0);
23+
24+
for f in [test_fun_record_on_creation, test_fun_record_later] {
25+
f();
26+
27+
let data = transport.fetch_and_clear_envelopes();
28+
assert_eq!(data.len(), 1);
29+
30+
let transaction = data.first().expect("should have 1 transaction");
31+
let transaction = match transaction.items().next().unwrap() {
32+
sentry::protocol::EnvelopeItem::Transaction(transaction) => transaction,
33+
unexpected => panic!("Expected transaction, but got {unexpected:#?}"),
34+
};
35+
36+
assert_eq!(transaction.name.as_deref().unwrap(), "updated name");
37+
let ctx = transaction.contexts.get("trace");
38+
match ctx {
39+
Some(sentry::protocol::Context::Trace(trace_ctx)) => {
40+
assert_eq!(trace_ctx.op, Some("updated op".to_owned()))
41+
}
42+
_ => panic!("expected trace context"),
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)