-
Notifications
You must be signed in to change notification settings - Fork 394
[sinatra] tag span with request id if it is available in response headers #427
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
Changes from all commits
5b21ab9
7ae275e
2d2ac80
98af086
44a0878
ddd8317
dafaee9
ab11f77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
module Datadog | ||
module Contrib | ||
module Sinatra | ||
# Middleware used for automatically tagging configured headers and handle request span | ||
module RequestSpan | ||
SINATRA_REQUEST_SPAN = 'datadog.sinatra_request_span'.freeze | ||
SINATRA_REQUEST_TRACE_NAME = 'sinatra.request'.freeze | ||
|
||
module_function | ||
|
||
def span!(env) | ||
env[SINATRA_REQUEST_SPAN] ||= build_span(env) | ||
end | ||
|
||
def fetch_span(env) | ||
env[SINATRA_REQUEST_SPAN] | ||
end | ||
|
||
def build_span(env) | ||
tracer = configuration[:tracer] | ||
distributed_tracing = configuration[:distributed_tracing] | ||
|
||
if distributed_tracing && tracer.provider.context.trace_id.nil? | ||
context = HTTPPropagator.extract(env) | ||
tracer.provider.context = context if context.trace_id | ||
end | ||
|
||
tracer.trace(SINATRA_REQUEST_TRACE_NAME, | ||
service: configuration[:service_name], | ||
span_type: Datadog::Ext::HTTP::TYPE) | ||
end | ||
|
||
def configuration | ||
Datadog.configuration[:sinatra] | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
require 'ddtrace/contrib/sinatra/request_span' | ||
|
||
module Datadog | ||
module Contrib | ||
module Sinatra | ||
# Middleware used for automatically tagging configured headers and handle request span | ||
class TracerMiddleware | ||
def initialize(app) | ||
@app = app | ||
end | ||
|
||
def call(env) | ||
span = RequestSpan.span!(env) | ||
|
||
# Request headers | ||
parse_request_headers(env).each do |name, value| | ||
span.set_tag(name, value) if span.get_tag(name).nil? | ||
end | ||
|
||
status, headers, response_body = @app.call(env) | ||
|
||
# Response headers | ||
parse_response_headers(headers).each do |name, value| | ||
span.set_tag(name, value) if span.get_tag(name).nil? | ||
end | ||
|
||
[status, headers, response_body] | ||
ensure | ||
span.finish | ||
end | ||
|
||
private | ||
|
||
def parse_request_headers(env) | ||
{}.tap do |result| | ||
whitelist = configuration[:headers][:request] || [] | ||
whitelist.each do |header| | ||
rack_header = header_to_rack_header(header) | ||
if env.key?(rack_header) | ||
result[Datadog::Ext::HTTP::RequestHeaders.to_tag(header)] = env[rack_header] | ||
end | ||
end | ||
end | ||
end | ||
|
||
def parse_response_headers(headers) | ||
{}.tap do |result| | ||
whitelist = configuration[:headers][:response] || [] | ||
whitelist.each do |header| | ||
if headers.key?(header) | ||
result[Datadog::Ext::HTTP::ResponseHeaders.to_tag(header)] = headers[header] | ||
else | ||
# Try a case-insensitive lookup | ||
uppercased_header = header.to_s.upcase | ||
matching_header = headers.keys.find { |h| h.upcase == uppercased_header } | ||
if matching_header | ||
result[Datadog::Ext::HTTP::ResponseHeaders.to_tag(header)] = headers[matching_header] | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
def configuration | ||
Datadog.configuration[:sinatra] | ||
end | ||
|
||
def header_to_rack_header(name) | ||
"HTTP_#{name.to_s.upcase.gsub(/[-\s]/, '_')}" | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
class TracerTest < TracerTestBase | ||
class TracerTestApp < Sinatra::Application | ||
get '/request' do | ||
headers['X-Request-Id'] = request.env['HTTP_X_REQUEST_ID'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose of this line? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since current implementation only observes existing headers. So to actually test request_id handling I need to somehow inject the header into response. I'll clarify that in the comments There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see; so this is to emulate a Sinatra app that adds a request ID header to its response? Seems okay to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes exactly. |
||
'hello world' | ||
end | ||
|
||
|
@@ -83,10 +84,10 @@ def test_distributed_request | |
# Enable distributed tracing | ||
Datadog.configuration.use(:sinatra, distributed_tracing: true) | ||
|
||
response = get '/request', {}, | ||
'HTTP_X_DATADOG_TRACE_ID' => '1', | ||
'HTTP_X_DATADOG_PARENT_ID' => '2', | ||
'HTTP_X_DATADOG_SAMPLING_PRIORITY' => Datadog::Ext::Priority::USER_KEEP.to_s | ||
response = get '/request', {}, | ||
'HTTP_X_DATADOG_TRACE_ID' => '1', | ||
'HTTP_X_DATADOG_PARENT_ID' => '2', | ||
'HTTP_X_DATADOG_SAMPLING_PRIORITY' => Datadog::Ext::Priority::USER_KEEP.to_s | ||
|
||
assert_equal(200, response.status) | ||
|
||
|
@@ -237,4 +238,61 @@ def test_literal_template | |
assert_equal(0, root.status) | ||
assert_nil(root.parent) | ||
end | ||
|
||
def test_tagging_default_connection_headers | ||
request_id = SecureRandom.uuid | ||
get '/request', {}, 'HTTP_X_REQUEST_ID' => request_id | ||
|
||
assert_equal(200, last_response.status) | ||
|
||
spans = @writer.spans | ||
assert_equal(1, spans.length) | ||
|
||
span = spans[0] | ||
assert_equal('sinatra', span.service) | ||
assert_equal('GET /request', span.resource) | ||
assert_equal('GET', span.get_tag(Datadog::Ext::HTTP::METHOD)) | ||
assert_equal('/request', span.get_tag(Datadog::Ext::HTTP::URL)) | ||
assert_equal(Datadog::Ext::HTTP::TYPE, span.span_type) | ||
assert_equal(request_id, span.get_tag('http.response.headers.x_request_id')) | ||
assert_equal('text/html;charset=utf-8', span.get_tag('http.response.headers.content_type')) | ||
|
||
assert_equal(0, span.status) | ||
assert_nil(span.parent) | ||
end | ||
|
||
def test_tagging_configured_connection_headers | ||
Datadog.configuration.use(:sinatra, | ||
headers: { | ||
response: ['Content-Type'], | ||
request: ['X-Request-Header'] | ||
}) | ||
|
||
request_headers = { | ||
'HTTP_X_REQUEST_HEADER' => 'header_value', | ||
'HTTP_X_HEADER' => "don't tag me" | ||
} | ||
|
||
get '/request#foo?a=1', {}, request_headers | ||
|
||
assert_equal(200, last_response.status) | ||
|
||
spans = @writer.spans | ||
assert_equal(1, spans.length) | ||
|
||
span = spans[0] | ||
assert_equal('sinatra', span.service) | ||
assert_equal('GET /request', span.resource) | ||
assert_equal('GET', span.get_tag(Datadog::Ext::HTTP::METHOD)) | ||
assert_equal('/request', span.get_tag(Datadog::Ext::HTTP::URL)) | ||
assert_equal(Datadog::Ext::HTTP::TYPE, span.span_type) | ||
assert_equal('header_value', span.get_tag('http.request.headers.x_request_header')) | ||
assert_equal('text/html;charset=utf-8', span.get_tag('http.response.headers.content_type')) | ||
assert_nil(span.get_tag('http.request.headers.x_header')) | ||
|
||
assert_equal(0, span.status) | ||
assert_nil(span.parent) | ||
ensure | ||
Datadog.configuration.use(:sinatra, headers: Datadog::Contrib::Sinatra::Tracer::DEFAULT_HEADERS) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a functional difference between this and
#span!
? Is there some place in our code you'd want tofetch
a span and get nothing back? And that would be okay?If not, couldn't you just use
#span!
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pawelchcki thoughts here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added
fetch_span
to support the existing case inTracer
response filter, where an error is logged if span doesn't exist. (Not sure how valid that approach is, but It wasn't something I wanted to change at this point).span!
will populate theenv
if the span does not exist hence!
to indicate that.fetch_span
is only meant to hide implementation detail ofenv[SINATRA_REQUEST_SPAN]