From b3490ffc769eafea7af200ea791bca92a7847b78 Mon Sep 17 00:00:00 2001 From: Aymerick Valette Date: Tue, 13 Apr 2021 19:31:17 +0200 Subject: [PATCH] subscriber: add span status fields (#1351) ## Motivation Allow users to set custom span status codes and messages to follow opentelemetry semantic conventions. ## Solution Support the status code and status message fields in `OpenTelemetrySubscriber` Proposal to close #1346 --- tracing-opentelemetry/src/layer.rs | 117 ++++++++++++++++++++-------- tracing-opentelemetry/src/lib.rs | 3 + tracing-opentelemetry/src/tracer.rs | 2 +- 3 files changed, 87 insertions(+), 35 deletions(-) diff --git a/tracing-opentelemetry/src/layer.rs b/tracing-opentelemetry/src/layer.rs index 0527d98e..4dabe73f 100644 --- a/tracing-opentelemetry/src/layer.rs +++ b/tracing-opentelemetry/src/layer.rs @@ -12,8 +12,10 @@ use tracing_subscriber::layer::Context; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::Layer; -static SPAN_NAME_FIELD: &str = "otel.name"; -static SPAN_KIND_FIELD: &str = "otel.kind"; +const SPAN_NAME_FIELD: &str = "otel.name"; +const SPAN_KIND_FIELD: &str = "otel.kind"; +const SPAN_STATUS_CODE_FIELD: &str = "otel.status_code"; +const SPAN_STATUS_MESSAGE_FIELD: &str = "otel.status_message"; /// An [OpenTelemetry] propagation layer for use in a project that uses /// [tracing]. @@ -85,18 +87,22 @@ impl WithContext { } fn str_to_span_kind(s: &str) -> Option { - if s.eq_ignore_ascii_case("SERVER") { - Some(otel::SpanKind::Server) - } else if s.eq_ignore_ascii_case("CLIENT") { - Some(otel::SpanKind::Client) - } else if s.eq_ignore_ascii_case("PRODUCER") { - Some(otel::SpanKind::Producer) - } else if s.eq_ignore_ascii_case("CONSUMER") { - Some(otel::SpanKind::Consumer) - } else if s.eq_ignore_ascii_case("INTERNAL") { - Some(otel::SpanKind::Internal) - } else { - None + match s { + s if s.eq_ignore_ascii_case("server") => Some(otel::SpanKind::Server), + s if s.eq_ignore_ascii_case("client") => Some(otel::SpanKind::Client), + s if s.eq_ignore_ascii_case("producer") => Some(otel::SpanKind::Producer), + s if s.eq_ignore_ascii_case("consumer") => Some(otel::SpanKind::Consumer), + s if s.eq_ignore_ascii_case("internal") => Some(otel::SpanKind::Internal), + _ => None, + } +} + +fn str_to_status_code(s: &str) -> Option { + match s { + s if s.eq_ignore_ascii_case("unset") => Some(otel::StatusCode::Unset), + s if s.eq_ignore_ascii_case("ok") => Some(otel::StatusCode::Ok), + s if s.eq_ignore_ascii_case("error") => Some(otel::StatusCode::Error), + _ => None, } } @@ -200,16 +206,18 @@ impl<'a> field::Visit for SpanAttributeVisitor<'a> { /// /// [`Span`]: opentelemetry::trace::Span fn record_str(&mut self, field: &field::Field, value: &str) { - if field.name() == SPAN_NAME_FIELD { - self.0.name = value.to_string().into(); - } else if field.name() == SPAN_KIND_FIELD { - self.0.span_kind = str_to_span_kind(value); - } else { - let attribute = KeyValue::new(field.name(), value.to_string()); - if let Some(attributes) = &mut self.0.attributes { - attributes.push(attribute); - } else { - self.0.attributes = Some(vec![attribute]); + match field.name() { + SPAN_NAME_FIELD => self.0.name = value.to_string().into(), + SPAN_KIND_FIELD => self.0.span_kind = str_to_span_kind(value), + SPAN_STATUS_CODE_FIELD => self.0.status_code = str_to_status_code(value), + SPAN_STATUS_MESSAGE_FIELD => self.0.status_message = Some(value.to_owned()), + _ => { + let attribute = KeyValue::new(field.name(), value.to_string()); + if let Some(attributes) = &mut self.0.attributes { + attributes.push(attribute); + } else { + self.0.attributes = Some(vec![attribute]); + } } } } @@ -219,16 +227,20 @@ impl<'a> field::Visit for SpanAttributeVisitor<'a> { /// /// [`Span`]: opentelemetry::trace::Span fn record_debug(&mut self, field: &field::Field, value: &dyn fmt::Debug) { - if field.name() == SPAN_NAME_FIELD { - self.0.name = format!("{:?}", value).into(); - } else if field.name() == SPAN_KIND_FIELD { - self.0.span_kind = str_to_span_kind(&format!("{:?}", value)); - } else { - let attribute = Key::new(field.name()).string(format!("{:?}", value)); - if let Some(attributes) = &mut self.0.attributes { - attributes.push(attribute); - } else { - self.0.attributes = Some(vec![attribute]); + match field.name() { + SPAN_NAME_FIELD => self.0.name = format!("{:?}", value).into(), + SPAN_KIND_FIELD => self.0.span_kind = str_to_span_kind(&format!("{:?}", value)), + SPAN_STATUS_CODE_FIELD => { + self.0.status_code = str_to_status_code(&format!("{:?}", value)) + } + SPAN_STATUS_MESSAGE_FIELD => self.0.status_message = Some(format!("{:?}", value)), + _ => { + let attribute = Key::new(field.name()).string(format!("{:?}", value)); + if let Some(attributes) = &mut self.0.attributes { + attributes.push(attribute); + } else { + self.0.attributes = Some(vec![attribute]); + } } } } @@ -676,6 +688,43 @@ mod tests { assert_eq!(recorded_kind, Some(otel::SpanKind::Server)) } + #[test] + fn span_status_code() { + let tracer = TestTracer(Arc::new(Mutex::new(None))); + let subscriber = + tracing_subscriber::registry().with(subscriber().with_tracer(tracer.clone())); + + tracing::collect::with_default(subscriber, || { + tracing::debug_span!("request", otel.status_code = ?otel::StatusCode::Ok); + }); + let recorded_status_code = tracer.0.lock().unwrap().as_ref().unwrap().status_code; + assert_eq!(recorded_status_code, Some(otel::StatusCode::Ok)) + } + + #[test] + fn span_status_message() { + let tracer = TestTracer(Arc::new(Mutex::new(None))); + let subscriber = + tracing_subscriber::registry().with(subscriber().with_tracer(tracer.clone())); + + let message = "message"; + + tracing::collect::with_default(subscriber, || { + tracing::debug_span!("request", otel.status_message = message); + }); + + let recorded_status_message = tracer + .0 + .lock() + .unwrap() + .as_ref() + .unwrap() + .status_message + .clone(); + + assert_eq!(recorded_status_message, Some(message.to_string())) + } + #[test] fn trace_id_from_existing_context() { let tracer = TestTracer(Arc::new(Mutex::new(None))); diff --git a/tracing-opentelemetry/src/lib.rs b/tracing-opentelemetry/src/lib.rs index eb155fa1..bb6bb669 100644 --- a/tracing-opentelemetry/src/lib.rs +++ b/tracing-opentelemetry/src/lib.rs @@ -23,8 +23,11 @@ //! Setting this field is useful if you want to display non-static information //! in your span name. //! * `otel.kind`: Set the span kind to one of the supported OpenTelemetry [span kinds]. +//! * `otel.status_code`: Set the span status code to one of the supported OpenTelemetry [span status codes]. +//! * `otel.status_message`: Set the span status message. //! //! [span kinds]: https://docs.rs/opentelemetry/latest/opentelemetry/trace/enum.SpanKind.html +//! [span status codes]: https://docs.rs/opentelemetry/latest/opentelemetry/trace/enum.StatusCode.html //! //! ### Semantic Conventions //! diff --git a/tracing-opentelemetry/src/tracer.rs b/tracing-opentelemetry/src/tracer.rs index 7d902dfe..de4b505b 100644 --- a/tracing-opentelemetry/src/tracer.rs +++ b/tracing-opentelemetry/src/tracer.rs @@ -224,7 +224,7 @@ impl otel::Span for CompatSpan { fn set_status(&self, _code: otel::StatusCode, _message: String) { #[cfg(debug_assertions)] - panic!("OpenTelemetry and tracing APIs cannot be mixed, use `tracing::span!` macro or `span.record()` instead."); + panic!("OpenTelemetry and tracing APIs cannot be mixed, use `tracing::span!` macro or `span.record()` with `otel.status_code` and `otel.status_message` instead."); } fn update_name(&self, _new_name: String) {