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:HTTP header tagging with DD_TRACE_HEADER_TAGS for servers #2935

Merged
merged 26 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ae2ce15
Added:HTTP header tagging with DD_TRACE_HEADER_TAGS
marcotc Jun 30, 2023
e9e22d4
fix test
marcotc Jun 30, 2023
00c2ade
fix rack
marcotc Jun 30, 2023
4127438
more fix rack
marcotc Jun 30, 2023
56a2624
fix rack test
marcotc Jun 30, 2023
1768690
Merge branch 'master' into DD_TRACE_HEADER_TAGS
marcotc Jul 4, 2023
11b6106
Refactor to ease client implementation
marcotc Jul 4, 2023
501bae1
Merge branch 'master' into DD_TRACE_HEADER_TAGS
marcotc Jul 13, 2023
534a8ef
Cover case where tag name is empty
marcotc Jul 13, 2023
5bedbd7
Make comment more accurate
marcotc Jul 14, 2023
d99f097
Remove lazy option
marcotc Jul 14, 2023
bbb38d7
Remote old require
marcotc Jul 14, 2023
99daebc
Merge branch 'master' into DD_TRACE_HEADER_TAGS
marcotc Jul 14, 2023
2c817e6
Prevent double wrapping of env
marcotc Jul 14, 2023
0babd5f
Merge branch 'master' into DD_TRACE_HEADER_TAGS
marcotc Jul 14, 2023
132c4f9
Simplify rack/header_tagging.rb
marcotc Jul 17, 2023
f24d359
Rename test subject
marcotc Jul 17, 2023
414d918
Merge branch 'master' into DD_TRACE_HEADER_TAGS
marcotc Jul 20, 2023
e3d6318
Update lib/datadog/tracing/configuration/settings.rb
marcotc Jul 21, 2023
adc6c70
Update lib/datadog/tracing/contrib/rack/header_tagging.rb
marcotc Jul 21, 2023
a01cde6
Apply suggetions
marcotc Jul 21, 2023
7119f2a
Add one character to save the day
marcotc Jul 22, 2023
5456ac4
Remove unnecessary empty fallback
marcotc Jul 24, 2023
6d808b5
Merge branch 'master' into DD_TRACE_HEADER_TAGS
marcotc Jul 24, 2023
1929f83
Fix:Options#using_default? on env var
marcotc Jul 24, 2023
d627531
Merge branch 'fix-en-var' into DD_TRACE_HEADER_TAGS
marcotc Jul 24, 2023
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
7 changes: 5 additions & 2 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -1612,7 +1612,7 @@ run app
| --- | ----------- | ------- |
| `application` | Your Rack application. Required for `middleware_names`. | `nil` |
| `distributed_tracing` | Enables [distributed tracing](#distributed-tracing) so that this service trace is connected with a trace of another service if tracing headers are received | `true` |
| `headers` | Hash of HTTP request or response headers to add as tags to the `rack.request`. Accepts `request` and `response` keys with Array values e.g. `['Last-Modified']`. Adds `http.request.headers.*` and `http.response.headers.*` tags respectively. | `{ response: ['Content-Type', 'X-Request-ID'] }` |
| `headers` | Hash of HTTP request or response headers to add as tags to the `rack.request`. Accepts `request` and `response` keys with Array values e.g. `['Last-Modified']`. Adds `http.request.headers.*` and `http.response.headers.*` tags respectively. This option overrides the global `DD_TRACE_HEADER_TAGS`, see [Applying header tags to root spans][header tags] for more information. | `{ response: ['Content-Type', 'X-Request-ID'] }` |
| `middleware_names` | Enable this if you want to use the last executed middleware class as the resource name for the `rack` span. If enabled alongside the `rails` instrumention, `rails` takes precedence by setting the `rack` resource name to the active `rails` controller when applicable. Requires `application` option to use. | `false` |
| `quantize` | Hash containing options for quantization. May include `:query` or `:fragment`. | `{}` |
| `quantize.base` | Defines behavior for URL base (scheme, host, port). May be `:show` to keep URL base in `http.url` tag and not set `http.base_url` tag, or `nil` to remove URL base from `http.url` tag by default, leaving a path and setting `http.base_url`. Option must be nested inside the `quantize` option. | `nil` |
Expand Down Expand Up @@ -2130,7 +2130,7 @@ end
| Key | Description | Default |
| --- | ----------- | ------- |
| `distributed_tracing` | Enables [distributed tracing](#distributed-tracing) so that this service trace is connected with a trace of another service if tracing headers are received | `true` |
| `headers` | Hash of HTTP request or response headers to add as tags to the `sinatra.request`. Accepts `request` and `response` keys with Array values e.g. `['Last-Modified']`. Adds `http.request.headers.*` and `http.response.headers.*` tags respectively. | `{ response: ['Content-Type', 'X-Request-ID'] }` |
| `headers` | Hash of HTTP request or response headers to add as tags to the `sinatra.request`. Accepts `request` and `response` keys with Array values e.g. `['Last-Modified']`. Adds `http.request.headers.*` and `http.response.headers.*` tags respectively. This option overrides the global `DD_TRACE_HEADER_TAGS`, see [Applying header tags to root spans][header tags] for more information. | `{ response: ['Content-Type', 'X-Request-ID'] }` |
| `resource_script_names` | Prepend resource names with script name | `false` |

### Sneakers
Expand Down Expand Up @@ -2233,6 +2233,7 @@ For example, if `tracing.sampling.default_rate` is configured by [Remote Configu
| `tracing.distributed_tracing.propagation_inject_style` | `DD_TRACE_PROPAGATION_STYLE_INJECT` | `['Datadog']` | Distributed tracing propagation formats to inject. Overrides `DD_TRACE_PROPAGATION_STYLE`. See [Distributed Tracing](#distributed-tracing) for more details. |
| `tracing.distributed_tracing.propagation_style` | `DD_TRACE_PROPAGATION_STYLE` | `nil` | Distributed tracing propagation formats to extract and inject. See [Distributed Tracing](#distributed-tracing) for more details. |
| `tracing.enabled` | `DD_TRACE_ENABLED` | `true` | Enables or disables tracing. If set to `false` instrumentation will still run, but no traces are sent to the trace agent. |
| `tracing.header_tags` | `DD_TRACE_HEADER_TAGS` | `nil` | Record HTTP headers as span tags. See [Applying header tags to root spans][header tags] for more information. |
| `tracing.instrument(<integration-name>, <options...>)` | | | Activates instrumentation for a specific library. See [Integration instrumentation](#integration-instrumentation) for more details. |
| `tracing.log_injection` | `DD_LOGS_INJECTION` | `true` | Injects [Trace Correlation](#trace-correlation) information into Rails logs if present. Supports the default logger (`ActiveSupport::TaggedLogging`), `lograge`, and `semantic_logger`. |
| `tracing.partial_flush.enabled` | | `false` | Enables or disables partial flushing. Partial flushing submits completed portions of a trace to the agent. Used when tracing instruments long running tasks (e.g. jobs) with many spans. |
Expand Down Expand Up @@ -2940,3 +2941,5 @@ As the implementation of `alias_method` exists within those libraries, Datadog g
For libraries without a known workaround, consider removing the library using `alias` or `Module#alias_method` or separating libraries into different environments for testing.

For any further questions or to report an occurence of this issue, please [reach out to Datadog support](https://docs.datadoghq.com/help)

[header tags]: https://docs.datadoghq.com/tracing/configure_data_security/#applying-header-tags-to-root-spans
47 changes: 47 additions & 0 deletions lib/datadog/core/utils/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,53 @@ def compact!
end
end
end

# A minimal {Hash} wrapper that provides case-insensitive access
# to hash keys, without the overhead of copying the original hash.
#
# This class should be used when the original hash is short lived *and*
# each hash key is only accesses a few times.
# For other cases, create a copy of the original hash with the keys
# normalized adequate to your use case.
class CaseInsensitiveWrapper
def initialize(hash)
raise ArgumentError, "must be a hash, but was #{hash.class}: #{hash.inspect}" unless hash.is_a?(::Hash)

@hash = hash
end

def [](key)
return nil unless key.is_a?(::String)

@hash.each do |k, value|
return value if key.casecmp(k) == 0
end

nil
end

def key?(key)
return false unless key.is_a?(::String)

@hash.each_key do |k|
return true if key.casecmp(k) == 0
end

false
end

def empty?
@hash.empty?
end

def length
@hash.length
end

def original_hash
@hash
end
end
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/datadog/tracing/configuration/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Configuration
# e.g. Env vars, default values, enums, etc...
module Ext
ENV_ENABLED = 'DD_TRACE_ENABLED'
ENV_HEADER_TAGS = 'DD_TRACE_HEADER_TAGS'
ENV_TRACE_ID_128_BIT_GENERATION_ENABLED = 'DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED'

# @public_api
Expand Down
74 changes: 74 additions & 0 deletions lib/datadog/tracing/configuration/http.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

module Datadog
module Tracing
module Configuration
module HTTP
# Datadog tracing supports capturing HTTP request and response headers as span tags.
#
# The provided configuration String for this feature has to be pre-processed to
# allow for ease of utilization by each HTTP integration.
#
# This class process configuration, stores the result, and provides methods to
# utilize this configuration.
class HeaderTags
# @param header_tags [Array<String>] The list of strings from DD_TRACE_HEADER_TAGS.
def initialize(header_tags)
@request_headers = {}
@response_headers = {}
@header_tags = header_tags

@header_tags.each do |header_tag|
header, tag = header_tag.split(':', 2)

next unless header # Empty string guard

if tag && !tag.empty?
# When a custom tag name is provided, use that name for both
# request and response tags.
normalized_tag = Tracing::Metadata::Ext::HTTP::Headers.to_tag(tag, allow_nested: true)
request = response = normalized_tag
else
# Otherwise, use our internal pattern of
# "http.{request|response}.headers.{header}" as tag name.
request = Tracing::Metadata::Ext::HTTP::RequestHeaders.to_tag(header)
response = Tracing::Metadata::Ext::HTTP::ResponseHeaders.to_tag(header)
end

@request_headers[header] = request
@response_headers[header] = response
end
end

# Receives a case insensitive hash with the request headers and returns
# a list of tag names and values that can be set in a span.
def request_tags(headers)
@request_headers.map do |header_name, span_tag|
# Case-insensitive search
header_value = headers[header_name]

[span_tag, header_value] if header_value
end.compact
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of map and compact, can we reduce?

@request_headers.reduce([]) do |array, (header_name, span_tag)|
  header_value = headers[header_name]
  array << [span_tag, header_value] if header_value
  array
end

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure, do you think the reduce easier to read?

@request_headers.reduce([]) do |array, (header_name, span_tag)|
  # Case-insensitive search
  header_value = headers[header_name]

  array << [span_tag, header_value] if header_value
  array
end

end

# Receives a case insensitive hash with the response headers and returns
# a list of tag names and values that can be set in a span.
def response_tags(headers)
@response_headers.map do |header_name, span_tag|
# Case-insensitive search
header_value = headers[header_name]

[span_tag, header_value] if header_value
end.compact
end

# For easy configuration inspection,
# print the original configuration setting.
def to_s
@header_tags.join(',').to_s
end
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/datadog/tracing/configuration/settings.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require_relative '../../tracing/configuration/ext'
require_relative 'http'

module Datadog
module Tracing
Expand Down Expand Up @@ -165,6 +166,24 @@ def self.extended(base)
o.default { env_to_bool(Tracing::Configuration::Ext::ENV_ENABLED, true) }
end

# Comma-separated, case-insensitive list of header names that are reported in incoming and outgoing HTTP requests.
#
# Each header in the list can either be:
# * A header name, which is mapped to the respective tags `http.request.headers.<header name>` and `http.response.headers.<header name>`.
# * A key value pair, "header name:tag name", which is mapped to the span tag `tag name`.
#
# You can mix the two types of header declaration in the same list.
# Tag names will be normalized based on the [Datadog tag normalization rules](https://docs.datadoghq.com/getting_started/tagging/#defining-tags).
#
# @default `DD_TRACE_HEADER_TAGS` environment variable, otherwise an empty set of tags
# @return [Array<String>]
option :header_tags do |o|
o.env Configuration::Ext::ENV_HEADER_TAGS
o.type :array
o.default []
o.setter { |header_tags, _| Configuration::HTTP::HeaderTags.new(header_tags) }
end

# Enable 128 bit trace id generation.
#
# @default `DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED` environment variable, otherwise `false`
Expand Down
3 changes: 3 additions & 0 deletions lib/datadog/tracing/contrib/rack/header_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ def get(header_name)
@env[Header.to_rack_header(header_name)]
end

# Allows this class to have a similar API to a {Hash}.
alias [] get

# Tests whether a header with the given name exists in the environment.
def key?(header_name)
@env.key?(Header.to_rack_header(header_name))
Expand Down
53 changes: 53 additions & 0 deletions lib/datadog/tracing/contrib/rack/header_tagging.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true
Copy link
Member Author

@marcotc marcotc Jun 30, 2023

Choose a reason for hiding this comment

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

This code was moved here from lib/datadog/tracing/contrib/sinatra/env.rb and lib/datadog/tracing/contrib/sinatra/headers.rb, with some refactoring taking place.

Sinatra already had helpers to perform header matching for Rack-style headers.
Rack was also performing header matching, but in a less organized fashion.

Because the DD_TRACE_HEADER_TAGS option has to be added to both Rack and Sinatra, this PR merges both header matching implementations into this file as they are the exact same: header matching for Rack-style headers.


module Datadog
module Tracing
module Contrib
module Rack
# Matches Rack-style headers with a matcher and sets matching headers into a span.
module HeaderTagging
def self.tag_request_headers(span, env, configuration)
# Wrap env in a case-insensitive Rack-style accessor.
headers = env.is_a?(Header::RequestHeaderCollection) ? env : Header::RequestHeaderCollection.new(env)

# Use global DD_TRACE_HEADER_TAGS if integration-level configuration is not provided
tags = if configuration.using_default?(:headers) && !Datadog.configuration.tracing.using_default?(:header_tags)
Datadog.configuration.tracing.header_tags.request_tags(headers)
else
whitelist = configuration[:headers][:request] || []
whitelist.each_with_object({}) do |header, result|
header_value = headers.get(header)
unless header_value.nil?
header_tag = Tracing::Metadata::Ext::HTTP::RequestHeaders.to_tag(header)
result[header_tag] = header_value
end
end
end

span.set_tags(tags)
end

def self.tag_response_headers(span, headers, configuration)
headers = Core::Utils::Hash::CaseInsensitiveWrapper.new(headers) # Make header access case-insensitive

# Use global DD_TRACE_HEADER_TAGS if integration-level configuration is not provided
tags = if configuration.using_default?(:headers) && !Datadog.configuration.tracing.using_default?(:header_tags)
Datadog.configuration.tracing.header_tags.response_tags(headers)
else
whitelist = configuration[:headers][:response] || []
whitelist.each_with_object({}) do |header, result|
header_value = headers[header]
unless header_value.nil?
header_tag = Tracing::Metadata::Ext::HTTP::ResponseHeaders.to_tag(header)
result[header_tag] = header_value
end
end
end

span.set_tags(tags)
end
end
end
end
end
end
43 changes: 3 additions & 40 deletions lib/datadog/tracing/contrib/rack/middlewares.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_relative '../utils/quantization/http'
require_relative 'ext'
require_relative 'header_collection'
require_relative 'header_tagging'
require_relative 'request_queue'

module Datadog
Expand Down Expand Up @@ -138,8 +139,6 @@ def call(env)
# rubocop:disable Metrics/MethodLength
def set_request_tags!(trace, request_span, env, status, headers, response, original_env)
request_header_collection = Header::RequestHeaderCollection.new(env)
marcotc marked this conversation as resolved.
Show resolved Hide resolved
request_headers_tags = parse_request_headers(request_header_collection)
response_headers_tags = parse_response_headers(headers || {})

# Since it could be mutated, it would be more accurate to fetch from the original env,
# e.g. ActionDispatch::ShowExceptions middleware with Rails exceptions_app configuration
Expand Down Expand Up @@ -228,15 +227,8 @@ def set_request_tags!(trace, request_span, env, status, headers, response, origi
request_span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_USER_AGENT, user_agent)
end

# Request headers
request_headers_tags.each do |name, value|
request_span.set_tag(name, value) if request_span.get_tag(name).nil?
end

# Response headers
response_headers_tags.each do |name, value|
request_span.set_tag(name, value) if request_span.get_tag(name).nil?
end
HeaderTagging.tag_request_headers(request_span, request_header_collection, configuration)
HeaderTagging.tag_response_headers(request_span, headers, configuration) if headers

# detect if the status code is a 5xx and flag the request span as an error
# unless it has been already set by the underlying framework
Expand Down Expand Up @@ -314,35 +306,6 @@ def parse_url(env, original_env)
def parse_user_agent_header(headers)
headers.get(Tracing::Metadata::Ext::HTTP::HEADER_USER_AGENT)
end

def parse_request_headers(headers)
whitelist = configuration[:headers][:request] || []
whitelist.each_with_object({}) do |header, result|
header_value = headers.get(header)
unless header_value.nil?
header_tag = Tracing::Metadata::Ext::HTTP::RequestHeaders.to_tag(header)
result[header_tag] = header_value
end
end
end

def parse_response_headers(headers)
{}.tap do |result|
whitelist = configuration[:headers][:response] || []
whitelist.each do |header|
if headers.key?(header)
result[Tracing::Metadata::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[Tracing::Metadata::Ext::HTTP::ResponseHeaders.to_tag(header)] = headers[matching_header]
end
end
end
end
end
end
end
end
Expand Down
17 changes: 0 additions & 17 deletions lib/datadog/tracing/contrib/sinatra/env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,6 @@ def set_datadog_span(env, span)
env[Ext::RACK_ENV_SINATRA_REQUEST_SPAN] = span
end

def request_header_tags(env, headers)
headers ||= []

{}.tap do |result|
headers.each do |header|
rack_header = header_to_rack_header(header)
if env.key?(rack_header)
result[Tracing::Metadata::Ext::HTTP::RequestHeaders.to_tag(header)] = env[rack_header]
end
end
end
end

def header_to_rack_header(name)
"HTTP_#{name.to_s.upcase.gsub(/[-\s]/, '_')}"
end

def route_path(env, use_script_names: Datadog.configuration.tracing[:sinatra][:resource_script_names])
return unless env['sinatra.route']

Expand Down
Loading
Loading