diff --git a/Makefile b/Makefile index f661fc4..951e8ff 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ test: bundle exec rake spec ruby example.rb ruby examples/fork_children/main.rb + ruby examples/rack/inject_extract.rb benchmark: ruby benchmark/bench.rb diff --git a/examples/rack/inject_extract.rb b/examples/rack/inject_extract.rb new file mode 100644 index 0000000..7fa8bce --- /dev/null +++ b/examples/rack/inject_extract.rb @@ -0,0 +1,66 @@ +require 'bundler/setup' +require 'lightstep' + +require 'rack' +require 'rack/server' + +$token = '{your_access_token}' +$request_id = 'abc123' + +class Router + def initialize + @tracer = LightStep::Tracer.new(component_name: 'router', access_token: $token) + end + + def call(env) + span = @tracer.start_span("router_call").set_baggage_item("request-id", $request_id) + span.log(event: "router_request", env: env) + puts "parent #{span.trace_id}" + + client = Net::HTTP.new("localhost", "9002") + req = Net::HTTP::Post.new("/") + @tracer.inject(span, LightStep::Tracer::FORMAT_HTTP, req) + res = client.request(req) + + span.log(event: "application_response", response: res.to_s) + span.finish + @tracer.flush + puts "----> https://app.lightstep.com/#{$token}/trace?span_guid=#{span.id}&at_micros=#{span.start_micros} <----" + [200, {}, [res.body]] + end +end + +class App + def initialize + @tracer = LightStep::Tracer.new(component_name: 'app', access_token: $token) + end + + def call(env) + span = @tracer.extract("app_call", LightStep::Tracer::FORMAT_HTTP, env) + puts "child #{span.to_h[:trace_guid]}" + span.log(event: "application", env: env) + sleep 0.05 + span.finish + @tracer.flush + [200, {}, ["application"]] + end +end + +router_thread = Thread.new do + Thread.abort_on_exception = true + Rack::Server.start(app: Router.new, Port: 9001) +end + +app_thread = Thread.new do + Thread.abort_on_exception = true + Rack::Server.start(app: App.new, Port: 9002) +end + +loop do + begin + p Net::HTTP.get(URI("http://localhost:9001/")) + break + rescue Errno::ECONNREFUSED + sleep 0.05 + end +end diff --git a/lib/lightstep/tracer.rb b/lib/lightstep/tracer.rb index 3bf9771..30e65e0 100644 --- a/lib/lightstep/tracer.rb +++ b/lib/lightstep/tracer.rb @@ -11,6 +11,7 @@ module LightStep class Tracer FORMAT_TEXT_MAP = 1 FORMAT_BINARY = 2 + FORMAT_HTTP = 3 class Error < LightStep::Error; end class ConfigurationError < LightStep::Tracer::Error; end @@ -89,13 +90,15 @@ module LightStep # Inject a span into the given carrier # @param span [Span] # @param format [LightStep::Tracer::FORMAT_TEXT_MAP, LightStep::Tracer::FORMAT_BINARY] - # @param carrier [Hash-like] + # @param carrier [Hash, Net::HTTP::Request] def inject(span, format, carrier) case format when LightStep::Tracer::FORMAT_TEXT_MAP inject_to_text_map(span, carrier) when LightStep::Tracer::FORMAT_BINARY warn 'Binary inject format not yet implemented' + when LightStep::Tracer::FORMAT_HTTP + inject_to_http_headers(span, carrier) else warn 'Unknown inject format' end @@ -104,7 +107,9 @@ module LightStep # Extract a span from a carrier # @param operation_name [String] # @param format [LightStep::Tracer::FORMAT_TEXT_MAP, LightStep::Tracer::FORMAT_BINARY] - # @param carrier [Hash-like] + # @param carrier [Hash] This can be either a normal hash of http headers (uppercase and + # separated by underscores) or a Rack environment header. The Rack `HTTP_` prefix + # will automatically be stripped. # @return [Span] def extract(operation_name, format, carrier) case format @@ -113,6 +118,8 @@ module LightStep when LightStep::Tracer::FORMAT_BINARY warn 'Binary join format not yet implemented' nil + when LightStep::Tracer::FORMAT_HTTP + extract_from_rack_env(operation_name, carrier) else warn 'Unknown join format' nil @@ -175,6 +182,8 @@ module LightStep CARRIER_TRACER_STATE_PREFIX = 'ot-tracer-'.freeze CARRIER_BAGGAGE_PREFIX = 'ot-baggage-'.freeze + CARRIER_RACK_HTTP_TRACER_STATE_PREFIX = "#{CARRIER_TRACER_STATE_PREFIX.gsub("-", "_").upcase}" + CARRIER_RACK_HTTP_BAGGAGE_PREFIX = "#{CARRIER_BAGGAGE_PREFIX.gsub("-", "_").upcase}" DEFAULT_MAX_LOG_RECORDS = 1000 MIN_MAX_LOG_RECORDS = 1 @@ -213,5 +222,27 @@ module LightStep span end + + def inject_to_http_headers(span, carrier) + carrier[CARRIER_RACK_HTTP_TRACER_STATE_PREFIX + 'SPANID'] = span.id + carrier[CARRIER_RACK_HTTP_TRACER_STATE_PREFIX + 'TRACEID'] = span.trace_id unless span.trace_id.nil? + carrier[CARRIER_RACK_HTTP_TRACER_STATE_PREFIX + 'SAMPLED'] = 'true' + + span.baggage.each do |key, value| + carrier[CARRIER_RACK_HTTP_BAGGAGE_PREFIX + key.gsub("-", "_").upcase.gsub(/[^A-Z0-9_]/, '')] = value + end + end + + def extract_from_rack_env(operation_name, env) + extract_from_text_map(operation_name, env.reduce({}){|memo, tuple| + raw_header, value = tuple + header = raw_header.gsub(/^HTTP_/, '') + if header.start_with?(CARRIER_RACK_HTTP_TRACER_STATE_PREFIX) || + header.start_with?(CARRIER_RACK_HTTP_BAGGAGE_PREFIX) + memo[header.gsub("_", "-").downcase] = value + end + memo + }) + end end end diff --git a/spec/lightstep_spec.rb b/spec/lightstep_spec.rb index 446f0bc..86a456b 100644 --- a/spec/lightstep_spec.rb +++ b/spec/lightstep_spec.rb @@ -283,6 +283,35 @@ describe LightStep do span2.finish end + it 'should handle inject/extract for http requests and rack' do + tracer = init_test_tracer + span1 = tracer.start_span('test_span') + span1.set_baggage_item('footwear', 'cleats') + span1.set_baggage_item('umbrella', 'golf') + span1.set_baggage_item('unsafe!@#$%$^&header', 'value') + + carrier = {} + tracer.inject(span1, LightStep::Tracer::FORMAT_HTTP, carrier) + expect(carrier['OT_TRACER_TRACEID']).to eq(span1.trace_id) + expect(carrier['OT_TRACER_SPANID']).to eq(span1.id) + expect(carrier['OT_BAGGAGE_FOOTWEAR']).to eq('cleats') + expect(carrier['OT_BAGGAGE_UMBRELLA']).to eq('golf') + expect(carrier['OT_BAGGAGE_UNSAFEHEADER']).to eq('value') + + span2 = tracer.extract('test_span_2', LightStep::Tracer::FORMAT_HTTP, carrier) + expect(span2.trace_id).to eq(span1.trace_id) + expect(span2.tags[:parent_span_guid]).to eq(span1.id) + expect(span2.get_baggage_item('footwear')).to eq('cleats') + expect(span2.get_baggage_item('umbrella')).to eq('golf') + + span3 = tracer.extract('test_span_3', LightStep::Tracer::FORMAT_HTTP, {'HTTP_OT_TRACER_TRACEID' => 'abc123'}) + expect(span3.trace_id).to eq('abc123') + + span1.finish + span2.finish + span3.finish + end + it 'should handle concurrent spans' do result = nil tracer = init_callback_tracer(proc { |obj|; result = obj; })