From a4d5eb245328530e1e439930b73afff2628cc365 Mon Sep 17 00:00:00 2001 From: ZStriker19 Date: Tue, 11 Feb 2025 17:37:30 -0500 Subject: [PATCH] initial implementation --- lib/datadog/tracing/configuration/settings.rb | 4 +- lib/datadog/tracing/distributed/baggage.rb | 109 ++++++++++++++++++ .../tracing/distributed/propagation.rb | 17 +++ lib/datadog/tracing/trace_operation.rb | 8 +- 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 lib/datadog/tracing/distributed/baggage.rb diff --git a/lib/datadog/tracing/configuration/settings.rb b/lib/datadog/tracing/configuration/settings.rb index 565193ffdfd..56f009cc9e8 100644 --- a/lib/datadog/tracing/configuration/settings.rb +++ b/lib/datadog/tracing/configuration/settings.rb @@ -42,7 +42,7 @@ def self.extended(base) # # The tracer will try to find distributed headers in the order they are present in the list provided to this option. # The first format to have valid data present will be used. - # + # Baggage style is a special case, as it will always be extracted in addition if present. # @default `DD_TRACE_PROPAGATION_STYLE_EXTRACT` environment variable (comma-separated list), # otherwise `['datadog','b3multi','b3']`. # @return [Array] @@ -53,6 +53,7 @@ def self.extended(base) [ Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_DATADOG, Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_TRACE_CONTEXT, + Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_BAGGAGE, ] ) o.after_set do |styles| @@ -74,6 +75,7 @@ def self.extended(base) o.default [ Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_DATADOG, Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_TRACE_CONTEXT, + Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_BAGGAGE, ] o.after_set do |styles| # Make values case-insensitive diff --git a/lib/datadog/tracing/distributed/baggage.rb b/lib/datadog/tracing/distributed/baggage.rb new file mode 100644 index 00000000000..489f5f800ff --- /dev/null +++ b/lib/datadog/tracing/distributed/baggage.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require_relative '../metadata/ext' +require_relative '../trace_digest' +require_relative 'datadog_tags_codec' +require_relative '../utils' +require_relative 'helpers' +require 'uri' + +module Datadog + module Tracing + module Distributed + # W3C Baggage propagator implementation. + # The baggage header is propagated through `baggage`. + # @see https://www.w3.org/TR/baggage/ + class Baggage + BAGGAGE_KEY = 'baggage' + DD_TRACE_BAGGAGE_MAX_ITEMS = 64 + DD_TRACE_BAGGAGE_MAX_BYTES = 8192 + SAFE_CHARACTERS_KEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~" + SAFE_CHARACTERS_VALUE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'()*+-./:<>?@[]^_`{|}~" + private_constant :BAGGAGE_KEY, + :DD_TRACE_BAGGAGE_MAX_ITEMS, + :DD_TRACE_BAGGAGE_MAX_BYTES, + :SAFE_CHARACTERS_KEY, + :SAFE_CHARACTERS_VALUE + + def initialize( + fetcher:, + baggage_key: BAGGAGE_KEY + ) + @baggage_key = baggage_key + @fetcher = fetcher + end + + def inject!(digest, data) + baggage_items = digest.baggage.to_a + return if baggage_items.empty? + + begin + if baggage_items.size > DD_TRACE_BAGGAGE_MAX_ITEMS + Datadog.logger.warn('Baggage item limit exceeded, dropping excess items') + baggage_items = baggage_items.first(DD_TRACE_BAGGAGE_MAX_ITEMS) + end + + encoded_items = [] + total_size = 0 + + baggage_items.each do |key, value| + item = "#{encode_key(key)}=#{encode_value(value)}" + item_size = item.bytesize + (encoded_items.empty? ? 0 : 1) # +1 for comma if not first item + if total_size + item_size > DD_TRACE_BAGGAGE_MAX_BYTES + Datadog.logger.warn('Baggage header size exceeded, dropping excess items') + break # stop adding items when size limit is reached + end + encoded_items << item + total_size += item_size + end + + header_value = encoded_items.join(',') + data[baggage_key] = header_value + rescue => e + Datadog.logger.warn("Failed to encode and inject baggage header: #{e.message}") + end + end + + def extract(data) + fetcher = @fetcher.new(data) + baggage = parse_baggage_header(fetcher[@baggage_key]) + return unless baggage + + TraceDigest.new( + baggage: baggage, + ) + end + + private + + def encode_key(key) + URI.encode_www_form_component(key.strip).gsub(/[^#{Regexp.escape(SAFE_CHARACTERS_KEY)}]/o) do |char| + "%#{char.ord.to_s(16).upcase}" + end + end + + def encode_value(value) + URI.encode_www_form_component(value.strip).gsub(/[^#{Regexp.escape(SAFE_CHARACTERS_VALUE)}]/o) do |char| + "%#{char.ord.to_s(16).upcase}" + end + end + + def parse_baggage_header(baggage_header) + baggage = {} + baggages = baggage_header.split(',') + baggages.each do |key_value| + next unless key_value.include?('=') + + key, value = key_value.split('=', 2) + key = URI.decode_www_form_component(key.strip) + value = URI.decode_www_form_component(value.strip) + next if key.empty? || value.empty? + + baggage[key] = value + end + baggage + end + end + end + end +end diff --git a/lib/datadog/tracing/distributed/propagation.rb b/lib/datadog/tracing/distributed/propagation.rb index 9ad8b63ee35..c5be26b6fa7 100644 --- a/lib/datadog/tracing/distributed/propagation.rb +++ b/lib/datadog/tracing/distributed/propagation.rb @@ -29,6 +29,11 @@ def initialize( @propagation_style_inject = propagation_style_inject.map { |style| propagation_styles[style] } @propagation_style_extract = propagation_style_extract.map { |style| propagation_styles[style] } + + # The baggage propagator is unique in that baggage should always be extracted, if present. + # Therefore we remove it from the `propagation_style_extract` list. + @baggage_propagator = @propagation_style_extract.find { |propagator| propagator.is_a?(Baggage) } + @propagation_style_extract.delete(@baggage_propagator) if @baggage_propagator end # inject! populates the env with span ID, trace ID and sampling priority @@ -138,12 +143,24 @@ def extract(data) "Error extracting distributed trace data. Cause: #{e} Location: #{Array(e.backtrace).first}" ) end + # Handle baggage after all other styles if present + extracted_trace_digest = propagate_baggage(data, extracted_trace_digest) if @baggage_propagator extracted_trace_digest end private + def propagate_baggage(data, extracted_trace_digest) + if extracted_trace_digest + # Merge with baggage if present + TraceDigest.merge(extracted_trace_digest, @baggage_propagator.extract(data)) + else + # Baggage is the only style + @baggage_propagator.extract(data) + end + end + def last_datadog_parent_id(headers, tracecontext_tags) dd_propagator = @propagation_style_extract.find { |propagator| propagator.is_a?(Datadog) } if tracecontext_tags&.fetch( diff --git a/lib/datadog/tracing/trace_operation.rb b/lib/datadog/tracing/trace_operation.rb index eb85182af75..b7d1664db70 100644 --- a/lib/datadog/tracing/trace_operation.rb +++ b/lib/datadog/tracing/trace_operation.rb @@ -37,7 +37,8 @@ class TraceOperation :rule_sample_rate, :sample_rate, :sampling_priority, - :remote_parent + :remote_parent, + :baggage attr_reader \ :active_span_count, @@ -76,7 +77,8 @@ def initialize( trace_state: nil, trace_state_unknown_fields: nil, remote_parent: false, - tracer: nil + tracer: nil, + baggage: nil ) # Attributes @@ -101,6 +103,7 @@ def initialize( @trace_state = trace_state @trace_state_unknown_fields = trace_state_unknown_fields @tracer = tracer + @baggage = baggage || {} # Generic tags set_tags(tags) if tags @@ -332,6 +335,7 @@ def to_digest trace_state: @trace_state, trace_state_unknown_fields: @trace_state_unknown_fields, span_remote: (@remote_parent && @active_span.nil?), + baggage: @baggage, ).freeze end