Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

B3 Header Support #753

Merged
merged 24 commits into from
May 17, 2019
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9847e34
[core] Add support for injecting/extract B3 headers
brettlangdon Apr 26, 2019
2eb7205
Merge remote-tracking branch 'origin/master' into brettlangdon/b3
brettlangdon May 1, 2019
4c78e38
reorganize distributed tracing header parsing
brettlangdon May 8, 2019
4377433
Merge remote-tracking branch 'origin/master' into brettlangdon/b3
brettlangdon May 8, 2019
67151f6
work on headers
brettlangdon May 8, 2019
f85dd30
finish up header parsing tests
brettlangdon May 9, 2019
16d4ad7
fix linting issues
brettlangdon May 9, 2019
1b83c9f
fix existing tests
brettlangdon May 9, 2019
247942b
refactor existing tests
brettlangdon May 9, 2019
a26a247
fix linting
brettlangdon May 9, 2019
635e5b1
Merge remote-tracking branch 'origin/master' into brettlangdon/b3
brettlangdon May 13, 2019
cf79a84
fix base16 trace id truncation and parsing
brettlangdon May 13, 2019
af215b4
ensure we return a context
brettlangdon May 13, 2019
554de34
add more tests
brettlangdon May 13, 2019
fec8101
move clamp_sampling_priority to helper
brettlangdon May 13, 2019
011b6b1
simplify logic for inject/extract
brettlangdon May 13, 2019
ff6b356
Rename to Datadog::DistributedTracing::Headers
brettlangdon May 13, 2019
73cf825
fix rubocop issues
brettlangdon May 13, 2019
f7e9e0f
refactor helper a bit
brettlangdon May 13, 2019
736e6c5
add Datadog.configuration.distributed_tracing
brettlangdon May 13, 2019
cb7b3db
Merge remote-tracking branch 'origin/0.24-dev' into brettlangdon/b3
brettlangdon May 15, 2019
f3d97e9
Merge remote-tracking branch 'origin/0.24-dev' into brettlangdon/b3
brettlangdon May 17, 2019
20255e7
move log into unless block
brettlangdon May 17, 2019
a186876
fix line too long
brettlangdon May 17, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions lib/ddtrace/configuration/settings.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'ddtrace/ext/analytics'
require 'ddtrace/ext/distributed'
require 'ddtrace/ext/runtime'
require 'ddtrace/configuration/options'

Expand All @@ -21,6 +22,24 @@ class Settings
default: -> { env_to_bool(Ext::Runtime::Metrics::ENV_ENABLED, false) },
lazy: true

# Look for all headers by default
option :propagation_extract_style,
delner marked this conversation as resolved.
Show resolved Hide resolved
brettlangdon marked this conversation as resolved.
Show resolved Hide resolved
default: lambda {
env_to_list(Ext::DistributedTracing::PROPAGATION_EXTRACT_STYLE_ENV,
[Ext::DistributedTracing::PROPAGATION_STYLE_DATADOG,
Ext::DistributedTracing::PROPAGATION_STYLE_B3,
Ext::DistributedTracing::PROPAGATION_STYLE_B3_SINGLE_HEADER])
},
lazy: true

# Only inject Datadog headers by default
option :propagation_inject_style,
default: lambda {
env_to_list(Ext::DistributedTracing::PROPAGATION_INJECT_STYLE_ENV,
[Ext::DistributedTracing::PROPAGATION_STYLE_DATADOG])
},
lazy: true

option :tracer, default: Tracer.new

def initialize(options = {})
Expand Down
8 changes: 8 additions & 0 deletions lib/ddtrace/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ def env_to_bool(var, default = nil)
def env_to_float(var, default = nil)
ENV.key?(var) ? ENV[var].to_f : default
end

def env_to_list(var, default = [])
if ENV.key?(var)
ENV[var].split(',').map(&:strip)
else
default
end
end
end
end
end
13 changes: 13 additions & 0 deletions lib/ddtrace/ext/distributed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ module DistributedTracing
HTTP_HEADER_ORIGIN = 'x-datadog-origin'.freeze
ORIGIN_KEY = '_dd.origin'.freeze

# B3 headers used for distributed tracing
B3_HEADER_TRACE_ID = 'x-b3-traceid'.freeze
B3_HEADER_SPAN_ID = 'x-b3-spanid'.freeze
B3_HEADER_SAMPLED = 'x-b3-sampled'.freeze
B3_HEADER_SINGLE = 'b3'.freeze

# Distributed tracing propagation options
PROPAGATION_STYLE_DATADOG = 'Datadog'.freeze
PROPAGATION_STYLE_B3 = 'B3'.freeze
PROPAGATION_STYLE_B3_SINGLE_HEADER = 'B3 single header'.freeze
PROPAGATION_INJECT_STYLE_ENV = 'DD_PROPAGATION_INJECT_STYLE'.freeze
PROPAGATION_EXTRACT_STYLE_ENV = 'DD_PROPAGATION_EXTRACT_STYLE'.freeze

# gRPC metadata keys for distributed tracing. https://github.com/grpc/grpc-go/blob/v1.10.x/Documentation/grpc-metadata.md
GRPC_METADATA_TRACE_ID = 'x-datadog-trace-id'.freeze
GRPC_METADATA_PARENT_ID = 'x-datadog-parent-id'.freeze
Expand Down
70 changes: 0 additions & 70 deletions lib/ddtrace/propagation/distributed_headers.rb

This file was deleted.

46 changes: 46 additions & 0 deletions lib/ddtrace/propagation/distributed_headers/b3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require 'ddtrace/ext/distributed'
require 'ddtrace/propagation/distributed_headers/base'
require 'ddtrace/propagation/distributed_headers/headers'

module Datadog
module DistributedHeaders
brettlangdon marked this conversation as resolved.
Show resolved Hide resolved
# B3 provides helpers to inject or extract headers for B3 style headers
class B3
brettlangdon marked this conversation as resolved.
Show resolved Hide resolved
include Ext::DistributedTracing

class << self
include Base
brettlangdon marked this conversation as resolved.
Show resolved Hide resolved
end

def self.inject!(context, env)
return if context.nil?

# DEV: We need these to be hex encoded
env[B3_HEADER_TRACE_ID] = context.trace_id.to_s(16)
env[B3_HEADER_SPAN_ID] = context.span_id.to_s(16)

unless context.sampling_priority.nil?
sampling_priority = clamp_sampling_priority(context.sampling_priority)
env[B3_HEADER_SAMPLED] = sampling_priority.to_s
end
end

def self.extract(env)
# Extract values from headers
# DEV: B3 doesn't have "origin"
headers = Headers.new(env)
trace_id = headers.id(B3_HEADER_TRACE_ID, 16)
span_id = headers.id(B3_HEADER_SPAN_ID, 16)
# We don't need to try and convert sampled since B3 supports 0/1 (AUTO_REJECT/AUTO_KEEP)
sampling_priority = headers.number(B3_HEADER_SAMPLED)

# Return early if this propagation is not valid
return unless trace_id && span_id

::Datadog::Context.new(trace_id: trace_id,
span_id: span_id,
sampling_priority: sampling_priority)
end
end
end
end
57 changes: 57 additions & 0 deletions lib/ddtrace/propagation/distributed_headers/b3_single.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require 'ddtrace/ext/distributed'
require 'ddtrace/propagation/distributed_headers/base'
require 'ddtrace/propagation/distributed_headers/headers'

module Datadog
module DistributedHeaders
# B3Single provides helpers to inject or extract headers for B3 single header style headers
module B3Single
include Ext::DistributedTracing
class << self
include Base
end

def self.inject!(context, env)
return if context.nil?

# Header format:
# b3: {TraceId}-{SpanId}-{SamplingState}-{ParentSpanId}
# https://github.com/apache/incubator-zipkin-b3-propagation/tree/7c6e9f14d6627832bd80baa87ac7dabee7be23cf#single-header
# DEV: `{SamplingState}` and `{ParentSpanId`}` are optional

# DEV: We need these to be hex encoded
header = "#{context.trace_id.to_s(16)}-#{context.span_id.to_s(16)}"

unless context.sampling_priority.nil?
sampling_priority = clamp_sampling_priority(context.sampling_priority)
header += "-#{sampling_priority}"
end

env[B3_HEADER_SINGLE] = header
end

def self.extract(env)
# Header format:
# b3: {TraceId}-{SpanId}-{SamplingState}-{ParentSpanId}
# https://github.com/apache/incubator-zipkin-b3-propagation/tree/7c6e9f14d6627832bd80baa87ac7dabee7be23cf#single-header
# DEV: `{SamplingState}` and `{ParentSpanId`}` are optional

headers = Headers.new(env)
value = headers.header(B3_HEADER_SINGLE)
return if value.nil?

parts = value.split('-')
trace_id = headers.value_to_id(parts[0], 16) unless parts.empty?
span_id = headers.value_to_id(parts[1], 16) if parts.length > 1
sampling_priority = headers.value_to_number(parts[2]) if parts.length > 2

# Return early if this propagation is not valid
return unless trace_id && span_id

::Datadog::Context.new(trace_id: trace_id,
span_id: span_id,
sampling_priority: sampling_priority)
end
end
end
end
21 changes: 21 additions & 0 deletions lib/ddtrace/propagation/distributed_headers/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'ddtrace/configuration'
require 'ddtrace/span'
require 'ddtrace/ext/priority'

module Datadog
module DistributedHeaders
# Base provides common methods for distributed header helper classes
module Base
brettlangdon marked this conversation as resolved.
Show resolved Hide resolved
def clamp_sampling_priority(sampling_priority)
# B3 doesn't have our -1 (USER_REJECT) and 2 (USER_KEEP) priorities so convert to acceptable 0/1
if sampling_priority < 0
sampling_priority = Ext::Priority::AUTO_REJECT
elsif sampling_priority > 1
sampling_priority = Ext::Priority::AUTO_KEEP
end

sampling_priority
end
end
end
end
44 changes: 44 additions & 0 deletions lib/ddtrace/propagation/distributed_headers/datadog.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'ddtrace/ext/distributed'
require 'ddtrace/propagation/distributed_headers/base'
require 'ddtrace/propagation/distributed_headers/headers'

module Datadog
module DistributedHeaders
# Datadog provides helpers to inject or extract headers for Datadog style headers
class Datadog
include Ext::DistributedTracing
class << self
include Base
end

def self.inject!(context, env)
return if context.nil?

env[HTTP_HEADER_TRACE_ID] = context.trace_id.to_s
env[HTTP_HEADER_PARENT_ID] = context.span_id.to_s
env[HTTP_HEADER_SAMPLING_PRIORITY] = context.sampling_priority.to_s unless context.sampling_priority.nil?
env[HTTP_HEADER_ORIGIN] = context.origin.to_s unless context.origin.nil?
end

def self.extract(env)
# Extract values from headers
headers = Headers.new(env)
trace_id = headers.id(HTTP_HEADER_TRACE_ID)
parent_id = headers.id(HTTP_HEADER_PARENT_ID)
origin = headers.header(HTTP_HEADER_ORIGIN)
sampling_priority = headers.number(HTTP_HEADER_SAMPLING_PRIORITY)

# Return early if this propagation is not valid
# DEV: To be valid we need to have a trace id and a parent id or when it is a synthetics trace, just the trace id
# DEV: `DistributedHeaders#id` will not return 0
return unless (trace_id && parent_id) || (origin == 'synthetics' && trace_id)

# Return new context
::Datadog::Context.new(trace_id: trace_id,
span_id: parent_id,
origin: origin,
sampling_priority: sampling_priority)
end
end
end
end
82 changes: 82 additions & 0 deletions lib/ddtrace/propagation/distributed_headers/headers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require 'ddtrace/configuration'
require 'ddtrace/span'
require 'ddtrace/ext/distributed'

module Datadog
module DistributedHeaders
# Headers provides easy access and validation methods for Rack headers
class Headers
include Ext::DistributedTracing

def initialize(env)
@env = env
end

def header(name)
rack_header = "http-#{name}".upcase!.tr('-', '_')

hdr = @env[rack_header]

# Only return the value if it is not an empty string
hdr if hdr != ''
end

def id(hdr, base = 10)
value_to_id(header(hdr), base)
end

def value_to_id(value, base = 10)
id = value_to_number(value, 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 > Span::EXTERNAL_MAX_ID
id < 0 ? id + (2**64) : id
end

def number(hdr, base = 10)
value_to_number(header(hdr), base)
end

def value_to_number(value, base = 10)
# It's important to make a difference between no header,
# and a header defined to zero.
return if value.nil?

# Be sure we have a string
value = value.to_s

if base == 16
# 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

# 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 = value.sub(/^0*(?=(0$)|[^0])/, '')
end

# Convert header to an integer
# DEV: Ruby `.to_i` will return `0` if a number could not be parsed
num = value.to_i(base)

# 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

num
end
end
end
end
Loading