diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 97ff6999a08..9d540d81d8d 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -2140,6 +2140,7 @@ end | `tracing.sampling.default_rate` | `DD_TRACE_SAMPLE_RATE` | `nil` | Sets the trace sampling rate between `0.0` (0%) and `1.0` (100%). See [Application-side sampling](#application-side-sampling) for details. | | `tracing.sampling.rate_limit` | `DD_TRACE_RATE_LIMIT` | `100` (per second) | Sets a maximum number of traces per second to sample. Set a rate limit to avoid the ingestion volume overages in the case of traffic spikes. | | `tracing.sampling.span_rules` | `DD_SPAN_SAMPLING_RULES`,`ENV_SPAN_SAMPLING_RULES_FILE` | `nil` | Sets [Single Span Sampling](#single-span-sampling) rules. These rules allow you to keep spans even when their respective traces are dropped. | +| `tracing.trace_id_128_bit_generation_enabled` | `DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED` | `false` | `true` to generate 128 bits trace ID and `false` to generate 64 bits trace ID | | `tracing.report_hostname` | `DD_TRACE_REPORT_HOSTNAME` | `false` | Adds hostname tag to traces. | | `tracing.test_mode.enabled` | `DD_TRACE_TEST_MODE_ENABLED` | `false` | Enables or disables test mode, for use of tracing in test suites. | | `tracing.test_mode.trace_flush` | | `nil` | Object that determines trace flushing behavior. | diff --git a/lib/datadog/tracing/configuration/ext.rb b/lib/datadog/tracing/configuration/ext.rb index 02a01728a8d..64bf4d4b50b 100644 --- a/lib/datadog/tracing/configuration/ext.rb +++ b/lib/datadog/tracing/configuration/ext.rb @@ -5,6 +5,7 @@ module Configuration # e.g. Env vars, default values, enums, etc... module Ext ENV_ENABLED = 'DD_TRACE_ENABLED'.freeze + ENV_TRACE_ID_128_BIT_GENERATION_ENABLED = 'DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED'.freeze # @public_api module Analytics @@ -14,6 +15,7 @@ module Analytics # @public_api module Correlation ENV_LOGS_INJECTION_ENABLED = 'DD_LOGS_INJECTION'.freeze + ENV_TRACE_ID_128_BIT_LOGGING_ENABLED = 'DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED'.freeze end # @public_api diff --git a/lib/datadog/tracing/configuration/settings.rb b/lib/datadog/tracing/configuration/settings.rb index 7caabc852b1..6b89fcd4524 100644 --- a/lib/datadog/tracing/configuration/settings.rb +++ b/lib/datadog/tracing/configuration/settings.rb @@ -173,6 +173,26 @@ def self.extended(base) o.lazy end + # Enable 128 bit trace id generation. + # + # @default `DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED` environment variable, otherwise `false` + # @return [Boolean] + option :trace_id_128_bit_generation_enabled do |o| + o.default { env_to_bool(Tracing::Configuration::Ext::ENV_TRACE_ID_128_BIT_GENERATION_ENABLED, false) } + o.lazy + end + + # Enable 128 bit trace id injected for logging. + # + # @default `DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED` environment variable, otherwise `false` + # @return [Boolean] + # + # It is not supported by our backend yet. Do not enable it. + option :trace_id_128_bit_logging_enabled do |o| + o.default { env_to_bool(Tracing::Configuration::Ext::Correlation::ENV_TRACE_ID_128_BIT_LOGGING_ENABLED, false) } + o.lazy + end + # A custom tracer instance. # # It must respect the contract of {Datadog::Tracing::Tracer}. diff --git a/lib/datadog/tracing/correlation.rb b/lib/datadog/tracing/correlation.rb index aad4c3eda6f..65dde53bf10 100644 --- a/lib/datadog/tracing/correlation.rb +++ b/lib/datadog/tracing/correlation.rb @@ -1,4 +1,6 @@ require_relative '../core' +require_relative 'utils' +require_relative 'metadata/ext' module Datadog module Tracing @@ -64,11 +66,23 @@ def to_log_format attributes << "#{LOG_ATTR_ENV}=#{env}" unless env.nil? attributes << "#{LOG_ATTR_SERVICE}=#{service}" attributes << "#{LOG_ATTR_VERSION}=#{version}" unless version.nil? - attributes << "#{LOG_ATTR_TRACE_ID}=#{trace_id}" + attributes << "#{LOG_ATTR_TRACE_ID}=#{logging_trace_id}" attributes << "#{LOG_ATTR_SPAN_ID}=#{span_id}" attributes.join(' ') end end + + private + + def logging_trace_id + @logging_trace_id ||= + if Datadog.configuration.tracing.trace_id_128_bit_logging_enabled && + !Tracing::Utils::TraceId.to_high_order(@trace_id).zero? + Kernel.format('%032x', trace_id) + else + Tracing::Utils::TraceId.to_low_order(@trace_id) + end + end end module_function diff --git a/lib/datadog/tracing/distributed/b3_multi.rb b/lib/datadog/tracing/distributed/b3_multi.rb index e651c0452b0..f5fcf5e7e23 100644 --- a/lib/datadog/tracing/distributed/b3_multi.rb +++ b/lib/datadog/tracing/distributed/b3_multi.rb @@ -2,6 +2,7 @@ require_relative 'helpers' require_relative '../trace_digest' +require_relative '../utils' module Datadog module Tracing @@ -45,13 +46,19 @@ def inject!(digest, data = {}) def extract(data) # DEV: B3 doesn't have "origin" fetcher = @fetcher.new(data) - trace_id = fetcher.id(@trace_id_key, base: 16) - span_id = fetcher.id(@span_id_key, base: 16) - # We don't need to try and convert sampled since B3 supports 0/1 (AUTO_REJECT/AUTO_KEEP) - sampling_priority = fetcher.number(@sampled_key) + + trace_id = Helpers.parse_hex_id(fetcher[@trace_id_key]) # Return early if this propagation is not valid - return unless trace_id && span_id + return if trace_id.nil? || trace_id <= 0 || trace_id > Tracing::Utils::TraceId::MAX + + span_id = Helpers.parse_hex_id(fetcher[@span_id_key]) + + # Return early if this propagation is not valid + return if span_id.nil? || span_id <= 0 || span_id >= Tracing::Utils::EXTERNAL_MAX_ID + + # We don't need to try and convert sampled since B3 supports 0/1 (AUTO_REJECT/AUTO_KEEP) + sampling_priority = Helpers.parse_decimal_id(fetcher[@sampled_key]) TraceDigest.new( trace_id: trace_id, diff --git a/lib/datadog/tracing/distributed/b3_single.rb b/lib/datadog/tracing/distributed/b3_single.rb index 585ecf48a6d..959829c5fbb 100644 --- a/lib/datadog/tracing/distributed/b3_single.rb +++ b/lib/datadog/tracing/distributed/b3_single.rb @@ -46,12 +46,15 @@ def extract(env) return unless value parts = value.split('-') - trace_id = Helpers.value_to_id(parts[0], base: 16) unless parts.empty? - span_id = Helpers.value_to_id(parts[1], base: 16) if parts.length > 1 - sampling_priority = Helpers.value_to_number(parts[2]) if parts.length > 2 + trace_id = Helpers.parse_hex_id(parts[0]) unless parts.empty? + # Return early if this propagation is not valid + return if trace_id.nil? || trace_id <= 0 || trace_id > Tracing::Utils::TraceId::MAX - # Return if this propagation is not valid - return unless trace_id && span_id + span_id = Helpers.parse_hex_id(parts[1]) if parts.length > 1 + # Return early if this propagation is not valid + return if span_id.nil? || span_id <= 0 || span_id >= Tracing::Utils::EXTERNAL_MAX_ID + + sampling_priority = Helpers.parse_decimal_id(parts[2]) if parts.length > 2 TraceDigest.new( span_id: span_id, diff --git a/lib/datadog/tracing/distributed/datadog.rb b/lib/datadog/tracing/distributed/datadog.rb index 9941bd6a1fd..ff7ce4f437f 100644 --- a/lib/datadog/tracing/distributed/datadog.rb +++ b/lib/datadog/tracing/distributed/datadog.rb @@ -3,6 +3,8 @@ require_relative '../metadata/ext' require_relative '../trace_digest' require_relative 'datadog_tags_codec' +require_relative '../utils' +require_relative 'helpers' module Datadog module Tracing @@ -38,21 +40,26 @@ def initialize( def inject!(digest, data) return if digest.nil? - data[@trace_id_key] = digest.trace_id.to_s + data[@trace_id_key] = Tracing::Utils::TraceId.to_low_order(digest.trace_id).to_s + data[@parent_id_key] = digest.span_id.to_s data[@sampling_priority_key] = digest.trace_sampling_priority.to_s if digest.trace_sampling_priority data[@origin_key] = digest.trace_origin.to_s if digest.trace_origin - inject_tags(digest, data) + build_tags(digest).tap do |tags| + inject_tags!(tags, data) unless tags.empty? + end data end def extract(data) fetcher = @fetcher.new(data) - trace_id = fetcher.id(@trace_id_key) - parent_id = fetcher.id(@parent_id_key) - sampling_priority = fetcher.number(@sampling_priority_key) + + trace_id = parse_trace_id(fetcher) + parent_id = parse_parent_id(fetcher) + + sampling_priority = Helpers.parse_decimal_id(fetcher[@sampling_priority_key]) origin = fetcher[@origin_key] # Return early if this propagation is not valid @@ -63,6 +70,10 @@ def extract(data) trace_distributed_tags = extract_tags(fetcher) + # If trace id is 128 bits long, + # Concatentated high order 64 bit hex-encoded `tid` tag + trace_id = extract_trace_id!(trace_id, trace_distributed_tags) + TraceDigest.new( span_id: parent_id, trace_id: trace_id, @@ -74,20 +85,57 @@ def extract(data) private + def parse_trace_id(fetcher_object) + trace_id = Helpers.parse_decimal_id(fetcher_object[@trace_id_key]) + + return unless trace_id + return if trace_id <= 0 || trace_id >= Tracing::Utils::EXTERNAL_MAX_ID + + trace_id + end + + def parse_parent_id(fetcher_object) + parent_id = Helpers.parse_decimal_id(fetcher_object[@parent_id_key]) + + return unless parent_id + return if parent_id <= 0 || parent_id >= Tracing::Utils::EXTERNAL_MAX_ID + + parent_id + end + + def build_tags(digest) + high_order = Tracing::Utils::TraceId.to_high_order(digest.trace_id) + tags = digest.trace_distributed_tags || {} + + return tags if high_order == 0 + + tags.merge(Tracing::Metadata::Ext::Distributed::TAG_TID => high_order.to_s(16)) + end + + # Side effect: Remove high order 64 bit hex-encoded `tid` tag from distributed tags + def extract_trace_id!(trace_id, tags) + return trace_id unless tags + return trace_id unless (high_order = tags.delete(Tracing::Metadata::Ext::Distributed::TAG_TID)) + + Tracing::Utils::TraceId.concatenate(high_order.to_i(16), trace_id) + end + # Export trace distributed tags through the `x-datadog-tags` key. # # DEV: This method accesses global state (the active trace) to record its error state as a trace tag. # DEV: This means errors cannot be reported if there's not active span. # DEV: Ideally, we'd have a dedicated error reporting stream for all of ddtrace. - def inject_tags(digest, data) - return if digest.trace_distributed_tags.nil? || digest.trace_distributed_tags.empty? + def inject_tags!(tags, data) return set_tags_propagation_error(reason: 'disabled') if tags_disabled? - tags = DatadogTagsCodec.encode(digest.trace_distributed_tags) + encoded_tags = DatadogTagsCodec.encode(tags) - return set_tags_propagation_error(reason: 'inject_max_size') if tags_too_large?(tags.size, scenario: 'inject') + return set_tags_propagation_error(reason: 'inject_max_size') if tags_too_large?( + encoded_tags.size, + scenario: 'inject' + ) - data[@tags_key] = tags + data[@tags_key] = encoded_tags rescue => e set_tags_propagation_error(reason: 'encoding_error') ::Datadog.logger.warn( diff --git a/lib/datadog/tracing/distributed/fetcher.rb b/lib/datadog/tracing/distributed/fetcher.rb index fd6eef5e2af..bde3891bcc9 100644 --- a/lib/datadog/tracing/distributed/fetcher.rb +++ b/lib/datadog/tracing/distributed/fetcher.rb @@ -15,14 +15,6 @@ def initialize(data) def [](key) @data[key] end - - def id(key, base: 10) - Helpers.value_to_id(self[key], base: base) - end - - def number(key, base: 10) - Helpers.value_to_number(self[key], base: base) - end end end end diff --git a/lib/datadog/tracing/distributed/helpers.rb b/lib/datadog/tracing/distributed/helpers.rb index 0a07bd1e284..17bab2dfaaa 100644 --- a/lib/datadog/tracing/distributed/helpers.rb +++ b/lib/datadog/tracing/distributed/helpers.rb @@ -20,54 +20,42 @@ def self.clamp_sampling_priority(sampling_priority) sampling_priority end - def self.truncate_base16_number(value) + def self.parse_decimal_id(value) + return unless value + + value = value.to_s + + num = value.to_i + + # Ensure the parsed number is the same as the original string value + # e.g. We want to make sure to throw away `'nan'.to_i == 0` + return unless num.to_s(10) == value + + num + end + + def self.parse_hex_id(value) + return unless value + # Lowercase if we want to parse base16 e.g. 3E8 => 3e8 # DEV: Ruby will parse `3E8` just fine, but to test # `num.to_s(base) == value` we need to lowercase - value = value.downcase - - # Truncate to trailing 16 characters if length is greater than 16 - # https://github.com/apache/incubator-zipkin/blob/21fe362899fef5c593370466bc5707d3837070c2/zipkin/src/main/java/zipkin2/storage/StorageComponent.java#L49-L53 - # DEV: This ensures we truncate B3 128-bit trace and span ids to 64-bit - value = value[value.length - 16, 16] if value.length > 16 + value = value.to_s.downcase # Remove any leading zeros # DEV: When we call `num.to_s(16)` later Ruby will not add leading zeros # for us so we want to make sure the comparision will work as expected # DEV: regex, remove all leading zeros up until we find the last 0 in the string # or we find the first non-zero, this allows `'0000' -> '0'` and `'00001' -> '1'` - value.sub(/^0*(?=(0$)|[^0])/, '') - end - - def self.value_to_id(value, base: 10) - id = value_to_number(value, base: base) - - # Return early if we could not parse a number - return if id.nil? - - # Zero or greater than max allowed value of 2**64 - return if id.zero? || id > Tracing::Utils::EXTERNAL_MAX_ID - - id < 0 ? id + (2**64) : id - end - - def self.value_to_number(value, base: 10) - # It's important to make a difference between no data and zero. - return if value.nil? - - # Be sure we have a string - value = value.to_s - - # If we are parsing base16 number then truncate to 64-bit - value = Helpers.truncate_base16_number(value) if base == 16 + value = value.sub(/^0*(?=(0$)|[^0])/, '') # Convert value to an integer # DEV: Ruby `.to_i` will return `0` if a number could not be parsed - num = value.to_i(base) + num = value.to_i(16) # Ensure the parsed number is the same as the original string value # e.g. We want to make sure to throw away `'nan'.to_i == 0` - return unless num.to_s(base) == value + return unless num.to_s(16) == value num end diff --git a/lib/datadog/tracing/distributed/trace_context.rb b/lib/datadog/tracing/distributed/trace_context.rb index 7db4be1924a..94074d8965e 100644 --- a/lib/datadog/tracing/distributed/trace_context.rb +++ b/lib/datadog/tracing/distributed/trace_context.rb @@ -38,7 +38,7 @@ def inject!(digest, data) def extract(data) fetcher = @fetcher.new(data) - trace_id, dd_trace_id, parent_id, sampled, trace_flags = extract_traceparent(fetcher[@traceparent_key]) + trace_id, parent_id, sampled, trace_flags = extract_traceparent(fetcher[@traceparent_key]) return unless trace_id # Could not parse traceparent @@ -48,11 +48,10 @@ def extract(data) TraceDigest.new( span_id: parent_id, - trace_id: dd_trace_id, + trace_id: trace_id, trace_origin: origin, trace_sampling_priority: sampling_priority, trace_distributed_tags: tags, - trace_distributed_id: trace_id, trace_flags: trace_flags, trace_state: tracestate, trace_state_unknown_fields: unknown_fields, @@ -92,7 +91,7 @@ def delete_prefix(prefix) # @see https://www.w3.org/TR/trace-context/#traceparent-header def build_traceparent(digest) build_traceparent_string( - digest.trace_distributed_id || digest.trace_id, + digest.trace_id, digest.span_id, build_trace_flags(digest) ) @@ -229,10 +228,9 @@ def extract_traceparent(traceparent) # Return unless all traceparent fields are valid. return unless trace_id && !trace_id.zero? && parent_id && !parent_id.zero? && trace_flags - dd_trace_id = parse_datadog_trace_id(trace_id) sampled = parse_sampled_flag(trace_flags) - [trace_id, dd_trace_id, parent_id, sampled, trace_flags] + [trace_id, parent_id, sampled, trace_flags] end def parse_traceparent_string(traceparent) @@ -253,12 +251,6 @@ def parse_traceparent_string(traceparent) nil end - # Datadog only allows 64 bits for the trace id. - # We extract the lower 64 bits from the original 128-bit id. - def parse_datadog_trace_id(trace_id) - trace_id & 0xFFFFFFFFFFFFFFFF - end - def parse_sampled_flag(trace_flags) trace_flags & TRACE_FLAGS_SAMPLED end @@ -299,6 +291,10 @@ def extract_datadog_fields(dd_tracestate) when /^t\./ key.slice!(0..1) # Delete `t.` prefix + # Ignore the high order 64 bit trace id propagation tag to avoid confusion, + # the single source of truth is from traceparent + next if key == Tracing::Metadata::Ext::Distributed::TID + value = deserialize_tag_value(value) tags ||= {} diff --git a/lib/datadog/tracing/metadata/ext.rb b/lib/datadog/tracing/metadata/ext.rb index 3ff75cdfdc9..3571033ab86 100644 --- a/lib/datadog/tracing/metadata/ext.rb +++ b/lib/datadog/tracing/metadata/ext.rb @@ -53,6 +53,11 @@ module Distributed # Trace tags with this prefix will propagate from a trace through distributed tracing. # Distributed headers tags with this prefix will be injected into the active trace. TAGS_PREFIX = '_dd.p.' + + # The distributed tag to carry hex encoded high order 64 bits of 127 bits trace id during + # the context restricted with 64 bits. Such as, Datadog propagation and messagepack encoding + TID = 'tid' + TAG_TID = TAGS_PREFIX + TID end # @public_api diff --git a/lib/datadog/tracing/span_operation.rb b/lib/datadog/tracing/span_operation.rb index a2be0c39db8..7056a8ae8f3 100644 --- a/lib/datadog/tracing/span_operation.rb +++ b/lib/datadog/tracing/span_operation.rb @@ -62,7 +62,7 @@ def initialize( @id = Tracing::Utils.next_id @parent_id = parent_id || 0 - @trace_id = trace_id || Tracing::Utils.next_id + @trace_id = trace_id || Tracing::Utils::TraceId.next_id @status = 0 diff --git a/lib/datadog/tracing/trace_operation.rb b/lib/datadog/tracing/trace_operation.rb index d0a5b95ec6f..857af99d3ec 100644 --- a/lib/datadog/tracing/trace_operation.rb +++ b/lib/datadog/tracing/trace_operation.rb @@ -69,7 +69,7 @@ def initialize( metrics: nil ) # Attributes - @id = id || Tracing::Utils.next_id + @id = id || Tracing::Utils::TraceId.next_id @max_length = max_length || DEFAULT_MAX_LENGTH @parent_span_id = parent_span_id @sampled = sampled.nil? ? true : sampled diff --git a/lib/datadog/tracing/trace_segment.rb b/lib/datadog/tracing/trace_segment.rb index b9975850c1a..e4b6a31d451 100644 --- a/lib/datadog/tracing/trace_segment.rb +++ b/lib/datadog/tracing/trace_segment.rb @@ -4,6 +4,7 @@ require_relative 'sampling/ext' require_relative 'metadata/ext' require_relative 'metadata/tagging' +require_relative 'utils' module Datadog module Tracing @@ -126,6 +127,12 @@ def sampled? || sampling_priority == Sampling::Ext::Priority::USER_KEEP end + def high_order_tid + high_order = Tracing::Utils::TraceId.to_high_order(@id) + + high_order.to_s(16) if high_order != 0 + end + protected attr_reader \ diff --git a/lib/datadog/tracing/utils.rb b/lib/datadog/tracing/utils.rb index 0bd833d418b..6cba06312a8 100644 --- a/lib/datadog/tracing/utils.rb +++ b/lib/datadog/tracing/utils.rb @@ -1,4 +1,5 @@ require_relative '../core/utils/forking' +require_relative '../core/utils/time' module Datadog module Tracing @@ -43,6 +44,38 @@ def self.reset! end private_class_method :id_rng, :reset! + + # The module handles bitwise operation for trace id + module TraceId + MAX = (1 << 128) - 1 + + module_function + + # Format for generating 128 bits trace id => + # - 32-bits : seconds since Epoch + # - 32-bits : set to zero, + # - 64 bits : random 64-bits + def next_id + return Utils.next_id unless Datadog.configuration.tracing.trace_id_128_bit_generation_enabled + + concatenate( + Core::Utils::Time.now.to_i << 32, + Utils.next_id + ) + end + + def to_high_order(trace_id) + trace_id >> 64 + end + + def to_low_order(trace_id) + trace_id & 0xFFFFFFFFFFFFFFFF + end + + def concatenate(high_order, low_order) + high_order << 64 | low_order + end + end end end end diff --git a/lib/ddtrace/transport/serializable_trace.rb b/lib/ddtrace/transport/serializable_trace.rb index e0fe57058f2..cbf6c3c16d5 100644 --- a/lib/ddtrace/transport/serializable_trace.rb +++ b/lib/ddtrace/transport/serializable_trace.rb @@ -1,5 +1,6 @@ require 'json' require 'msgpack' +require 'datadog/tracing/utils' module Datadog module Transport @@ -28,7 +29,7 @@ def to_msgpack(packer = nil) # JSON serializer interface. # Used by older version of the transport. def to_json(*args) - trace.spans.map(&:to_hash).to_json(*args) + trace.spans.map { |s| SerializableSpan.new(s).to_hash }.to_json(*args) end end @@ -39,6 +40,7 @@ class SerializableSpan def initialize(span) @span = span + @trace_id = Tracing::Utils::TraceId.to_low_order(span.trace_id) end # MessagePack serializer interface. Making this object @@ -75,7 +77,7 @@ def to_msgpack(packer = nil) packer.write('parent_id') packer.write(span.parent_id) packer.write('trace_id') - packer.write(span.trace_id) + packer.write(@trace_id) packer.write('name') packer.write(span.name) packer.write('service') @@ -97,7 +99,7 @@ def to_msgpack(packer = nil) # JSON serializer interface. # Used by older version of the transport. def to_json(*args) - span.to_hash.to_json(*args) + to_hash.to_json(*args) end # Used for serialization @@ -106,6 +108,10 @@ def time_nano(time) time.to_i * 1000000000 + time.nsec end + def to_hash + span.to_hash.merge(trace_id: @trace_id) + end + # Used for serialization # @return [Integer] in nanoseconds since Epoch def duration_nano(duration) diff --git a/lib/ddtrace/transport/trace_formatter.rb b/lib/ddtrace/transport/trace_formatter.rb index 81ae5f397a1..ac05759c977 100644 --- a/lib/ddtrace/transport/trace_formatter.rb +++ b/lib/ddtrace/transport/trace_formatter.rb @@ -47,6 +47,7 @@ def format! tag_rate_limiter_rate! tag_sample_rate! tag_sampling_decision_maker! + tag_high_order_trace_id! tag_sampling_priority! trace @@ -166,6 +167,12 @@ def tag_sampling_priority! ) end + def tag_high_order_trace_id! + return unless (high_order_tid = trace.high_order_tid) + + root_span.set_tag(Tracing::Metadata::Ext::Distributed::TAG_TID, high_order_tid) + end + private def partial? diff --git a/spec/datadog/opentracer/rack_propagator_spec.rb b/spec/datadog/opentracer/rack_propagator_spec.rb index a0a0ad4b3ea..32b5f88a10c 100644 --- a/spec/datadog/opentracer/rack_propagator_spec.rb +++ b/spec/datadog/opentracer/rack_propagator_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' require 'datadog/tracing/context' +require 'datadog/tracing/utils' require 'datadog/tracing/propagation/http' require 'datadog/tracing/trace_digest' require 'datadog/tracing/trace_operation' @@ -10,7 +11,7 @@ describe '#inject' do subject { described_class.inject(span_context, carrier) } - let(:trace_id) { double('trace ID') } + let(:trace_id) { Datadog::Tracing::Utils::TraceId.next_id } let(:span_id) { double('span ID') } let(:sampling_decision) { '-1' } let(:sampling_priority) { double('sampling priority') } diff --git a/spec/datadog/tracing/configuration/settings_spec.rb b/spec/datadog/tracing/configuration/settings_spec.rb index 2cc88a85fc9..61d477c5197 100644 --- a/spec/datadog/tracing/configuration/settings_spec.rb +++ b/spec/datadog/tracing/configuration/settings_spec.rb @@ -690,5 +690,89 @@ def propagation_inject_style .to(123) end end + + describe '#trace_id_128_bit_generation_enabled' do + subject { settings.tracing.trace_id_128_bit_generation_enabled } + + context 'when given environment variable `DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED`' do + around do |example| + ClimateControl.modify( + 'DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED' => env_var + ) do + example.run + end + end + + context 'is not defined' do + let(:env_var) { nil } + + it { is_expected.to eq(false) } + end + + context 'is `true`' do + let(:env_var) { 'true' } + + it { is_expected.to eq(true) } + end + + context 'is `false`' do + let(:env_var) { 'false' } + + it { is_expected.to eq(false) } + end + end + end + + describe '#trace_id_128_bit_generation_enabled=' do + it 'updates the #trace_id_128_bit_generation_enabled setting' do + expect do + settings.tracing.trace_id_128_bit_generation_enabled = true + end.to change { settings.tracing.trace_id_128_bit_generation_enabled } + .from(false) + .to(true) + end + end + + describe '#trace_id_128_bit_logging_enabled' do + subject { settings.tracing.trace_id_128_bit_logging_enabled } + + context 'when given environment variable `DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED ' do + around do |example| + ClimateControl.modify( + 'DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED' => env_var + ) do + example.run + end + end + + context 'is not defined' do + let(:env_var) { nil } + + it { is_expected.to eq(false) } + end + + context 'is `true`' do + let(:env_var) { 'true' } + + it { is_expected.to eq(true) } + end + + context 'is `false`' do + let(:env_var) { 'false' } + + it { is_expected.to eq(false) } + end + end + end + + describe '#trace_id_128_bit_logging_enabled=' do + it 'updates the #trace_id_128_bit_logging_enabled setting' do + expect do + settings.tracing.trace_id_128_bit_logging_enabled = true + end.to change { settings.tracing.trace_id_128_bit_logging_enabled } + .from(false) + .to(true) + end + end end end diff --git a/spec/datadog/tracing/correlation_spec.rb b/spec/datadog/tracing/correlation_spec.rb index 8a1dea17c4b..caa3d814207 100644 --- a/spec/datadog/tracing/correlation_spec.rb +++ b/spec/datadog/tracing/correlation_spec.rb @@ -206,7 +206,7 @@ service: service, span_id: span_id, trace_id: trace_id, - version: version + version: version, ) end @@ -229,12 +229,77 @@ def have_attribute(attribute) context 'when #trace_id' do context 'is defined' do - it_behaves_like 'a log format string' do - let(:trace_id) { Datadog::Tracing::Utils.next_id } - it do - is_expected.to have_attribute( - "#{Datadog::Tracing::Correlation::Identifier::LOG_ATTR_TRACE_ID}=#{trace_id}" - ) + context 'when 128 bit trace id logging is not enabled' do + before do + allow(Datadog.configuration.tracing).to receive(:trace_id_128_bit_logging_enabled).and_return(false) + end + + context 'when given 64 bit trace id' do + it_behaves_like 'a log format string' do + let(:trace_id) { 0xaaaaaaaaaaaaaaaa } + let(:expected_trace_id) { trace_id } + it do + is_expected.to have_attribute( + "#{Datadog::Tracing::Correlation::Identifier::LOG_ATTR_TRACE_ID}=#{expected_trace_id}" + ) + end + end + end + + context 'when given 128 bit trace id' do + it_behaves_like 'a log format string' do + let(:trace_id) { 0xaaaaaaaaaaaaaaaaffffffffffffffff } + let(:expected_trace_id) { 0xffffffffffffffff } + it do + is_expected.to have_attribute( + "#{Datadog::Tracing::Correlation::Identifier::LOG_ATTR_TRACE_ID}=#{expected_trace_id}" + ) + end + end + end + end + + context 'when 128 bit trace id logging is enabled' do + before do + allow(Datadog.configuration.tracing).to receive(:trace_id_128_bit_logging_enabled).and_return(true) + end + + context 'when given 64 bit trace id' do + it_behaves_like 'a log format string' do + let(:trace_id) { 0xaaaaaaaaaaaaaaaa } + let(:expected_trace_id) { trace_id } + it do + is_expected.to have_attribute( + "#{Datadog::Tracing::Correlation::Identifier::LOG_ATTR_TRACE_ID}=#{expected_trace_id}" + ) + end + end + end + + context 'when given > 64 bit trace id' do + it_behaves_like 'a log format string' do + let(:trace_id) { 0xffffffffffffffffaaaaaaaaaaaaaaaa } + let(:expected_trace_id) { trace_id } + + it do + is_expected.to have_attribute( + "#{Datadog::Tracing::Correlation::Identifier::LOG_ATTR_TRACE_ID}=ffffffffffffffffaaaaaaaaaaaaaaaa" + ) + end + end + end + end + + context 'when given > 64 bit trace id but high order is 0' do + it_behaves_like 'a log format string' do + let(:trace_id) { 0x00000000000000000aaaaaaaaaaaaaaaa } + let(:expected_trace_id) { trace_id } + + it do + is_expected.to have_attribute( + "#{Datadog::Tracing::Correlation::Identifier::LOG_ATTR_TRACE_ID}=#{expected_trace_id}" + ) + end end end end diff --git a/spec/datadog/tracing/distributed/b3_multi_spec.rb b/spec/datadog/tracing/distributed/b3_multi_spec.rb index 10844018046..faa90b03e55 100644 --- a/spec/datadog/tracing/distributed/b3_multi_spec.rb +++ b/spec/datadog/tracing/distributed/b3_multi_spec.rb @@ -75,6 +75,24 @@ end end end + + context 'with 128 bit trace id and distributed tag `_dd.p.tid`' do + let(:digest) do + Datadog::Tracing::TraceDigest.new( + trace_id: 0xaaaaaaaaaaaaaaaaffffffffffffffff, + span_id: 0xbbbbbbbbbbbbbbbb + ) + end + + it do + inject! + + expect(data).to eq( + 'x-b3-traceid' => 'aaaaaaaaaaaaaaaaffffffffffffffff', + 'x-b3-spanid' => 'bbbbbbbbbbbbbbbb', + ) + end + end end describe '#extract' do @@ -123,6 +141,46 @@ it { expect(digest.trace_sampling_priority).to be nil } it { expect(digest.trace_origin).to be nil } end + + context 'when given invalid trace id' do + [ + ((1 << 128)).to_s(16), # 0 + ((1 << 128) + 1).to_s(16), + '0', + '-1', + ].each do |invalid_trace_id| + context "when given trace id: #{invalid_trace_id}" do + let(:data) do + { + prepare_key['x-b3-traceid'] => invalid_trace_id, + prepare_key['x-b3-spanid'] => 20000.to_s(16) + } + end + + it { is_expected.to be nil } + end + end + end + + context 'when given invalid span id' do + [ + ((1 << 64)).to_s(16), + ((1 << 64) + 1).to_s(16), + '0', + '-1', + ].each do |invalid_span_id| + context "when given span id: #{invalid_span_id}" do + let(:data) do + { + prepare_key['x-b3-traceid'] => 10000.to_s(16), + prepare_key['x-b3-spanid'] => invalid_span_id, + } + end + + it { is_expected.to be nil } + end + end + end end context 'with span_id' do @@ -142,6 +200,18 @@ it { is_expected.to be nil } end + + context 'with 128 bit trace id' do + let(:data) do + { + prepare_key['x-b3-traceid'] => 'aaaaaaaaaaaaaaaaffffffffffffffff', + prepare_key['x-b3-spanid'] => 'bbbbbbbbbbbbbbbb', + } + end + + it { expect(digest.trace_id).to eq(0xaaaaaaaaaaaaaaaaffffffffffffffff) } + it { expect(digest.span_id).to eq(0xbbbbbbbbbbbbbbbb) } + end end end diff --git a/spec/datadog/tracing/distributed/b3_single_spec.rb b/spec/datadog/tracing/distributed/b3_single_spec.rb index d14f1c6cdc0..79f1e88bc13 100644 --- a/spec/datadog/tracing/distributed/b3_single_spec.rb +++ b/spec/datadog/tracing/distributed/b3_single_spec.rb @@ -52,13 +52,28 @@ context 'with origin' do let(:digest) do Datadog::Tracing::TraceDigest.new( - span_id: 100000, - trace_id: 90000, + trace_id: 0xabcdef, + span_id: 0xfedcba, trace_origin: 'synthetics' ) end - it { expect(data).to eq(b3_single_header => '15f90-186a0') } + it { expect(data).to eq(b3_single_header => 'abcdef-fedcba') } + end + end + + context 'with 128 bits trace id and distributed tag `_dd.p.tid`' do + let(:digest) do + Datadog::Tracing::TraceDigest.new( + trace_id: 0xaaaaaaaaaaaaaaaaffffffffffffffff, + span_id: 0xbbbbbbbbbbbbbbbb + ) + end + + it do + inject! + + expect(data).to eq(b3_single_header => 'aaaaaaaaaaaaaaaaffffffffffffffff-bbbbbbbbbbbbbbbb') end end end @@ -74,40 +89,78 @@ end context 'with trace_id and span_id' do - let(:data) { { prepare_key[b3_single_header] => '15f90-186a0' } } + let(:data) { { prepare_key[b3_single_header] => 'abcdef-fedcba' } } - it { expect(digest.span_id).to eq(100000) } - it { expect(digest.trace_id).to eq(90000) } + it { expect(digest.trace_id).to eq(0xabcdef) } + it { expect(digest.span_id).to eq(0xfedcba) } it { expect(digest.trace_origin).to be nil } it { expect(digest.trace_sampling_priority).to be nil } context 'with sampling priority' do - let(:data) { { prepare_key[b3_single_header] => '15f90-186a0-1' } } + let(:data) { { prepare_key[b3_single_header] => 'abcdef-fedcba-1' } } - it { expect(digest.span_id).to eq(100000) } - it { expect(digest.trace_id).to eq(90000) } + it { expect(digest.trace_id).to eq(0xabcdef) } + it { expect(digest.span_id).to eq(0xfedcba) } it { expect(digest.trace_origin).to be nil } it { expect(digest.trace_sampling_priority).to eq(1) } context 'with parent_id' do let(:data) do { - prepare_key[b3_single_header] => '15f90-186a0-1-4e20' + prepare_key[b3_single_header] => 'abcdef-fedcba-1-4e20' } end - it { expect(digest.trace_id).to eq(90000) } - it { expect(digest.span_id).to eq(100000) } + it { expect(digest.trace_id).to eq(0xabcdef) } + it { expect(digest.span_id).to eq(0xfedcba) } it { expect(digest.trace_sampling_priority).to eq(1) } it { expect(digest.trace_origin).to be nil } end end + + context 'when given invalid trace id' do + [ + ((1 << 128)).to_s(16), # 0 + ((1 << 128) + 1).to_s(16), + '0', + '-1', + ].each do |invalid_trace_id| + context "when given trace id: #{invalid_trace_id}" do + let(:data) { { prepare_key[b3_single_header] => "#{invalid_trace_id}-fedcba" } } + + it { is_expected.to be nil } + end + end + end + + context 'when given invalid span id' do + [ + ((1 << 64)).to_s(16), + ((1 << 64) + 1).to_s(16), + '0', + ].each do |invalid_span_id| + context "when given span id: #{invalid_span_id}" do + let(:data) { { prepare_key[b3_single_header] => "abcdef-#{invalid_span_id}" } } + + it { is_expected.to be nil } + end + end + end end context 'with trace_id' do - let(:env) { { prepare_key[b3_single_header] => '15f90' } } + let(:data) { { prepare_key[b3_single_header] => 'abcdef' } } it { is_expected.to be nil } + + context 'with 128 bits trace id and 64 bits span id' do + let(:data) do + { prepare_key[b3_single_header] => 'aaaaaaaaaaaaaaaaffffffffffffffff-bbbbbbbbbbbbbbbb' } + end + + it { expect(digest.trace_id).to eq(0xaaaaaaaaaaaaaaaaffffffffffffffff) } + it { expect(digest.span_id).to eq(0xbbbbbbbbbbbbbbbb) } + end end end end diff --git a/spec/datadog/tracing/distributed/datadog_spec.rb b/spec/datadog/tracing/distributed/datadog_spec.rb index 881ccb0f6ae..dfd0c0c429d 100644 --- a/spec/datadog/tracing/distributed/datadog_spec.rb +++ b/spec/datadog/tracing/distributed/datadog_spec.rb @@ -2,6 +2,7 @@ require 'datadog/tracing/distributed/datadog' require 'datadog/tracing/trace_digest' +require 'datadog/tracing/utils' RSpec.shared_examples 'Datadog distributed format' do subject(:datadog) { described_class.new(fetcher: fetcher_class) } @@ -94,7 +95,12 @@ end context 'with trace_distributed_tags' do - let(:digest) { Datadog::Tracing::TraceDigest.new(trace_distributed_tags: tags) } + let(:digest) do + Datadog::Tracing::TraceDigest.new( + trace_id: Datadog::Tracing::Utils::TraceId.next_id, + trace_distributed_tags: tags + ) + end context 'nil' do let(:tags) { nil } @@ -189,6 +195,43 @@ end end end + + context 'when given a trace digest with 128 bit trace id' do + let(:digest) do + Datadog::Tracing::TraceDigest.new( + trace_id: 0xaaaaaaaaaaaaaaaaffffffffffffffff, + span_id: 0xbbbbbbbbbbbbbbbb + ) + end + + it do + inject! + + expect(data).to eq( + 'x-datadog-trace-id' => 0xffffffffffffffff.to_s, + 'x-datadog-parent-id' => 0xbbbbbbbbbbbbbbbb.to_s, + 'x-datadog-tags' => '_dd.p.tid=aaaaaaaaaaaaaaaa' + ) + end + end + + context 'when given a trace digest with 64 bit trace id' do + let(:digest) do + Datadog::Tracing::TraceDigest.new( + trace_id: 0xffffffffffffffff, + span_id: 0xbbbbbbbbbbbbbbbb + ) + end + + it do + inject! + + expect(data).to eq( + 'x-datadog-trace-id' => 0xffffffffffffffff.to_s, + 'x-datadog-parent-id' => 0xbbbbbbbbbbbbbbbb.to_s + ) + end + end end end @@ -339,6 +382,40 @@ end end end + + context 'when given invalid trace_id' do + [ + (1 << 64).to_s, + '0', + '-1' + ].each do |invalid_trace_id| + context "when given invalid trace_id: #{invalid_trace_id}" do + let(:data) do + { prepare_key['x-datadog-trace-id'] => invalid_trace_id, + prepare_key['x-datadog-parent-id'] => '20000' } + end + + it { is_expected.to be nil } + end + end + end + + context 'when given invalid span_id' do + [ + (1 << 64).to_s, + '0', + '-1' + ].each do |invalid_span_id| + context "when given invalid span_id: #{invalid_span_id}" do + let(:data) do + { prepare_key['x-datadog-trace-id'] => '10000', + prepare_key['x-datadog-parent-id'] => invalid_span_id } + end + + it { is_expected.to be nil } + end + end + end end context 'with span_id' do @@ -388,6 +465,20 @@ it { expect(digest.trace_sampling_priority).to be nil } end end + + context 'with trace id and distributed tags `_dd.p.tid`' do + let(:data) do + { + prepare_key['x-datadog-trace-id'] => 0xffffffffffffffff.to_s, + prepare_key['x-datadog-parent-id'] => 0xbbbbbbbbbbbbbbbb.to_s, + prepare_key['x-datadog-tags'] => '_dd.p.tid=aaaaaaaaaaaaaaaa' + } + end + + it { expect(digest.trace_id).to eq(0xaaaaaaaaaaaaaaaaffffffffffffffff) } + it { expect(digest.span_id).to eq(0xbbbbbbbbbbbbbbbb) } + it { expect(digest.trace_distributed_tags).not_to include('_dd.p.tid') } + end end end diff --git a/spec/datadog/tracing/distributed/fetcher_spec.rb b/spec/datadog/tracing/distributed/fetcher_spec.rb index 38aeecc4443..54d13593939 100644 --- a/spec/datadog/tracing/distributed/fetcher_spec.rb +++ b/spec/datadog/tracing/distributed/fetcher_spec.rb @@ -22,32 +22,4 @@ it { is_expected.to eq('value') } end end - - describe '#id' do - subject(:id) { fetcher.id(key, base: base) } - let(:data) { { key => value } } - let(:key) { double('key') } - let(:value) { double('value') } - let(:base) { double('base') } - - it 'delegates to Datadog::Tracing::Distributed::Helpers.value_to_id' do - ret = double('return') - expect(Datadog::Tracing::Distributed::Helpers).to receive(:value_to_id).with(value, base: base).and_return(ret) - is_expected.to eq(ret) - end - end - - describe '#number' do - subject(:number) { fetcher.number(key, base: base) } - let(:data) { { key => value } } - let(:key) { double('key') } - let(:value) { double('value') } - let(:base) { double('base') } - - it 'delegates to Datadog::Tracing::Distributed::Helpers.value_to_number' do - ret = double('return') - expect(Datadog::Tracing::Distributed::Helpers).to receive(:value_to_number).with(value, base: base).and_return(ret) - is_expected.to eq(ret) - end - end end diff --git a/spec/datadog/tracing/distributed/helpers_spec.rb b/spec/datadog/tracing/distributed/helpers_spec.rb index 305efb495e3..821f3f4594b 100644 --- a/spec/datadog/tracing/distributed/helpers_spec.rb +++ b/spec/datadog/tracing/distributed/helpers_spec.rb @@ -21,156 +21,46 @@ end end - describe '#truncate_base16_number' do - subject(:number) { described_class.truncate_base16_number(value) } - + describe '.parse_decimal_id' do [ - %w[1 1], - - # Test removing leading zeros - %w[0 0], - %w[000000 0], - %w[000001 1], - %w[000010 10], - - # Test lowercase - %w[DEADBEEF deadbeef], - - # Test at boundary (64-bit) - # 64-bit max, which is 17 characters long, so we truncate to the last 16, which is all zeros - [(2**64).to_s(16), '0'], - # 64-bit - 1, which is the max 16 characters we allow - [((2**64) - 1).to_s(16), 'ffffffffffffffff'], - - # Our max generated id - [Datadog::Tracing::Utils::RUBY_MAX_ID.to_s(16), '3fffffffffffffff'], - # Our max external id - # DEV: This is the same as (2**64) above, but use the constant to be sure - [Datadog::Tracing::Utils::EXTERNAL_MAX_ID.to_s(16), '0'], - - # 128-bit max, which is 32 characters long, so we truncate to the last 16, which is all zeros - [(2**128).to_s(16), '0'], - # 128-bit - 1, which is 32 characters long and all `f`s - [((2**128) - 1).to_s(16), 'ffffffffffffffff'] + [nil, nil], + ['', nil], + ['not a number', nil], + ['1 2', nil], # "1 2".to_i => 1, but it's an invalid format + ['1.2', nil], + ['0', 0], + ['1', 1], + ['-1', -1], + ['123456789', 123456789], ].each do |value, expected| - context "with input of #{value}" do - let(:value) { value } - - it { is_expected.to eq(expected) } + context "when given #{value.inspect}" do + it { expect(described_class.parse_decimal_id(value)).to eq(expected) } end end end - describe '#value_to_id' do - context 'when value is not present' do - subject(:subject) { described_class.value_to_id(nil) } - - it { is_expected.to be_nil } - end - - context 'when value is' do + describe '.parse_hex_id' do + context 'when given with `length`' do [ [nil, nil], - ['not a number', nil], - ['1 2', nil], # "1 2".to_i => 1, but it's an invalid format - ['0', nil], ['', nil], - - # Larger than we allow - [(Datadog::Tracing::Utils::EXTERNAL_MAX_ID + 1).to_s, nil], - - # Negative number - ['-100', -100 + (2**64)], - - # Allowed values - [Datadog::Tracing::Utils::RUBY_MAX_ID.to_s, Datadog::Tracing::Utils::RUBY_MAX_ID], - [Datadog::Tracing::Utils::EXTERNAL_MAX_ID.to_s, Datadog::Tracing::Utils::EXTERNAL_MAX_ID], - ['1', 1], - ['123456789', 123456789] - ].each do |value, expected| - context value.inspect do - it { expect(described_class.value_to_id(value)).to eq(expected) } - end - end - - # Base 16 - [ - # Larger than we allow - # DEV: We truncate to 64-bit for base16 - [(Datadog::Tracing::Utils::EXTERNAL_MAX_ID + 1).to_s(16), 1], - [Datadog::Tracing::Utils::EXTERNAL_MAX_ID.to_s(16), nil], - - [Datadog::Tracing::Utils::RUBY_MAX_ID.to_s(16), Datadog::Tracing::Utils::RUBY_MAX_ID], - [(Datadog::Tracing::Utils::EXTERNAL_MAX_ID - 1).to_s(16), Datadog::Tracing::Utils::EXTERNAL_MAX_ID - 1], - - ['3e8', 1000], - ['3E8', 1000], - ['10000', 65536], - ['deadbeef', 3735928559], - ].each do |value, expected| - context value.inspect do - it { expect(described_class.value_to_id(value, base: 16)).to eq(expected) } - end - end - end - end - - describe '#value_to_number' do - context 'when value is not present' do - subject(:subject) { described_class.value_to_number(nil) } - - it { is_expected.to be_nil } - end - - context 'when value is ' do - [ - [nil, nil], ['not a number', nil], ['1 2', nil], # "1 2".to_i => 1, but it's an invalid format - ['', nil], - - # Sampling priorities - ['-1', -1], + ['1.2', nil], ['0', 0], ['1', 1], - ['2', 2], - - # Allowed values - [Datadog::Tracing::Utils::RUBY_MAX_ID.to_s, Datadog::Tracing::Utils::RUBY_MAX_ID], - [(Datadog::Tracing::Utils::RUBY_MAX_ID + 1).to_s, Datadog::Tracing::Utils::RUBY_MAX_ID + 1], - [Datadog::Tracing::Utils::EXTERNAL_MAX_ID.to_s, Datadog::Tracing::Utils::EXTERNAL_MAX_ID], - [(Datadog::Tracing::Utils::EXTERNAL_MAX_ID + 1).to_s, Datadog::Tracing::Utils::EXTERNAL_MAX_ID + 1], - ['-100', -100], - ['100', 100], - ].each do |value, expected| - context value.inspect do - subject(:subject) { described_class.value_to_number(value) } - - it { is_expected.to be == expected } - end - end - - # Base 16 - [ - # Larger than we allow - # DEV: We truncate to 64-bit for base16, so the - [Datadog::Tracing::Utils::EXTERNAL_MAX_ID.to_s(16), 0], - [(Datadog::Tracing::Utils::EXTERNAL_MAX_ID + 1).to_s(16), 1], - - [Datadog::Tracing::Utils::RUBY_MAX_ID.to_s(16), Datadog::Tracing::Utils::RUBY_MAX_ID], - [(Datadog::Tracing::Utils::RUBY_MAX_ID + 1).to_s(16), Datadog::Tracing::Utils::RUBY_MAX_ID + 1], - - ['3e8', 1000], - ['3E8', 1000], - ['deadbeef', 3735928559], - ['10000', 65536], - - ['invalid-base16', nil] + ['-1', -1], + ['123456789', 0x123456789], + ['abcdef', 0xabcdef], # lower case + ['ABCDEF', 0xabcdef], # upper case + ['00123456789', 0x123456789], # leading zeros + ['000abcdef', 0xabcdef], # leading zeros + ['000ABCDEF', 0xabcdef], # leading zeros + ['aaaaaaaaaaaaaaaaffffffffffffffff', 0xaaaaaaaaaaaaaaaaffffffffffffffff], # 128 bits + ['ffffffffffffffff', 0xffffffffffffffff], # 64 bits ].each do |value, expected| - context value.inspect do - subject(:subject) { described_class.value_to_number(value, base: 16) } - - it { is_expected.to be == expected } + context "when given #{value.inspect}" do + it { expect(described_class.parse_hex_id(value)).to eq(expected) } end end end diff --git a/spec/datadog/tracing/distributed/trace_context_spec.rb b/spec/datadog/tracing/distributed/trace_context_spec.rb index f96255edb75..242db4f5a53 100644 --- a/spec/datadog/tracing/distributed/trace_context_spec.rb +++ b/spec/datadog/tracing/distributed/trace_context_spec.rb @@ -28,13 +28,6 @@ it { expect(traceparent).to eq('00-00000000000000000000000000c0ffee-0000000000000bee-00') } - context 'with trace_distributed_id' do - let(:options) { { trace_distributed_id: 0xACE00000000000000000000000C0FFEE } } - it 'prioritizes the original trace_distributed_id' do - expect(traceparent).to eq('00-ace00000000000000000000000c0ffee-0000000000000bee-00') - end - end - context 'with trace_flags' do context 'with a dropped trace' do let(:options) { { trace_flags: 0xFF, trace_sampling_priority: -1 } } @@ -133,6 +126,21 @@ end end + context 'with 128 bit trace id' do + let(:digest) do + Datadog::Tracing::TraceDigest.new( + trace_id: 0xaaaaaaaaaaaaaaaaffffffffffffffff, + span_id: 0xbbbbbbbbbbbbbbbb + ) + end + + it do + inject! + + expect(traceparent).to eq('00-aaaaaaaaaaaaaaaaffffffffffffffff-bbbbbbbbbbbbbbbb-00') + end + end + context 'with trace_distributed_tags' do let(:digest) do Datadog::Tracing::TraceDigest.new( @@ -311,6 +319,44 @@ it { is_expected.to be nil } end + context 'with traceparent and distributed_tag `t.tid` in tracestate' do + let(:data) do + { + prepare_key['traceparent'] => '00-aaaaaaaaaaaaaaaaffffffffffffffff-bbbbbbbbbbbbbbbb-00', + prepare_key['tracestate'] => 'dd=t.tid:cccccccccccccccc' + } + end + + it { expect(digest.trace_id).to eq(0xaaaaaaaaaaaaaaaaffffffffffffffff) } + it { expect(digest.span_id).to eq(0xbbbbbbbbbbbbbbbb) } + it { expect(digest.trace_distributed_tags).to be_nil } + end + + context 'with traceparent without tracestate' do + let(:data) do + { + prepare_key['traceparent'] => '00-aaaaaaaaaaaaaaaaffffffffffffffff-bbbbbbbbbbbbbbbb-00', + } + end + + it { expect(digest.trace_id).to eq(0xaaaaaaaaaaaaaaaaffffffffffffffff) } + it { expect(digest.span_id).to eq(0xbbbbbbbbbbbbbbbb) } + it { expect(digest.trace_distributed_tags).to be_nil } + end + + context 'with traceparent and with empty tracestate' do + let(:data) do + { + prepare_key['traceparent'] => '00-aaaaaaaaaaaaaaaaffffffffffffffff-bbbbbbbbbbbbbbbb-00', + prepare_key['tracestate'] => '' + } + end + + it { expect(digest.trace_id).to eq(0xaaaaaaaaaaaaaaaaffffffffffffffff) } + it { expect(digest.span_id).to eq(0xbbbbbbbbbbbbbbbb) } + it { expect(digest.trace_distributed_tags).to be_nil } + end + context 'with valid trace_id and parent_id' do it { expect(digest.trace_id).to eq(0xC0FFEE) } it { expect(digest.span_id).to eq(0xBEE) } @@ -320,8 +366,7 @@ context 'and trace_id larger than 64 bits' do let(:trace_id) { 'ace00000000000000000000000c0ffee' } - it { expect(digest.trace_id).to eq(0xC0FFEE) } - it { expect(digest.trace_distributed_id).to eq(0xACE00000000000000000000000C0FFEE) } + it { expect(digest.trace_id).to eq(0xACE00000000000000000000000C0FFEE) } end context 'with sampling priority' do diff --git a/spec/datadog/tracing/trace_operation_spec.rb b/spec/datadog/tracing/trace_operation_spec.rb index 3972db8b60e..7fc73937fa8 100644 --- a/spec/datadog/tracing/trace_operation_spec.rb +++ b/spec/datadog/tracing/trace_operation_spec.rb @@ -3,6 +3,7 @@ require 'time' require 'datadog/core' +require 'datadog/core/utils/time' require 'datadog/core/environment/identity' require 'datadog/tracing/sampling/ext' @@ -86,6 +87,40 @@ it do expect(trace_op.send(:metrics)).to eq({}) end + + context 'when 128 bit trace id generation enabled' do + before do + allow(Datadog.configuration.tracing).to receive(:trace_id_128_bit_generation_enabled).and_return(true) + end + + it '64 bits trace_id' do + expect(trace_op.id.bit_length).to be <= 128 + end + + it do + allow(Datadog::Core::Utils::Time).to receive(:now).and_return(0xffffffff) + allow(Datadog::Tracing::Utils).to receive(:next_id).and_return(0xaaaaaaaaaaaaaaaa) + + expect(trace_op.id).to eq(0xffffffff00000000aaaaaaaaaaaaaaaa) + end + end + + context 'when 128 bit trace id generation disabled' do + before do + allow(Datadog.configuration.tracing).to receive(:trace_id_128_bit_generation_enabled).and_return(false) + end + + it '64 bits trace_id' do + expect(trace_op.id.bit_length).to be <= 64 + end + + it do + allow(Datadog::Tracing::Utils).to receive(:next_id).and_return(0xffffffffffffffff) + + expect(trace_op.id).to eq(0xffffffffffffffff) + expect(trace_op).not_to have_tag('_dd.p.tid') + end + end end context 'given' do diff --git a/spec/datadog/tracing/trace_segment_spec.rb b/spec/datadog/tracing/trace_segment_spec.rb index bb835b47e41..fb9f3d56456 100644 --- a/spec/datadog/tracing/trace_segment_spec.rb +++ b/spec/datadog/tracing/trace_segment_spec.rb @@ -4,15 +4,16 @@ require 'datadog/tracing/sampling/ext' require 'datadog/tracing/span' require 'datadog/tracing/trace_segment' +require 'datadog/tracing/utils' RSpec.describe Datadog::Tracing::TraceSegment do - subject(:trace_segment) { described_class.new(spans, **options) } + let(:trace_id) { Datadog::Tracing::Utils::TraceId.next_id } let(:options) { {} } - let(:spans) do Array.new(3) do |i| span = Datadog::Tracing::Span.new( 'job.work', + trace_id: trace_id, resource: 'generate_report', service: 'jobs-worker', type: 'worker' @@ -23,6 +24,7 @@ span end end + subject(:trace_segment) { described_class.new(spans, **options.merge(id: trace_id)) } describe '::new' do context 'by default' do @@ -30,7 +32,7 @@ is_expected.to have_attributes( agent_sample_rate: nil, hostname: nil, - id: nil, + id: trace_id, lang: nil, name: nil, origin: nil, @@ -341,4 +343,22 @@ it { is_expected.to be false } end end + + describe '#high_order_tid' do + context 'when given 64 bits id' do + let(:trace_id) { 0xffffffffffffffff } + + it do + expect(trace_segment.high_order_tid).to eq(nil) + end + end + + context 'when given 128 bits id' do + let(:trace_id) { 0xaaaaaaaaaaaaaaaaffffffffffffffff } + + it do + expect(trace_segment.high_order_tid).to eq('aaaaaaaaaaaaaaaa') + end + end + end end diff --git a/spec/datadog/tracing/utils_spec.rb b/spec/datadog/tracing/utils_spec.rb index f977f6cc913..6c2aea5dadc 100644 --- a/spec/datadog/tracing/utils_spec.rb +++ b/spec/datadog/tracing/utils_spec.rb @@ -31,3 +31,76 @@ end end end + +RSpec.describe Datadog::Tracing::Utils::TraceId do + describe '.to_low_order' do + context 'when given <= 64 bit' do + [ + 0xaaaaaaaaaaaaaaaa, + 0xffffffffffffffff, + 0, + ].each do |input| + it 'returns itself' do + expect(described_class.to_low_order(input)).to eq(input) + end + end + end + + context 'when given > 64 bit' do + { + 0xffffffffffffffffaaaaaaaaaaaaaaaa => 0xaaaaaaaaaaaaaaaa, + 0xaaaaaaaaaaaaaaaaffffffffffffffff => 0xffffffffffffffff, + }.each do |input, result| + context "when given `0x#{input.to_s(16)}`" do + it "returns the lower order 64 bits `0x#{result.to_s(16)}`" do + expect(described_class.to_low_order(input)).to eq(result) + end + end + end + end + end + + describe '.to_high_order' do + context 'when given <= 64 bit' do + [ + 0xaaaaaaaaaaaaaaaa, + 0xffffffffffffffff, + 0, + ].each do |input| + it 'returns 0' do + expect(described_class.to_high_order(input)).to eq(0) + end + end + end + + context 'when given > 64 bit' do + { + 0xffffffffffffffffaaaaaaaaaaaaaaaa => 0xffffffffffffffff, + 0xaaaaaaaaaaaaaaaaffffffffffffffff => 0xaaaaaaaaaaaaaaaa, + }.each do |input, result| + context "when given `0x#{input.to_s(16)}`" do + it "returns the lower order 64 bits `0x#{result.to_s(16)}`" do + expect(described_class.to_high_order(input)).to eq(result) + end + end + end + end + end + + describe '.concatenate' do + { + [0xaaaaaaaaaaaaaaaa, 0xffffffffffffffff] => 0xaaaaaaaaaaaaaaaaffffffffffffffff, + [0xffffffffffffffff, 0xaaaaaaaaaaaaaaaa] => 0xffffffffffffffffaaaaaaaaaaaaaaaa, + [0x00000000aaaaaaaa, 0xffffffffffffffff] => 0x00000000aaaaaaaaffffffffffffffff, + [0xaaaaaaaaaaaaaaaa, 0xffffffff] => 0xaaaaaaaaaaaaaaaa00000000ffffffff, + [0, 0xffffffffffffffff] => 0xffffffffffffffff, + [0xaaaaaaaaaaaaaaaa, 0] => 0xaaaaaaaaaaaaaaaa0000000000000000, + }.each do |(high_order, low_order), result| + context "when given `#{high_order}` and `#{low_order}`" do + it "returns `0x#{result.to_s(16)}`" do + expect(described_class.concatenate(high_order, low_order)).to eq(result) + end + end + end + end +end diff --git a/spec/ddtrace/transport/serializable_trace_spec.rb b/spec/ddtrace/transport/serializable_trace_spec.rb index 0173806846b..b60cf977544 100644 --- a/spec/ddtrace/transport/serializable_trace_spec.rb +++ b/spec/ddtrace/transport/serializable_trace_spec.rb @@ -41,6 +41,35 @@ is_expected.to eq(original_spans) end end + + context 'when given trace_id' do + subject(:unpacked_trace) { MessagePack.unpack(to_msgpack) } + + let(:spans) do + Array.new(3) do |_i| + Datadog::Tracing::Span.new( + 'dummy', + trace_id: trace_id + ) + end + end + + context 'when given 64 bits trace id' do + let(:trace_id) { 0xffffffffffffffff } + + it 'serializes 64 bits trace id entirely' do + expect(unpacked_trace.map { |s| s['trace_id'] }).to all(eq(0xffffffffffffffff)) + end + end + + context 'when given 128 bits trace id' do + let(:trace_id) { 0xaaaaaaaaaaaaaaaaffffffffffffffff } + + it 'serializes the low order 64 bits trace id' do + expect(unpacked_trace.map { |s| s['trace_id'] }).to all(eq(0xffffffffffffffff)) + end + end + end end describe '#to_json' do @@ -59,5 +88,34 @@ is_expected.to eq(original_spans) end end + + context 'when given trace_id' do + subject(:unpacked_trace) { JSON(to_json) } + + let(:spans) do + Array.new(3) do |_i| + Datadog::Tracing::Span.new( + 'dummy', + trace_id: trace_id + ) + end + end + + context 'when given 64 bits trace id' do + let(:trace_id) { 0xffffffffffffffff } + + it 'serializes 64 bits trace id entirely' do + expect(unpacked_trace.map { |s| s['trace_id'] }).to all(eq(0xffffffffffffffff)) + end + end + + context 'when given 128 bits trace id' do + let(:trace_id) { 0xaaaaaaaaaaaaaaaaffffffffffffffff } + + it 'serializes the low order 64 bits trace id' do + expect(unpacked_trace.map { |s| s['trace_id'] }).to all(eq(0xffffffffffffffff)) + end + end + end end end diff --git a/spec/ddtrace/transport/trace_formatter_spec.rb b/spec/ddtrace/transport/trace_formatter_spec.rb index 0d240df15f8..addf9a38e01 100644 --- a/spec/ddtrace/transport/trace_formatter_spec.rb +++ b/spec/ddtrace/transport/trace_formatter_spec.rb @@ -12,7 +12,8 @@ RSpec.describe Datadog::Transport::TraceFormatter do subject(:trace_formatter) { described_class.new(trace) } - let(:trace_options) { {} } + let(:trace_options) { { id: trace_id } } + let(:trace_id) { Datadog::Tracing::Utils::TraceId.next_id } shared_context 'trace metadata' do let(:trace_tags) do @@ -21,6 +22,7 @@ let(:trace_options) do { + id: trace_id, resource: resource, agent_sample_rate: agent_sample_rate, hostname: hostname, @@ -57,6 +59,7 @@ 'foo' => 'bar', 'baz' => 42, '_dd.p.dm' => '-1', + '_dd.p.tid' => 'aaaaaaaaaaaaaaaa' } end end @@ -164,18 +167,26 @@ context 'meta' do it 'sets root span tags from trace tags' do format! - expect(root_span.meta).to include({ 'foo' => 'bar', '_dd.p.dm' => '-1' }) + expect(root_span.meta).to include( + { + 'foo' => 'bar', + '_dd.p.dm' => '-1', + '_dd.p.tid' => 'aaaaaaaaaaaaaaaa' + } + ) end end end shared_examples 'root span without generic tags' do context 'metrics' do - it { expect(root_span.metrics).to_not include({ 'baz' => 42 }) } + it { expect(root_span.metrics).to_not include('baz') } end context 'meta' do - it { expect(root_span.meta).to_not include({ 'foo' => 'bar', '_dd.p.dm' => '-1' }) } + it { expect(root_span.meta).to_not include('foo') } + it { expect(root_span.meta).to_not include('_dd.p.dm') } + it { expect(root_span.meta).to_not include('_dd.p.tid') } end end