-
Notifications
You must be signed in to change notification settings - Fork 373
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 http.client_ip
tag for Rack-based frameworks
#2248
Merged
+706
−17
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
70440ae
Add support for `http.client_ip` tag for Rack-based frameworks
barweiss cbfca12
Address @lloeki's great review comments
barweiss 010066e
Fix linter issues
barweiss 0c9eecb
Revert accidental changes to http instrumentation
barweiss ee61750
Fix require line
barweiss ca4c899
Fix comments from @TonyCTHsu's awesome review
barweiss d9532ca
Fix bad 'set_client_ip' call in Rack middleware
barweiss 5d22113
Fix Ruby 2.2 compatibilty issue
barweiss ef6f427
Fix rubocop stuff that only appears in circleci
barweiss 00f3c7d
Rename configuration option 'disabled' -> 'enabled', revert to using …
barweiss f49d03e
Remove 'ipaddress' from LICENSE-3rdparty
barweiss 2dae9e6
Update some documentation
barweiss 76e6e0a
Fix IPAddr usage for Ruby <= 2.3
barweiss ea3b064
Fix Ruby 2.1-2.4 compatibility issue
barweiss 7bbb322
Merge branch 'master' into bar.weiss/client-ip-rack-support
marcotc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
module Datadog | ||
module Core | ||
# A some-what abstract class representing a collection of headers. | ||
# | ||
# Use the `HeaderCollection.from_hash` function to create a header collection from a `Hash`. | ||
# Another option is to use `HashHeaderCollection` directly. | ||
class HeaderCollection | ||
# Gets a single value of the header with the given name, case insensitive. | ||
# | ||
# @param [String] header_name Name of the header to get the value of. | ||
# @returns [String, nil] A single value of the header, or nil if the header with | ||
# the given name is missing from the collection. | ||
def get(header_name) | ||
nil | ||
end | ||
|
||
# Create a header collection that retrieves headers from the given Hash. | ||
# | ||
# This can be useful for testing or other trivial use cases. | ||
# | ||
# @param [Hash] hash Hash with the headers. | ||
def self.from_hash(hash) | ||
HashHeaderCollection.new(hash) | ||
end | ||
end | ||
|
||
# A header collection implementation that looks up headers in a Hash. | ||
class HashHeaderCollection < HeaderCollection | ||
def initialize(hash) | ||
super() | ||
@hash = {}.tap do |res| | ||
hash.each_pair { |key, value| res[key.downcase] = value } | ||
end | ||
end | ||
|
||
def get(header_name) | ||
@hash[header_name.downcase] | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
# typed: true | ||
|
||
require_relative '../core/configuration' | ||
require_relative 'metadata/ext' | ||
require_relative 'span' | ||
|
||
require 'ipaddr' | ||
|
||
module Datadog | ||
module Tracing | ||
# Common functions for supporting the `http.client_ip` span attribute. | ||
module ClientIp | ||
DEFAULT_IP_HEADERS_NAMES = %w[ | ||
x-forwarded-for | ||
x-real-ip | ||
x-client-ip | ||
x-forwarded | ||
x-cluster-client-ip | ||
forwarded-for | ||
forwarded | ||
via | ||
true-client-ip | ||
].freeze | ||
|
||
TAG_MULTIPLE_IP_HEADERS = '_dd.multiple-ip-headers'.freeze | ||
|
||
# Sets the `http.client_ip` tag on the given span. | ||
# | ||
# This function respects the user's settings: if they disable the client IP tagging, | ||
# or provide a different IP header name. | ||
# | ||
# If multiple IP headers are present in the request, this function will instead set | ||
# the `_dd.multiple-ip-headers` tag with the names of the present headers, | ||
# and **NOT** set the `http.client_ip` tag. | ||
# | ||
# @param [Span] span The span that's associated with the request. | ||
# @param [HeaderCollection, #get, nil] headers A collection with the request headers. | ||
# @param [String, nil] remote_ip The remote IP the request associated with the span is sent to. | ||
def self.set_client_ip_tag(span, headers: nil, remote_ip: nil) | ||
return unless configuration.enabled | ||
|
||
result = raw_ip_from_request(headers, remote_ip) | ||
|
||
if result.raw_ip | ||
ip = strip_decorations(result.raw_ip) | ||
return unless valid_ip?(ip) | ||
|
||
span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, ip) | ||
elsif result.multiple_ip_headers | ||
span.set_tag(TAG_MULTIPLE_IP_HEADERS, result.multiple_ip_headers.keys.join(',')) | ||
end | ||
end | ||
|
||
IpExtractionResult = Struct.new(:raw_ip, :multiple_ip_headers) | ||
|
||
# Returns a result struct that holds the raw client IP associated with the request if it was | ||
# retrieved successfully. | ||
# | ||
# The client IP is looked up by the following logic: | ||
# * If the user has configured a header name, return that header's value. | ||
# * If exactly one of the known IP headers is present, return that header's value. | ||
# * If none of the known IP headers are present, return the remote IP from the request. | ||
# | ||
# If more than one of the known IP headers is present, the result will have a `multiple_ip_headers` | ||
# field with the name of the present IP headers. | ||
# | ||
# @param [Datadog::Core::HeaderCollection, #get, nil] headers The request headers | ||
# @param [String] remote_ip The remote IP of the request. | ||
# @return [IpExtractionResult] A struct that holds the unprocessed IP value, | ||
# or `nil` if it wasn't found. Additionally, the `multiple_ip_headers` fields will hold the | ||
# name of known IP headers present in the request if more than one of these were found. | ||
def self.raw_ip_from_request(headers, remote_ip) | ||
return IpExtractionResult.new(headers && headers.get(configuration.header_name), nil) if configuration.header_name | ||
|
||
headers_present = ip_headers(headers) | ||
|
||
case headers_present.size | ||
when 0 | ||
IpExtractionResult.new(remote_ip, nil) | ||
when 1 | ||
IpExtractionResult.new(headers_present.values.first, nil) | ||
else | ||
IpExtractionResult.new(nil, headers_present) | ||
end | ||
end | ||
|
||
# Removes any port notations or zone specifiers from the IP address without | ||
# verifying its validity. | ||
def self.strip_decorations(address) | ||
return strip_ipv4_port(address) if likely_ipv4?(address) | ||
|
||
address = strip_ipv6_port(address) | ||
|
||
strip_zone_specifier(address) | ||
end | ||
|
||
def self.strip_zone_specifier(ipv6) | ||
ipv6.gsub(/%.*/, '') | ||
end | ||
|
||
def self.strip_ipv4_port(ip) | ||
ip.gsub(/:\d+\z/, '') | ||
end | ||
|
||
def self.strip_ipv6_port(ip) | ||
if /\[(.*)\](?::\d+)?/ =~ ip | ||
Regexp.last_match(1) | ||
else | ||
ip | ||
end | ||
end | ||
|
||
# Returns whether the given value is more likely to be an IPv4 than an IPv6 address. | ||
# | ||
# This is done by checking if a dot (`'.'`) character appears before a colon (`':'`) in the value. | ||
# The rationale is that in valid IPv6 addresses, colons will always preced dots, | ||
# and in valid IPv4 addresses dots will always preced colons. | ||
def self.likely_ipv4?(value) | ||
dot_index = value.index('.') || value.size | ||
colon_index = value.index(':') || value.size | ||
|
||
dot_index < colon_index | ||
end | ||
|
||
# Determines whether the given string is a valid IPv4 or IPv6 address. | ||
def self.valid_ip?(ip) | ||
# Client IPs should not have subnet masks even though IPAddr can parse them. | ||
return false if ip.include?('/') | ||
|
||
begin | ||
IPAddr.new(ip) | ||
|
||
true | ||
rescue IPAddr::Error | ||
false | ||
end | ||
end | ||
|
||
def self.ip_headers(headers) | ||
return {} unless headers | ||
|
||
DEFAULT_IP_HEADERS_NAMES.each_with_object({}) do |name, result| | ||
value = headers.get(name) | ||
result[name] = value unless value.nil? | ||
end | ||
end | ||
|
||
def self.configuration | ||
Datadog.configuration.tracing.client_ip | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
require_relative '../../../core/header_collection' | ||
|
||
module Datadog | ||
module Tracing | ||
module Contrib | ||
module Rack | ||
# Classes and utilities for handling headers in Rack. | ||
module Header | ||
# An implementation of a header collection that looks up headers from a Rack environment. | ||
class RequestHeaderCollection < Datadog::Core::HeaderCollection | ||
# Creates a header collection from a rack environment. | ||
def initialize(env) | ||
super() | ||
@env = env | ||
end | ||
|
||
# Gets the value of the header with the given name. | ||
def get(header_name) | ||
@env[Header.to_rack_header(header_name)] | ||
end | ||
|
||
# Tests whether a header with the given name exists in the environment. | ||
def key?(header_name) | ||
@env.key?(Header.to_rack_header(header_name)) | ||
end | ||
end | ||
|
||
def self.to_rack_header(name) | ||
"HTTP_#{name.to_s.upcase.gsub(/[-\s]/, '_')}" | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Linter should have caught this?
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.
Whoops.
Rubocop seems to pass but I shouldv'e considered the formatting in the other options.
Fixed