From 38b6701e7a8dc00d3c65183b3564db6ac5151230 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Thu, 24 Oct 2019 14:27:25 -0700 Subject: [PATCH] B3 context propagation (#86) * Extract LS context propagation into configurable propagator class * Extract context prop test cases to LightStepPropagator spec * Make LightStepPropagator easier to subclass Constant lookup will search in Module.nesting before looking up the inheritance chain, which makes constants hard to override when subclassing. This commit prefixes constants in the LightStep propagator with self.class in order to steer lookup to the inheritance chain. * Introduce B3 Propagator This commit introduces a B3 Propagator where keys are properly injected and extracted under the proper names. Special handling of the trace id and sampled flag will come in subsequent commits. * Propagate 16 byte trace ids for B3; use 16 byte ids internally * Honor and propagate sampled flag for B3 * Clean up tests * Specify default propagator for Tracer#configure * Test against currently maintained Rubies This updates our build matrix to test against Ruby versions that are still under maintenance by the Ruby core team. * The mutating tr! and downcase! methods are not chainable; don't chain them * Make it easier to specify a propgator * Update changelog for B3 --- .circleci/config.yml | 41 ++-- CHANGELOG.md | 4 + circle.yml | 3 - lib/lightstep/propagation.rb | 25 ++ lib/lightstep/propagation/b3_propagator.rb | 26 +++ .../propagation/lightstep_propagator.rb | 127 ++++++++++ lib/lightstep/span_context.rb | 25 +- lib/lightstep/tracer.rb | 123 +++------- spec/helpers/rack_helpers.rb | 12 + .../propagation/b3_propagator_spec.rb | 216 ++++++++++++++++++ .../propagation/lightstep_propagator_spec.rb | 184 +++++++++++++++ spec/lightstep/propagation_spec.rb | 25 ++ spec/spec_helper.rb | 2 + 13 files changed, 692 insertions(+), 121 deletions(-) delete mode 100644 circle.yml create mode 100644 lib/lightstep/propagation.rb create mode 100644 lib/lightstep/propagation/b3_propagator.rb create mode 100644 lib/lightstep/propagation/lightstep_propagator.rb create mode 100644 spec/helpers/rack_helpers.rb create mode 100644 spec/lightstep/propagation/b3_propagator_spec.rb create mode 100644 spec/lightstep/propagation/lightstep_propagator_spec.rb create mode 100644 spec/lightstep/propagation_spec.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index 44712e4..2aec297 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,31 +1,32 @@ version: 2 jobs: - test: + test-ruby-24: docker: - - image: circleci/ruby:2 + - image: circleci/ruby:2.4-stretch steps: - checkout - - restore_cache: - keys: - - gem-cache-v2-{{ .Branch }}-{{ checksum "Gemfile.lock" }} - - gem-cache-v2-{{ .Branch }}- - - gem-cache-v2- - - run: - name: "set up environment" - command: | - echo 'export BUNDLE_PATH="$HOME/project/.bundler_cache"' >> $BASH_ENV - source $BASH_ENV - - run: bundle - - save_cache: - paths: - - ~/project/.bundler_cache - key: gem-cache-v2-{{ .Branch }}-{{ checksum "Gemfile.lock" }} - - run: make test + - run: gem install --no-document bundler && bundle install --jobs=3 --retry=3 + - run: bundle exec rake + test-ruby-25: + docker: + - image: circleci/ruby:2.5-stretch + steps: + - checkout + - run: gem install --no-document bundler && bundle install --jobs=3 --retry=3 + - run: bundle exec rake + test-ruby-26: + docker: + - image: circleci/ruby:2.6-stretch + steps: + - checkout + - run: gem install --no-document bundler && bundle install --jobs=3 --retry=3 + - run: bundle exec rake workflows: version: 2 test: jobs: - - test - + - test-ruby-24 + - test-ruby-25 + - test-ruby-26 diff --git a/CHANGELOG.md b/CHANGELOG.md index 43b4931..4ff7864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v0.16.0 +### Added +- The tracer now supports B3 context propagation. Propagation can be set by using the `propagator` keyword argument to `Tracer#configure`. Valid values are `:lightstep` (default), and `:b3`. + ## v0.15.0 ### Added - A Changelog diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 4afa7e9..0000000 --- a/circle.yml +++ /dev/null @@ -1,3 +0,0 @@ -machine: - ruby: - version: 2.2.3 diff --git a/lib/lightstep/propagation.rb b/lib/lightstep/propagation.rb new file mode 100644 index 0000000..cf27302 --- /dev/null +++ b/lib/lightstep/propagation.rb @@ -0,0 +1,25 @@ +#frozen_string_literal: true + +require 'lightstep/propagation/lightstep_propagator' +require 'lightstep/propagation/b3_propagator' + +module LightStep + module Propagation + PROPAGATOR_MAP = { + lightstep: LightStepPropagator, + b3: B3Propagator + } + + class << self + # Constructs a propagator instance from the given propagator name. If the + # name is unknown returns the LightStepPropagator as a default + # + # @param [Symbol, String] propagator_name One of :lightstep or :b3 + # @return [Propagator] + def [](propagator_name) + klass = PROPAGATOR_MAP[propagator_name.to_sym] || LightStepPropagator + klass.new + end + end + end +end diff --git a/lib/lightstep/propagation/b3_propagator.rb b/lib/lightstep/propagation/b3_propagator.rb new file mode 100644 index 0000000..d470c60 --- /dev/null +++ b/lib/lightstep/propagation/b3_propagator.rb @@ -0,0 +1,26 @@ +#frozen_string_literal: true + +module LightStep + module Propagation + class B3Propagator < LightStepPropagator + CARRIER_TRACER_STATE_PREFIX = 'x-b3-' + CARRIER_SPAN_ID = 'x-b3-spanid' + CARRIER_TRACE_ID = 'x-b3-traceid' + CARRIER_SAMPLED = 'x-b3-sampled' + + private + + def trace_id_from_ctx(ctx) + ctx.trace_id16 + end + + def sampled_flag_from_ctx(ctx) + ctx.sampled? ? '1' : '0' + end + + def sampled_flag_from_carrier(carrier) + carrier[self.class::CARRIER_SAMPLED] == '1' ? true : false + end + end + end +end diff --git a/lib/lightstep/propagation/lightstep_propagator.rb b/lib/lightstep/propagation/lightstep_propagator.rb new file mode 100644 index 0000000..8a1f2ab --- /dev/null +++ b/lib/lightstep/propagation/lightstep_propagator.rb @@ -0,0 +1,127 @@ +#frozen_string_literal: true + +module LightStep + module Propagation + class LightStepPropagator + CARRIER_TRACER_STATE_PREFIX = 'ot-tracer-' + CARRIER_SPAN_ID = 'ot-tracer-spanid' + CARRIER_TRACE_ID = 'ot-tracer-traceid' + CARRIER_SAMPLED = 'ot-tracer-sampled' + CARRIER_BAGGAGE_PREFIX = 'ot-baggage-' + + # Inject a SpanContext into the given carrier + # + # @param spancontext [SpanContext] + # @param format [OpenTracing::FORMAT_TEXT_MAP, OpenTracing::FORMAT_BINARY] + # @param carrier [Carrier] A carrier object of the type dictated by the specified `format` + def inject(span_context, format, carrier) + case format + when OpenTracing::FORMAT_TEXT_MAP + inject_to_text_map(span_context, carrier) + when OpenTracing::FORMAT_BINARY + warn 'Binary inject format not yet implemented' + when OpenTracing::FORMAT_RACK + inject_to_rack(span_context, carrier) + else + warn 'Unknown inject format' + end + end + + # Extract a SpanContext from a carrier + # @param format [OpenTracing::FORMAT_TEXT_MAP, OpenTracing::FORMAT_BINARY, OpenTracing::FORMAT_RACK] + # @param carrier [Carrier] A carrier object of the type dictated by the specified `format` + # @return [SpanContext] the extracted SpanContext or nil if none could be found + def extract(format, carrier) + case format + when OpenTracing::FORMAT_TEXT_MAP + extract_from_text_map(carrier) + when OpenTracing::FORMAT_BINARY + warn 'Binary join format not yet implemented' + nil + when OpenTracing::FORMAT_RACK + extract_from_rack(carrier) + else + warn 'Unknown join format' + nil + end + end + + private + + def inject_to_text_map(span_context, carrier) + if trace_id = trace_id_from_ctx(span_context) + carrier[self.class::CARRIER_TRACE_ID] = trace_id + end + carrier[self.class::CARRIER_SPAN_ID] = span_context.id + carrier[self.class::CARRIER_SAMPLED] = sampled_flag_from_ctx(span_context) + + span_context.baggage.each do |key, value| + carrier[self.class::CARRIER_BAGGAGE_PREFIX + key] = value + end + end + + def extract_from_text_map(carrier) + # If the carrier does not have both the span_id and trace_id key + # skip the processing and just return a normal span + if !carrier.has_key?(self.class::CARRIER_SPAN_ID) || !carrier.has_key?(self.class::CARRIER_TRACE_ID) + return nil + end + + baggage = carrier.reduce({}) do |baggage, (key, value)| + if key.start_with?(self.class::CARRIER_BAGGAGE_PREFIX) + plain_key = key.to_s[self.class::CARRIER_BAGGAGE_PREFIX.length..key.to_s.length] + baggage[plain_key] = value + end + baggage + end + + SpanContext.new( + id: carrier[self.class::CARRIER_SPAN_ID], + trace_id: carrier[self.class::CARRIER_TRACE_ID], + sampled: sampled_flag_from_carrier(carrier), + baggage: baggage, + ) + end + + def inject_to_rack(span_context, carrier) + if trace_id = trace_id_from_ctx(span_context) + carrier[self.class::CARRIER_TRACE_ID] = trace_id + end + carrier[self.class::CARRIER_SPAN_ID] = span_context.id + carrier[self.class::CARRIER_SAMPLED] = sampled_flag_from_ctx(span_context) + + span_context.baggage.each do |key, value| + if key =~ /[^A-Za-z0-9\-_]/ + # TODO: log the error internally + next + end + carrier[self.class::CARRIER_BAGGAGE_PREFIX + key] = value + end + end + + def extract_from_rack(env) + extract_from_text_map(env.reduce({}){|memo, (raw_header, value)| + header = raw_header.to_s.gsub(/^HTTP_/, '') + header.tr!('_', '-') + header.downcase! + + memo[header] = value if header.start_with?(self.class::CARRIER_TRACER_STATE_PREFIX, + self.class::CARRIER_BAGGAGE_PREFIX) + memo + }) + end + + def trace_id_from_ctx(ctx) + ctx.trace_id + end + + def sampled_flag_from_ctx(_) + 'true' + end + + def sampled_flag_from_carrier(_) + true + end + end + end +end diff --git a/lib/lightstep/span_context.rb b/lib/lightstep/span_context.rb index 43eafcc..cbbf986 100644 --- a/lib/lightstep/span_context.rb +++ b/lib/lightstep/span_context.rb @@ -1,12 +1,31 @@ +#frozen_string_literal: true + module LightStep # SpanContext holds the data for a span that gets inherited to child spans class SpanContext - attr_reader :id, :trace_id, :baggage + attr_reader :id, :trace_id, :trace_id16, :sampled, :baggage + alias_method :sampled?, :sampled - def initialize(id:, trace_id:, baggage: {}) + ZERO_PADDING = '0' * 16 + + def initialize(id:, trace_id:, sampled: true, baggage: {}) @id = id.freeze - @trace_id = trace_id.freeze + @trace_id16 = pad_id(trace_id).freeze + @trace_id = truncate_id(trace_id).freeze + @sampled = sampled @baggage = baggage.freeze end + + private + + def truncate_id(id) + return id unless id && id.size == 32 + id[16..-1] + end + + def pad_id(id) + return id unless id && id.size == 16 + "#{ZERO_PADDING}#{id}" + end end end diff --git a/lib/lightstep/tracer.rb b/lib/lightstep/tracer.rb index 036def3..0e04528 100644 --- a/lib/lightstep/tracer.rb +++ b/lib/lightstep/tracer.rb @@ -5,6 +5,7 @@ require 'opentracing' require 'lightstep/span' require 'lightstep/reporter' +require 'lightstep/propagation' require 'lightstep/transport/http_json' require 'lightstep/transport/nil' require 'lightstep/transport/callback' @@ -14,6 +15,11 @@ module LightStep class Error < LightStep::Error; end class ConfigurationError < LightStep::Tracer::Error; end + DEFAULT_MAX_LOG_RECORDS = 1000 + MIN_MAX_LOG_RECORDS = 1 + DEFAULT_MAX_SPAN_RECORDS = 1000 + MIN_MAX_SPAN_RECORDS = 1 + attr_reader :access_token, :guid # Initialize a new tracer. Either an access_token or a transport must be @@ -22,10 +28,20 @@ module LightStep # @param access_token [String] The project access token when pushing to LightStep # @param transport [LightStep::Transport] How the data should be transported # @param tags [Hash] Tracer-level tags + # @param propagator [Propagator] Symbol one of :lightstep, :b3 indicating the propgator + # to use # @return LightStep::Tracer # @raise LightStep::ConfigurationError if the group name or access token is not a valid string. - def initialize(component_name:, access_token: nil, transport: nil, tags: {}) - configure(component_name: component_name, access_token: access_token, transport: transport, tags: tags) + def initialize(component_name:, + access_token: nil, + transport: nil, + tags: {}, + propagator: :lightstep) + configure(component_name: component_name, + access_token: access_token, + transport: transport, + tags: tags, + propagator: propagator) end def max_log_records @@ -172,22 +188,14 @@ module LightStep end end + # Inject a SpanContext into the given carrier # # @param spancontext [SpanContext] # @param format [OpenTracing::FORMAT_TEXT_MAP, OpenTracing::FORMAT_BINARY] # @param carrier [Carrier] A carrier object of the type dictated by the specified `format` def inject(span_context, format, carrier) - case format - when OpenTracing::FORMAT_TEXT_MAP - inject_to_text_map(span_context, carrier) - when OpenTracing::FORMAT_BINARY - warn 'Binary inject format not yet implemented' - when OpenTracing::FORMAT_RACK - inject_to_rack(span_context, carrier) - else - warn 'Unknown inject format' - end + @propagator.inject(span_context, format, carrier) end # Extract a SpanContext from a carrier @@ -195,18 +203,7 @@ module LightStep # @param carrier [Carrier] A carrier object of the type dictated by the specified `format` # @return [SpanContext] the extracted SpanContext or nil if none could be found def extract(format, carrier) - case format - when OpenTracing::FORMAT_TEXT_MAP - extract_from_text_map(carrier) - when OpenTracing::FORMAT_BINARY - warn 'Binary join format not yet implemented' - nil - when OpenTracing::FORMAT_RACK - extract_from_rack(carrier) - else - warn 'Unknown join format' - nil - end + @propagator.extract(format, carrier) end # @return true if the tracer is enabled @@ -243,7 +240,11 @@ module LightStep protected - def configure(component_name:, access_token: nil, transport: nil, tags: {}) + def configure(component_name:, + access_token: nil, + transport: nil, tags: {}, + propagator: :lightstep) + raise ConfigurationError, "component_name must be a string" unless component_name.is_a?(String) raise ConfigurationError, "component_name cannot be blank" if component_name.empty? @@ -254,6 +255,8 @@ module LightStep raise ConfigurationError, "you must provide an access token or a transport" if transport.nil? raise ConfigurationError, "#{transport} is not a LightStep transport class" if !(LightStep::Transport::Base === transport) + @propagator = Propagation[propagator] + @guid = LightStep.guid @reporter = LightStep::Reporter.new( @@ -264,75 +267,5 @@ module LightStep tags: tags ) end - - private - - CARRIER_TRACER_STATE_PREFIX = 'ot-tracer-'.freeze - CARRIER_BAGGAGE_PREFIX = 'ot-baggage-'.freeze - - CARRIER_SPAN_ID = (CARRIER_TRACER_STATE_PREFIX + 'spanid').freeze - CARRIER_TRACE_ID = (CARRIER_TRACER_STATE_PREFIX + 'traceid').freeze - CARRIER_SAMPLED = (CARRIER_TRACER_STATE_PREFIX + 'sampled').freeze - - DEFAULT_MAX_LOG_RECORDS = 1000 - MIN_MAX_LOG_RECORDS = 1 - DEFAULT_MAX_SPAN_RECORDS = 1000 - MIN_MAX_SPAN_RECORDS = 1 - - def inject_to_text_map(span_context, carrier) - carrier[CARRIER_SPAN_ID] = span_context.id - carrier[CARRIER_TRACE_ID] = span_context.trace_id unless span_context.trace_id.nil? - carrier[CARRIER_SAMPLED] = 'true' - - span_context.baggage.each do |key, value| - carrier[CARRIER_BAGGAGE_PREFIX + key] = value - end - end - - def extract_from_text_map(carrier) - # If the carrier does not have both the span_id and trace_id key - # skip the processing and just return a normal span - if !carrier.has_key?(CARRIER_SPAN_ID) || !carrier.has_key?(CARRIER_TRACE_ID) - return nil - end - - baggage = carrier.reduce({}) do |baggage, tuple| - key, value = tuple - if key.start_with?(CARRIER_BAGGAGE_PREFIX) - plain_key = key.to_s[CARRIER_BAGGAGE_PREFIX.length..key.to_s.length] - baggage[plain_key] = value - end - baggage - end - SpanContext.new( - id: carrier[CARRIER_SPAN_ID], - trace_id: carrier[CARRIER_TRACE_ID], - baggage: baggage, - ) - end - - def inject_to_rack(span_context, carrier) - carrier[CARRIER_SPAN_ID] = span_context.id - carrier[CARRIER_TRACE_ID] = span_context.trace_id unless span_context.trace_id.nil? - carrier[CARRIER_SAMPLED] = 'true' - - span_context.baggage.each do |key, value| - if key =~ /[^A-Za-z0-9\-_]/ - # TODO: log the error internally - next - end - carrier[CARRIER_BAGGAGE_PREFIX + key] = value - end - end - - def extract_from_rack(env) - extract_from_text_map(env.reduce({}){|memo, tuple| - raw_header, value = tuple - header = raw_header.to_s.gsub(/^HTTP_/, '').tr('_', '-').downcase - - memo[header] = value if header.start_with?(CARRIER_TRACER_STATE_PREFIX, CARRIER_BAGGAGE_PREFIX) - memo - }) - end end end diff --git a/spec/helpers/rack_helpers.rb b/spec/helpers/rack_helpers.rb new file mode 100644 index 0000000..518c8c7 --- /dev/null +++ b/spec/helpers/rack_helpers.rb @@ -0,0 +1,12 @@ +module RackHelpers + def to_rack_env(input_hash) + input_hash.inject({}) do |memo, (k, v)| + memo[to_rack_key(k)] = v + memo + end + end + + def to_rack_key(key) + "HTTP_#{key.gsub("-", "_").upcase!}" + end +end diff --git a/spec/lightstep/propagation/b3_propagator_spec.rb b/spec/lightstep/propagation/b3_propagator_spec.rb new file mode 100644 index 0000000..be0b75d --- /dev/null +++ b/spec/lightstep/propagation/b3_propagator_spec.rb @@ -0,0 +1,216 @@ +require 'spec_helper' + +describe LightStep::Propagation::B3Propagator, :rack_helpers do + let(:propagator) { subject } + let(:trace_id_high_bytes) { LightStep.guid } + let(:trace_id_low_bytes) { LightStep.guid } + let(:trace_id) { [trace_id_high_bytes, trace_id_low_bytes].join } + let(:span_id) { LightStep.guid } + let(:baggage) do + { + 'footwear' => 'cleats', + 'umbrella' => 'golf' + } + end + let(:span_context) do + LightStep::SpanContext.new( + id: span_id, + trace_id: trace_id, + baggage: baggage + ) + end + let(:span_context_trace_id_low_bytes) do + LightStep::SpanContext.new( + id: span_id, + trace_id: trace_id_low_bytes, + baggage: baggage + ) + end + let(:unsampled_span_context) do + LightStep::SpanContext.new( + id: span_id, + trace_id: trace_id, + sampled: true, + baggage: baggage + ) + end + + describe '#inject' do + it 'handles text carriers' do + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_TEXT_MAP, carrier) + + expect(carrier['x-b3-traceid']).to eq(trace_id) + expect(carrier['x-b3-spanid']).to eq(span_id) + expect(carrier['x-b3-sampled']).to eq('1') + expect(carrier['ot-baggage-footwear']).to eq('cleats') + expect(carrier['ot-baggage-umbrella']).to eq('golf') + end + + it 'handles rack carriers' do + baggage.merge!({ + 'unsafe!@#$%$^&header' => 'value', + 'CASE-Sensitivity_Underscores'=> 'value' + }) + + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_RACK, carrier) + + expect(carrier['x-b3-traceid']).to eq(trace_id) + expect(carrier['x-b3-spanid']).to eq(span_id) + expect(carrier['x-b3-sampled']).to eq('1') + expect(carrier['ot-baggage-footwear']).to eq('cleats') + expect(carrier['ot-baggage-umbrella']).to eq('golf') + expect(carrier['ot-baggage-unsafeheader']).to be_nil + expect(carrier['ot-baggage-CASE-Sensitivity_Underscores']).to eq('value') + end + + it 'propagates a 16 byte trace id' do + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_TEXT_MAP, carrier) + + expect(carrier['x-b3-traceid']).to eq(trace_id) + expect(carrier['x-b3-traceid'].size).to eq(32) + end + + it 'pads 8 byte trace_ids' do + carrier = {} + + propagator.inject(span_context_trace_id_low_bytes, OpenTracing::FORMAT_RACK, carrier) + expect(carrier['x-b3-traceid']).to eq('0' * 16 << trace_id_low_bytes) + expect(carrier['x-b3-spanid']).to eq(span_id) + end + end + + describe '#extract' do + it 'handles text carriers' do + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_TEXT_MAP, carrier) + extracted_ctx = propagator.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + + expect(extracted_ctx.trace_id).to eq(trace_id_low_bytes) + expect(extracted_ctx.trace_id16).to eq(trace_id) + expect(extracted_ctx.id).to eq(span_id) + expect(extracted_ctx.baggage['footwear']).to eq('cleats') + expect(extracted_ctx.baggage['umbrella']).to eq('golf') + end + + it 'handles rack carriers' do + baggage.merge!({ + 'unsafe!@#$%$^&header' => 'value', + 'CASE-Sensitivity_Underscores'=> 'value' + }) + + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_RACK, carrier) + extracted_ctx = propagator.extract(OpenTracing::FORMAT_RACK, to_rack_env(carrier)) + + expect(extracted_ctx.trace_id).to eq(trace_id_low_bytes) + expect(extracted_ctx.trace_id16).to eq(trace_id) + expect(extracted_ctx.id).to eq(span_id) + expect(extracted_ctx.baggage['footwear']).to eq('cleats') + expect(extracted_ctx.baggage['umbrella']).to eq('golf') + expect(extracted_ctx.baggage['unsafe!@#$%$^&header']).to be_nil + expect(extracted_ctx.baggage['unsafeheader']).to be_nil + expect(extracted_ctx.baggage['case-sensitivity-underscores']).to eq('value') + end + + it 'returns a span context when carrier has both a span_id and trace_id' do + extracted_ctx = propagator.extract( + OpenTracing::FORMAT_RACK, + {'HTTP_X_B3_TRACEID' => trace_id} + ) + + expect(extracted_ctx).to be_nil + extracted_ctx = propagator.extract( + OpenTracing::FORMAT_RACK, + {'HTTP_X_B3_SPANID' => span_id} + ) + expect(extracted_ctx).to be_nil + + # We need both a TRACEID and SPANID; this has both so it should work. + extracted_ctx = propagator.extract( + OpenTracing::FORMAT_RACK, + {'HTTP_X_B3_SPANID' => span_id, 'HTTP_X_B3_TRACEID' => trace_id} + ) + expect(extracted_ctx.id).to eq(span_id) + expect(extracted_ctx.trace_id16).to eq(trace_id) + expect(extracted_ctx.trace_id).to eq(trace_id_low_bytes) + end + + it 'handles carriers with string keys' do + carrier_with_strings = { + 'HTTP_X_B3_TRACEID' => trace_id, + 'HTTP_X_B3_SPANID' => span_id, + 'HTTP_X_B3_SAMPLED' => '1' + } + string_ctx = propagator.extract(OpenTracing::FORMAT_RACK, carrier_with_strings) + + expect(string_ctx).not_to be_nil + expect(string_ctx.trace_id16).to eq(trace_id) + expect(string_ctx.trace_id).to eq(trace_id_low_bytes) + expect(string_ctx).to be_sampled + expect(string_ctx.id).to eq(span_id) + end + + it 'handles carriers symbol keys' do + carrier_with_symbols = { + HTTP_X_B3_TRACEID: trace_id, + HTTP_X_B3_SPANID: span_id, + HTTP_X_B3_SAMPLED: '1' + } + symbol_ctx = propagator.extract(OpenTracing::FORMAT_RACK, carrier_with_symbols) + + expect(symbol_ctx).not_to be_nil + expect(symbol_ctx.trace_id16).to eq(trace_id) + expect(symbol_ctx.trace_id).to eq(trace_id_low_bytes) + expect(symbol_ctx).to be_sampled + expect(symbol_ctx.id).to eq(span_id) + end + + it 'pads 8 byte trace_ids' do + carrier = { + 'x-b3-traceid' => trace_id_low_bytes, + 'x-b3-spanid' => span_id, + 'x-b3-sampled' => '1' + } + + extracted_ctx = propagator.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + expect(extracted_ctx.trace_id16).to eq('0' * 16 << trace_id_low_bytes) + expect(extracted_ctx.trace_id).to eq(trace_id_low_bytes) + end + + it 'interprets a true sampled flag properly' do + carrier = { + 'x-b3-traceid' => trace_id, + 'x-b3-spanid' => span_id, + 'x-b3-sampled' => '1' + } + + extracted_ctx = propagator.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + expect(extracted_ctx).to be_sampled + end + + it 'interprets a false sampled flag properly' do + carrier = { + 'x-b3-traceid' => trace_id, + 'x-b3-spanid' => span_id, + 'x-b3-sampled' => '0' + } + + extracted_ctx = propagator.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + expect(extracted_ctx).not_to be_sampled + end + + it 'maintains 8 and 16 byte trace ids' do + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_TEXT_MAP, carrier) + + extracted_ctx = propagator.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + expect(extracted_ctx.trace_id16).to eq(trace_id) + expect(extracted_ctx.trace_id16.size).to eq(32) + expect(extracted_ctx.trace_id).to eq(trace_id_low_bytes) + expect(extracted_ctx.trace_id.size).to eq(16) + end + end +end diff --git a/spec/lightstep/propagation/lightstep_propagator_spec.rb b/spec/lightstep/propagation/lightstep_propagator_spec.rb new file mode 100644 index 0000000..c49e630 --- /dev/null +++ b/spec/lightstep/propagation/lightstep_propagator_spec.rb @@ -0,0 +1,184 @@ +require 'spec_helper' + +describe LightStep::Propagation::LightStepPropagator, :rack_helpers do + let(:propagator) { subject } + let(:trace_id) { LightStep.guid } + let(:padded_trace_id) { '0' * 16 << trace_id } + let(:span_id) { LightStep.guid } + let(:baggage) do + { + 'footwear' => 'cleats', + 'umbrella' => 'golf' + } + end + let(:span_context) do + LightStep::SpanContext.new( + id: span_id, + trace_id: trace_id, + baggage: baggage + ) + end + + describe '#inject' do + it 'handles text carriers' do + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_TEXT_MAP, carrier) + + expect(carrier['ot-tracer-traceid']).to eq(trace_id) + expect(carrier['ot-tracer-spanid']).to eq(span_id) + expect(carrier['ot-baggage-footwear']).to eq('cleats') + expect(carrier['ot-baggage-umbrella']).to eq('golf') + end + + it 'handles rack carriers' do + baggage.merge!({ + 'unsafe!@#$%$^&header' => 'value', + 'CASE-Sensitivity_Underscores'=> 'value' + }) + + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_RACK, carrier) + + expect(carrier['ot-tracer-traceid']).to eq(trace_id) + expect(carrier['ot-tracer-spanid']).to eq(span_id) + expect(carrier['ot-baggage-footwear']).to eq('cleats') + expect(carrier['ot-baggage-umbrella']).to eq('golf') + expect(carrier['ot-baggage-unsafeheader']).to be_nil + expect(carrier['ot-baggage-CASE-Sensitivity_Underscores']).to eq('value') + end + + it 'propagates an 8 byte trace id' do + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_TEXT_MAP, carrier) + + expect(carrier['ot-tracer-traceid']).to eq(trace_id) + expect(carrier['ot-tracer-traceid'].size).to eq(16) + end + + it 'always propagates a true sampled flag' do + [true, false].each do |sampled| + ctx = LightStep::SpanContext.new( + id: span_id, + trace_id: trace_id, + sampled: sampled, + baggage: baggage + ) + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_TEXT_MAP, carrier) + expect(carrier['ot-tracer-sampled']).to eq('true') + end + end + end + + describe '#extract' do + it 'handles text carriers' do + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_TEXT_MAP, carrier) + extracted_ctx = propagator.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + + expect(extracted_ctx.trace_id).to eq(trace_id) + expect(extracted_ctx.trace_id16).to eq(padded_trace_id) + expect(extracted_ctx.id).to eq(span_id) + expect(extracted_ctx.baggage['footwear']).to eq('cleats') + expect(extracted_ctx.baggage['umbrella']).to eq('golf') + end + + it 'handles rack carriers' do + baggage.merge!({ + 'unsafe!@#$%$^&header' => 'value', + 'CASE-Sensitivity_Underscores'=> 'value' + }) + + carrier = {} + propagator.inject(span_context, OpenTracing::FORMAT_RACK, carrier) + extracted_ctx = propagator.extract(OpenTracing::FORMAT_RACK, to_rack_env(carrier)) + + expect(extracted_ctx.trace_id).to eq(trace_id) + expect(extracted_ctx.trace_id16).to eq(padded_trace_id) + expect(extracted_ctx.id).to eq(span_id) + expect(extracted_ctx.baggage['footwear']).to eq('cleats') + expect(extracted_ctx.baggage['umbrella']).to eq('golf') + expect(extracted_ctx.baggage['unsafe!@#$%$^&header']).to be_nil + expect(extracted_ctx.baggage['unsafeheader']).to be_nil + expect(extracted_ctx.baggage['case-sensitivity-underscores']).to eq('value') + end + + it 'returns a span context when carrier has both a span_id and trace_id' do + extracted_ctx = propagator.extract( + OpenTracing::FORMAT_RACK, + {'HTTP_OT_TRACER_TRACEID' => trace_id} + ) + + expect(extracted_ctx).to be_nil + extracted_ctx = propagator.extract( + OpenTracing::FORMAT_RACK, + {'HTTP_OT_TRACER_SPANID' => span_id} + ) + expect(extracted_ctx).to be_nil + + # We need both a TRACEID and SPANID; this has both so it should work. + extracted_ctx = propagator.extract( + OpenTracing::FORMAT_RACK, + {'HTTP_OT_TRACER_SPANID' => span_id, 'HTTP_OT_TRACER_TRACEID' => trace_id} + ) + expect(extracted_ctx.id).to eq(span_id) + expect(extracted_ctx.trace_id).to eq(trace_id) + expect(extracted_ctx.trace_id16).to eq(padded_trace_id) + end + + it 'handles carriers with string keys' do + carrier = { + 'HTTP_OT_TRACER_TRACEID' => trace_id, + 'HTTP_OT_TRACER_SPANID' => span_id, + } + extracted_ctx = propagator.extract(OpenTracing::FORMAT_RACK, carrier) + + expect(extracted_ctx).not_to be_nil + expect(extracted_ctx.trace_id).to eq(trace_id) + expect(extracted_ctx.trace_id16).to eq(padded_trace_id) + expect(extracted_ctx.id).to eq(span_id) + end + + it 'handles carriers with symbol keys' do + carrier = { + HTTP_OT_TRACER_TRACEID: trace_id, + HTTP_OT_TRACER_SPANID: span_id, + } + extracted_ctx = propagator.extract(OpenTracing::FORMAT_RACK, carrier) + + expect(extracted_ctx).not_to be_nil + expect(extracted_ctx.trace_id).to eq(trace_id) + expect(extracted_ctx.trace_id16).to eq(padded_trace_id) + expect(extracted_ctx.id).to eq(span_id) + end + + it 'maintains 8 and 16 byte trace ids' do + trace_id16 = [LightStep.guid, trace_id].join + + carrier = { + 'ot-tracer-traceid' => trace_id16, + 'ot-tracer-spanid' => span_id, + 'ot-tracer-sampled' => 'true' + } + + extracted_ctx = propagator.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + expect(extracted_ctx.trace_id16).to eq(trace_id16) + expect(extracted_ctx.trace_id16.size).to eq(32) + expect(extracted_ctx.trace_id).to eq(trace_id) + expect(extracted_ctx.trace_id.size).to eq(16) + end + + it 'always sets sampled: true on returned context' do + ['true', 'false'].each do |sampled| + carrier = { + 'ot-tracer-traceid' => trace_id, + 'ot-tracer-spanid' => span_id, + 'ot-tracer-sampled' => sampled + } + + extracted_ctx = propagator.extract(OpenTracing::FORMAT_TEXT_MAP, carrier) + expect(extracted_ctx).to be_sampled + end + end + end +end diff --git a/spec/lightstep/propagation_spec.rb b/spec/lightstep/propagation_spec.rb new file mode 100644 index 0000000..2567e7d --- /dev/null +++ b/spec/lightstep/propagation_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe LightStep::Propagation do + let(:propagator_map) { LightStep::Propagation::PROPAGATOR_MAP } + describe "[]" do + it 'returns propagator instance from symbol' do + propagator_map.each_pair do |sym, klass| + propagator = LightStep::Propagation[sym] + expect(propagator).to be_an_instance_of(klass) + end + end + + it 'returns propagator instance from a string' do + propagator_map.each_pair do |sym, klass| + propagator = LightStep::Propagation[sym.to_s] + expect(propagator).to be_an_instance_of(klass) + end + end + + it 'returns lightstep propagator when name is unknown' do + propagator = LightStep::Propagation[:this_propagator_is_unknown] + expect(propagator).to be_an_instance_of(LightStep::Propagation::LightStepPropagator) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 253f313..516fc60 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,9 +20,11 @@ SimpleCov.start require 'pp' require 'lightstep' require 'timecop' +require 'helpers/rack_helpers' # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| + config.include RackHelpers, :rack_helpers # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer.