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

Add support for reading B3 single header #165

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,6 @@
# 0.40.0
* Add support for reading B3 single header.

# 0.39.2
* Make Faraday 0.13 the minimum requirement.

Expand Down
1 change: 1 addition & 0 deletions lib/zipkin-tracer.rb
Expand Up @@ -5,6 +5,7 @@
require 'zipkin-tracer/trace_container'
require 'zipkin-tracer/trace_generator'
require 'zipkin-tracer/trace_wrapper'
require 'zipkin-tracer/zipkin_b3_single_header_format'

begin
require 'faraday'
Expand Down
31 changes: 21 additions & 10 deletions lib/zipkin-tracer/rack/zipkin_env.rb
Expand Up @@ -10,33 +10,44 @@ def initialize(env, config)
end

def trace_id(default_flags = Trace::Flags::EMPTY)
trace_id, span_id, parent_span_id, shared = retrieve_or_generate_ids
sampled = sampled_header_value(@env['HTTP_X_B3_SAMPLED'])
flags = (@env['HTTP_X_B3_FLAGS'] || default_flags).to_i
trace_id, span_id, parent_span_id, sampled, flags, shared = retrieve_or_generate_ids
sampled = sampled_header_value(sampled)
flags = (flags || default_flags).to_i
Trace::TraceId.new(trace_id, parent_span_id, span_id, sampled, flags, shared)
end

def called_with_zipkin_b3_single_header?
@called_with_zipkin_b3_single_header ||= @env.key?(B3_SINGLE_HEADER)
end

def called_with_zipkin_headers?
@called_with_zipkin_headers ||= B3_REQUIRED_HEADERS.all? { |key| @env.key?(key) }
end

private

B3_REQUIRED_HEADERS = %w(HTTP_X_B3_TRACEID HTTP_X_B3_SPANID).freeze
B3_OPT_HEADERS = %w(HTTP_X_B3_PARENTSPANID HTTP_X_B3_SAMPLED HTTP_X_B3_FLAGS).freeze
B3_SINGLE_HEADER = 'HTTP_B3'.freeze
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

easier to quote, ain't it :)

B3_REQUIRED_HEADERS = %w[HTTP_X_B3_TRACEID HTTP_X_B3_SPANID].freeze
B3_OPT_HEADERS = %w[HTTP_X_B3_PARENTSPANID HTTP_X_B3_SAMPLED HTTP_X_B3_FLAGS].freeze

def retrieve_or_generate_ids
if called_with_zipkin_headers?
trace_id, span_id = @env.values_at(*B3_REQUIRED_HEADERS)
parent_span_id = @env['HTTP_X_B3_PARENTSPANID']
if called_with_zipkin_b3_single_header?
trace_id, span_id, parent_span_id, sampled, flags =
B3SingleHeaderFormat.parse_from_header(@env[B3_SINGLE_HEADER])
shared = true
else
elsif called_with_zipkin_headers?
trace_id, span_id, parent_span_id, sampled, flags = @env.values_at(*B3_REQUIRED_HEADERS, *B3_OPT_HEADERS)
shared = true
end

unless trace_id
span_id = TraceGenerator.new.generate_id
trace_id = TraceGenerator.new.generate_id_from_span_id(span_id)
parent_span_id = nil
shared = false
end
[trace_id, span_id, parent_span_id, shared]

[trace_id, span_id, parent_span_id, sampled, flags, shared]
end

def new_sampled_header_value(sampled)
Expand Down
2 changes: 1 addition & 1 deletion lib/zipkin-tracer/version.rb
@@ -1,3 +1,3 @@
module ZipkinTracer
VERSION = '0.39.2'.freeze
VERSION = '0.40.0'.freeze
end
28 changes: 28 additions & 0 deletions lib/zipkin-tracer/zipkin_b3_single_header_format.rb
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module ZipkinTracer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about adding the magic comment about frozen strings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in f8d6a89

# This format corresponds to the propagation key "b3" (or "B3").
# b3: {x-b3-traceid}-{x-b3-spanid}-{if x-b3-flags 'd' else x-b3-sampled}-{x-b3-parentspanid}
# For details, see: https://github.com/openzipkin/b3-propagation
class B3SingleHeaderFormat
def self.parse_from_header(b3_single_header)
if b3_single_header.size == 1
flag = b3_single_header
else
trace_id, span_id, flag, parent_span_id = b3_single_header.split('-')
end
[trace_id, span_id, parent_span_id, parse_sampled(flag), parse_flags(flag)]
end

def self.parse_sampled(flag)
case flag
when '1', '0'
flag
end
end

def self.parse_flags(flag)
flag == 'd' ? Trace::Flags::DEBUG : Trace::Flags::EMPTY
end
end
end
86 changes: 86 additions & 0 deletions spec/lib/rack/zipkin_env_spec.rb
Expand Up @@ -213,4 +213,90 @@ def mock_env(params = {}, path = '/')
end
end
end

context 'with zipkin b3 single header' do
let(:id) { "e457b5a2e4d86bd1" }
let(:zipkin_headers) { { 'HTTP_B3' => b3_single_header } }
let(:env) { mock_env(zipkin_headers) }

context 'not yet sampled root span' do
let(:b3_single_header) { "#{id}-#{id}" }

it '#called_with_zipkin_b3_single_header? returns true' do
expect(zipkin_env.called_with_zipkin_b3_single_header?).to eq(true)
end

it 'shared is true' do
expect(zipkin_env.trace_id.shared).to eq(true)
end

it 'sampling information is set' do
# Because sample rate == 1
expect(zipkin_env.trace_id.sampled?).to eq(true)
end

context 'parent_id is not provided' do
it 'uses the trace_id and span_id' do
trace_id = zipkin_env.trace_id
expect(trace_id.trace_id.to_s).to eq(id)
expect(trace_id.span_id.to_s).to eq(id)
end

it 'parent_id is empty' do
expect(zipkin_env.trace_id.parent_id).to eq(nil)
end
end
end

context 'all information is provided' do
let(:parent_id) { "05e3ac9a4f6e3b90" }
let(:b3_single_header) { "#{id}-#{id}-1-#{parent_id}" }

it 'uses the trace_id and span_id' do
trace_id = zipkin_env.trace_id
expect(trace_id.trace_id.to_s).to eq(id)
expect(trace_id.span_id.to_s).to eq(id)
end

it 'uses the parent_id' do
expect(zipkin_env.trace_id.parent_id.to_s).to eq(parent_id.to_s)
end

it 'uses the sampling information' do
expect(zipkin_env.trace_id.sampled?).to eq(true)
end

it 'uses the flags' do
expect(zipkin_env.trace_id.flags.to_i).to eq(0)
end
end

context 'debug flag only' do
let(:b3_single_header) { "d" }

it 'generates a trace_id and a span_id' do
trace_id = zipkin_env.trace_id
expect(trace_id.trace_id).not_to eq(nil)
expect(trace_id.span_id).not_to eq(nil)
expect(trace_id.span_id.to_s).to eq(trace_id.trace_id.to_s)
end

it 'parent_id is nil' do
expect(zipkin_env.trace_id.parent_id).to eq(nil)
end

it 'flags is debug' do
expect(zipkin_env.trace_id.flags).to eq(Trace::Flags::DEBUG)
end

it 'sampling information is set' do
# Because sample rate == 1
expect(zipkin_env.trace_id.sampled?).to eq(true)
end

it 'shared is false' do
expect(zipkin_env.trace_id.shared).to eq(false)
end
end
end
end
73 changes: 73 additions & 0 deletions spec/lib/zipkin_b3_single_header_format_spec.rb
@@ -0,0 +1,73 @@
require 'spec_helper'

describe ZipkinTracer::B3SingleHeaderFormat do
let(:b3_single_header_format) { described_class.parse_from_header(b3_single_header) }

context 'child span' do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add some malformed tests? Ex in brave we use

"not-a-tumor"
"b970dafd-0d95-40aa-95d8-1d8725aebe40"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adriancole In ruby client, we are doing malformed check in another class:

class TraceId128Bit < SpanId
HEX_REGEX_16 = /^[a-f0-9]{16}$/i
HEX_REGEX_32 = /^[a-f0-9]{32}$/i
MAX_SIGNED_I128 = (2 ** 128 / 2) -1
MASK = (2 ** 128) - 1
def self.from_value(v)
if v.is_a?(String) && v =~ HEX_REGEX_16
SpanId.new(v.hex)
elsif v.is_a?(String) && v =~ HEX_REGEX_32
new(v.hex)
elsif v.is_a?(Numeric)
new(v)
elsif v.is_a?(SpanId)
v
end
end

I think we don't need to have malformed tests here but what do you think?

let(:b3_single_header) { '80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90' }

it 'has all fields' do
expect(b3_single_header_format.to_a).to eq(
['80f198ee56343ba864fe8b2a57d3eff7', 'e457b5a2e4d86bd1', '05e3ac9a4f6e3b90', '1', 0]
)
end
end

context 'sampled root span' do
let(:b3_single_header) { '80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1' }

it 'does not have parent_span_id' do
expect(b3_single_header_format.to_a).to eq(['80f198ee56343ba864fe8b2a57d3eff7', 'e457b5a2e4d86bd1', nil, '1', 0])
end
end

context 'not yet sampled root span' do
let(:b3_single_header) { '80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1' }

it 'does not have parent_span_id and sampled' do
expect(b3_single_header_format.to_a).to eq(['80f198ee56343ba864fe8b2a57d3eff7', 'e457b5a2e4d86bd1', nil, nil, 0])
end
end

context 'debug RPC child span' do
let(:b3_single_header) { '80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-d-05e3ac9a4f6e3b90' }

it 'has debug flag' do
expect(b3_single_header_format.to_a).to eq(
['80f198ee56343ba864fe8b2a57d3eff7', 'e457b5a2e4d86bd1', '05e3ac9a4f6e3b90', nil, 1]
)
end
end

context 'do not sample flag only' do
let(:b3_single_header) { '0' }

it 'has do not sample flag only' do
expect(b3_single_header_format.to_a).to eq([nil, nil, nil, '0', 0])
end
end

context 'sampled flag only' do
let(:b3_single_header) { '1' }

it 'has sampled flag only' do
expect(b3_single_header_format.to_a).to eq([nil, nil, nil, '1', 0])
end
end

context 'debug flag only' do
let(:b3_single_header) { 'd' }

it 'has debug flag only' do
expect(b3_single_header_format.to_a).to eq([nil, nil, nil, nil, 1])
end
end

context 'unknown flag only' do
let(:b3_single_header) { 'u' }

it 'has nothing' do
expect(b3_single_header_format.to_a).to eq([nil, nil, nil, nil, 0])
end
end
end