diff --git a/.gitignore b/.gitignore index 0b513cc74..43d2239ea 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ Gemfile.lock .ruby-gemset .idea *.rdb +.yardoc/ examples/**/node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eaff1a37..ad5bcd65c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ config.enabled_patches += [:graphql] end ``` +- Add [W3C traceparent header](https://www.w3.org/TR/trace-context/#traceparent-header) support ([#2310](https://github.com/getsentry/sentry-ruby/pull/2310)) + + The SDK now also propagates and accepts incoming W3C `traceparent` headers along with the currently implemented `sentry-trace` and `baggage` headers. ### Bug Fixes diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index d412dec2a..f3a0a5c80 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -44,7 +44,7 @@ module Sentry LOGGER_PROGNAME = "sentry".freeze SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze - + W3C_TRACEPARENT_HEADER_NAME = "traceparent".freeze BAGGAGE_HEADER_NAME = "baggage".freeze THREAD_LOCAL = :sentry_hub @@ -533,6 +533,17 @@ def get_traceparent get_current_hub.get_traceparent end + # Returns the W3C traceparent header for distributed tracing. + # Can be either from the currently active span or the propagation context. + # + # @see https://www.w3.org/TR/trace-context/#traceparent-header W3C Traceparent specification + # + # @return [String, nil] + def get_w3c_traceparent + return nil unless initialized? + get_current_hub.get_w3c_traceparent + end + # Returns the baggage header for distributed tracing. # Can be either from the currently active span or the propagation context. # diff --git a/sentry-ruby/lib/sentry/hub.rb b/sentry-ruby/lib/sentry/hub.rb index 05d22152b..3e9067673 100644 --- a/sentry-ruby/lib/sentry/hub.rb +++ b/sentry-ruby/lib/sentry/hub.rb @@ -265,6 +265,13 @@ def get_traceparent current_scope.propagation_context.get_traceparent end + def get_w3c_traceparent + return nil unless current_scope + + current_scope.get_span&.get_w3c_traceparent || + current_scope.propagation_context.get_w3c_traceparent + end + def get_baggage return nil unless current_scope @@ -278,6 +285,9 @@ def get_trace_propagation_headers traceparent = get_traceparent headers[SENTRY_TRACE_HEADER_NAME] = traceparent if traceparent + w3c_traceparent = get_w3c_traceparent + headers[W3C_TRACEPARENT_HEADER_NAME] = w3c_traceparent if w3c_traceparent + baggage = get_baggage headers[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty? diff --git a/sentry-ruby/lib/sentry/propagation_context.rb b/sentry-ruby/lib/sentry/propagation_context.rb index 12ce55540..86ab01d7f 100644 --- a/sentry-ruby/lib/sentry/propagation_context.rb +++ b/sentry-ruby/lib/sentry/propagation_context.rb @@ -13,6 +13,17 @@ class PropagationContext "[ \t]*$" # whitespace ) + W3C_TRACEPARENT_REGEX = Regexp.new( + "^[ \t]*" + # whitespace + "([0-9a-f]{2})?" + # version + "-?([0-9a-f]{32})?" + # trace_id + "-?([0-9a-f]{16})?" + # parent_span_id + "-?([0-9a-f]{2})?" + # trace_flags + "[ \t]*$" # whitespace + ) + + W3C_TRACEPARENT_VERSION = '00' + # An uuid that can be used to identify a trace. # @return [String] attr_reader :trace_id @@ -42,6 +53,7 @@ def initialize(scope, env = nil) if env sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME] + w3c_traceparent_header = env["HTTP_TRACEPARENT"] || env[W3C_TRACEPARENT_HEADER_NAME] baggage_header = env["HTTP_BAGGAGE"] || env[BAGGAGE_HEADER_NAME] if sentry_trace_header @@ -49,21 +61,30 @@ def initialize(scope, env = nil) if sentry_trace_data @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data + @incoming_trace = true + end + elsif w3c_traceparent_header + traceparent_data = self.class.extract_w3c_traceparent(w3c_traceparent_header) - @baggage = - if baggage_header && !baggage_header.empty? - Baggage.from_incoming_header(baggage_header) - else - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and frozen and won't be populated as head SDK. - Baggage.new({}) - end - - @baggage.freeze! + if traceparent_data + @trace_id, @parent_span_id, @parent_sampled = traceparent_data @incoming_trace = true end end + + if @incoming_trace + @baggage = + if baggage_header && !baggage_header.empty? + Baggage.from_incoming_header(baggage_header) + else + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and frozen and won't be populated as head SDK. + Baggage.new({}) + end + + @baggage.freeze! + end end @trace_id ||= SecureRandom.uuid.delete("-") @@ -84,6 +105,20 @@ def self.extract_sentry_trace(sentry_trace) [trace_id, parent_span_id, parent_sampled] end + # Extract the trace_id, parent_span_id and parent_sampled values from a W3C traceparent header. + # + # @param traceparent [String] the traceparent header value from the previous transaction. + # @return [Array, nil] + def self.extract_w3c_traceparent(traceparent) + match = W3C_TRACEPARENT_REGEX.match(traceparent) + return nil if match.nil? + + trace_id, parent_span_id, trace_flags = match[2..4] + parent_sampled = (trace_flags.hex & 0x01) == 0x01 + + [version, trace_id, parent_span_id, parent_sampled] + end + # Returns the trace context that can be used to embed in an Event. # @return [Hash] def get_trace_context @@ -100,6 +135,12 @@ def get_traceparent "#{trace_id}-#{span_id}" end + # Returns the w3c traceparent header from the propagation context. + # @return [String] + def get_w3c_traceparent + "#{W3C_TRACEPARENT_VERSION}-#{trace_id}-#{span_id}-00" + end + # Returns the Baggage from the propagation context or populates as head SDK if empty. # @return [Baggage, nil] def get_baggage diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index 69374b496..825982123 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "securerandom" +require "sentry/propagation_context" require "sentry/metrics/local_aggregator" module Sentry @@ -141,6 +142,13 @@ def to_sentry_trace "#{@trace_id}-#{@span_id}-#{sampled_flag}" end + # Generates a w3c traceparent header that can be used to connect other transactions. + # @return [String] + def get_w3c_traceparent + trace_flags = @sampled ? '01' : '00' + "#{PropagationContext::W3C_TRACEPARENT_VERSION}-#{@trace_id}-#{@span_id}-#{trace_flags}" + end + # Generates a W3C Baggage header string for distributed tracing # from the incoming baggage stored on the transaction. # @return [String, nil] diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index c3f12777a..989e3e124 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -777,7 +777,8 @@ it "returns a Hash of sentry-trace and baggage" do expect(described_class.get_trace_propagation_headers).to eq({ "sentry-trace" => described_class.get_traceparent, - "baggage" => described_class.get_baggage + "baggage" => described_class.get_baggage, + "traceparent" => described_class.get_w3c_traceparent }) end end