Skip to content

Commit

Permalink
fix: Adds User-Agent header in OTLP exporter (#1404)
Browse files Browse the repository at this point in the history
* fix: Adds User-Agent header in OTLP exporter

* fix: Removes version bump

* fix: remove changelog entry for removed version bump

* compliant user-agent name (no spaces)

BONUSES:

+ dup a Hash given for headers to ignore future mutations of that Hash
+ don't clobber a User-Agent supplied by config, append instead
+ include more Ruby runtime information in the user-agent to help
  future troubleshooting

* appease Rubocop

Offenses:

* Lint/RedundantCopDisableDirective: Unnecessary disabling of
  `Metrics/CyclomaticComplexity`.
* Style/RedundantParentheses: Don't use parentheses around a literal.
* Style/StringConcatenation: Prefer string interpolation to string
  concatenation.
* Layout/SpaceAfterComma: Space missing after comma.
* Layout/EmptyLinesAroundBlockBody: Extra empty line detected at block
  body end.

---------

Co-authored-by: Robb Kidd <robbkidd@honeycomb.io>
  • Loading branch information
Coolomina and robbkidd authored May 18, 2023
1 parent c513018 commit 24c8032
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 21 deletions.
24 changes: 17 additions & 7 deletions exporter/otlp/lib/opentelemetry/exporter/otlp/exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class Exporter # rubocop:disable Metrics/ClassLength
ERROR_MESSAGE_INVALID_HEADERS = 'headers must be a String with comma-separated URL Encoded UTF-8 k=v pairs or a Hash'
private_constant(:ERROR_MESSAGE_INVALID_HEADERS)

DEFAULT_USER_AGENT = "OTel-OTLP-Exporter-Ruby/#{OpenTelemetry::Exporter::OTLP::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})"

def self.ssl_verify_mode
if ENV.key?('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER')
OpenSSL::SSL::VERIFY_PEER
Expand All @@ -45,7 +47,7 @@ def self.ssl_verify_mode
end
end

def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'http://localhost:4318/v1/traces'), # rubocop:disable Metrics/CyclomaticComplexity
def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'http://localhost:4318/v1/traces'),
certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'),
ssl_verify_mode: Exporter.ssl_verify_mode,
headers: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', default: {}),
Expand All @@ -64,12 +66,7 @@ def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPOR
@http = http_connection(@uri, ssl_verify_mode, certificate_file)

@path = @uri.path
@headers = case headers
when String then parse_headers(headers)
when Hash then headers
else
raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS
end
@headers = prepare_headers(headers)
@timeout = timeout.to_f
@compression = compression
@metrics_reporter = metrics_reporter || OpenTelemetry::SDK::Trace::Export::MetricsReporter
Expand Down Expand Up @@ -389,6 +386,19 @@ def as_otlp_any_value(value)
result
end

def prepare_headers(config_headers)
headers = case config_headers
when String then parse_headers(config_headers)
when Hash then config_headers.dup
else
raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS
end

headers['User-Agent'] = "#{headers.fetch('User-Agent', '')} #{DEFAULT_USER_AGENT}".strip

headers
end

def parse_headers(raw)
entries = raw.split(',')
raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if entries.empty?
Expand Down
56 changes: 42 additions & 14 deletions exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
describe OpenTelemetry::Exporter::OTLP::Exporter do
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
VERSION = OpenTelemetry::Exporter::OTLP::VERSION
DEFAULT_USER_AGENT = OpenTelemetry::Exporter::OTLP::Exporter::DEFAULT_USER_AGENT

describe '#initialize' do
it 'initializes with defaults' do
exp = OpenTelemetry::Exporter::OTLP::Exporter.new
_(exp).wont_be_nil
_(exp.instance_variable_get(:@headers)).must_be_empty
_(exp.instance_variable_get(:@headers)).must_equal('User-Agent' => DEFAULT_USER_AGENT)
_(exp.instance_variable_get(:@timeout)).must_equal 10.0
_(exp.instance_variable_get(:@path)).must_equal '/v1/traces'
_(exp.instance_variable_get(:@compression)).must_equal 'gzip'
Expand All @@ -27,6 +29,15 @@
_(http.port).must_equal 4318
end

it 'provides a useful, spec-compliant default user agent header' do
# spec compliance: OTLP Exporter name and version
_(DEFAULT_USER_AGENT).must_match("OTel-OTLP-Exporter-Ruby/#{VERSION}")
# bonus: incredibly useful troubleshooting information
_(DEFAULT_USER_AGENT).must_match("Ruby/#{RUBY_VERSION}")
_(DEFAULT_USER_AGENT).must_match(RUBY_PLATFORM)
_(DEFAULT_USER_AGENT).must_match("#{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION}")
end

it 'refuses invalid endpoint' do
assert_raises ArgumentError do
OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: 'not a url')
Expand Down Expand Up @@ -72,7 +83,7 @@
'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd')
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'User-Agent' => DEFAULT_USER_AGENT)
_(exp.instance_variable_get(:@timeout)).must_equal 11.0
_(exp.instance_variable_get(:@path)).must_equal '/v1/traces'
_(exp.instance_variable_get(:@compression)).must_equal 'gzip'
Expand All @@ -98,7 +109,7 @@
ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE,
timeout: 12)
end
_(exp.instance_variable_get(:@headers)).must_equal('x' => 'y')
_(exp.instance_variable_get(:@headers)).must_equal('x' => 'y', 'User-Agent' => DEFAULT_USER_AGENT)
_(exp.instance_variable_get(:@timeout)).must_equal 12.0
_(exp.instance_variable_get(:@path)).must_equal ''
_(exp.instance_variable_get(:@compression)).must_equal 'gzip'
Expand Down Expand Up @@ -130,10 +141,10 @@

it 'restricts explicit headers to a String or Hash' do
exp = OpenTelemetry::Exporter::OTLP::Exporter.new(headers: { 'token' => 'über' })
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über')
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)

exp = OpenTelemetry::Exporter::OTLP::Exporter.new(headers: 'token=%C3%BCber')
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über')
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)

error = _() {
exp = OpenTelemetry::Exporter::OTLP::Exporter.new(headers: Object.new)
Expand All @@ -142,58 +153,75 @@
_(error.message).must_match(/headers/i)
end

it 'ignores later mutations of a headers Hash parameter' do
a_hash_to_mutate_later = { 'token' => 'über' }
exp = OpenTelemetry::Exporter::OTLP::Exporter.new(headers: a_hash_to_mutate_later)
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)

a_hash_to_mutate_later['token'] = 'unter'
a_hash_to_mutate_later['oops'] = 'i forgot to add this, too'
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
end

describe 'Headers Environment Variable' do
it 'allows any number of the equal sign (=) characters in the value' do
exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d==,e=f') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd==', 'e' => 'f')
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd==', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT)

exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_TRACES_HEADERS' => 'a=b,c=d==,e=f') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd==', 'e' => 'f')
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd==', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT)
end

it 'trims any leading or trailing whitespaces in keys and values' do
exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a = b ,c=d , e=f') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'e' => 'f')
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT)

exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_TRACES_HEADERS' => 'a = b ,c=d , e=f') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'e' => 'f')
_(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT)
end

it 'decodes values as URL encoded UTF-8 strings' do
exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'token=%C3%BCber') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über')
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)

exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => '%C3%BCber=token') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('über' => 'token')
_(exp.instance_variable_get(:@headers)).must_equal('über' => 'token', 'User-Agent' => DEFAULT_USER_AGENT)

exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_TRACES_HEADERS' => 'token=%C3%BCber') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über')
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)

exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_TRACES_HEADERS' => '%C3%BCber=token') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('über' => 'token')
_(exp.instance_variable_get(:@headers)).must_equal('über' => 'token', 'User-Agent' => DEFAULT_USER_AGENT)
end

it 'appends the default user agent to one provided in config' do
exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'User-Agent=%C3%BCber/3.2.1') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('User-Agent' => "über/3.2.1 #{DEFAULT_USER_AGENT}")
end

it 'prefers TRACES specific variable' do
exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d==,e=f', 'OTEL_EXPORTER_OTLP_TRACES_HEADERS' => 'token=%C3%BCber') do
OpenTelemetry::Exporter::OTLP::Exporter.new
end
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über')
_(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
end

it 'fails fast when header values are missing' do
Expand Down

0 comments on commit 24c8032

Please sign in to comment.