From 84aceebc84be37e84902b20232754c99d9b8a52b Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 6 Apr 2026 22:33:04 +0530 Subject: [PATCH 1/2] feat(otlp-http): improve endpoint handling, add User-Agent header, and enhance metrics - Add spec-compliant default User-Agent header with exporter version and Ruby runtime info - Fix endpoint path construction to properly handle OTEL_EXPORTER_OTLP_ENDPOINT with/without trailing slashes - Implement proper endpoint precedence: explicit endpoint > OTEL_EXPORTER_OTLP_TRACES_ENDPOINT > OTEL_EXPORTER_OTLP_ENDPOINT - Add metrics for compressed and uncompressed message sizes - Add specific error handling and logging for 404 responses - Refactor endpoint and header preparation into dedicated methods - Prevent header mutation by duplicating Hash parameters - Add comprehensive test coverage for endpoint path handling and User-Agent header Signed-off-by: Arjun Rajappa --- .../exporter/otlp/http/trace_exporter.rb | 70 ++++++--- .../exporter/otlp/http/trace_exporter_test.rb | 146 ++++++++++++++++-- 2 files changed, 183 insertions(+), 33 deletions(-) diff --git a/exporter/otlp-http/lib/opentelemetry/exporter/otlp/http/trace_exporter.rb b/exporter/otlp-http/lib/opentelemetry/exporter/otlp/http/trace_exporter.rb index f6f0ecd98e..09d70f7b50 100644 --- a/exporter/otlp-http/lib/opentelemetry/exporter/otlp/http/trace_exporter.rb +++ b/exporter/otlp-http/lib/opentelemetry/exporter/otlp/http/trace_exporter.rb @@ -30,7 +30,9 @@ class TraceExporter # 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) - def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'http://localhost:4318/v1/traces'), + DEFAULT_USER_AGENT = "OTel-OTLP-Exporter-Ruby/#{OpenTelemetry::Exporter::OTLP::HTTP::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})".freeze + + def initialize(endpoint: nil, certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'), client_certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE'), client_key_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY', 'OTEL_EXPORTER_OTLP_CLIENT_KEY'), @@ -39,24 +41,14 @@ def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPOR compression: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION', default: 'gzip'), timeout: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10), metrics_reporter: nil) - raise ArgumentError, "invalid url for OTLP::Exporter #{endpoint}" unless OpenTelemetry::Common::Utilities.valid_url?(endpoint) raise ArgumentError, "unsupported compression key #{compression}" unless compression.nil? || %w[gzip none].include?(compression) - @uri = if endpoint == ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] - URI("#{endpoint}/v1/traces") - else - URI(endpoint) - end + @uri = prepare_endpoint(endpoint) @http = http_connection(@uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_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 @@ -133,17 +125,21 @@ def around_request def send_bytes(bytes, timeout:) # rubocop:disable Metrics/MethodLength return FAILURE if bytes.nil? + @metrics_reporter.record_value('otel.otlp_exporter.message.uncompressed_size', value: bytes.bytesize) + retry_count = 0 timeout ||= @timeout start_time = OpenTelemetry::Common::Utilities.timeout_timestamp around_request do # rubocop:disable Metrics/BlockLength request = Net::HTTP::Post.new(@path) - request.body = if @compression == 'gzip' - request.add_field('Content-Encoding', 'gzip') - Zlib.gzip(bytes) - else - bytes - end + if @compression == 'gzip' + request.add_field('Content-Encoding', 'gzip') + body = Zlib.gzip(bytes) + @metrics_reporter.record_value('otel.otlp_exporter.message.compressed_size', value: body.bytesize) + else + body = bytes + end + request.body = body request.add_field('Content-Type', 'application/x-protobuf') @headers.each { |key, value| request.add_field(key, value) } @@ -168,6 +164,9 @@ def send_bytes(bytes, timeout:) # rubocop:disable Metrics/MethodLength response.body # Read and discard body redo if backoff?(retry_count: retry_count += 1, reason: response.code) FAILURE + when Net::HTTPNotFound + log_request_failure(response.code) + FAILURE when Net::HTTPBadRequest, Net::HTTPClientError, Net::HTTPServerError log_status(response.body) @metrics_reporter.add_to_counter('otel.otlp_exporter.failure', labels: { 'reason' => response.code }) @@ -226,6 +225,11 @@ def log_status(body) OpenTelemetry.handle_error(exception: e, message: 'unexpected error decoding rpc.Status in OTLP::Exporter#log_status') end + def log_request_failure(response_code) + OpenTelemetry.handle_error(message: "OTLP exporter received http.code=#{response_code} for uri='#{@uri}' in OTLP::Exporter#send_bytes") + @metrics_reporter.add_to_counter('otel.otlp_exporter.failure', labels: { 'reason' => response_code }) + end + def measure_request_duration start = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin @@ -265,6 +269,34 @@ def backoff?(retry_count:, reason:, retry_after: nil) true 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 prepare_endpoint(endpoint) + endpoint ||= ENV['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] + if endpoint.nil? + endpoint = ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] || 'http://localhost:4318' + endpoint += '/' unless endpoint.end_with?('/') + URI.join(endpoint, 'v1/traces') + elsif endpoint.strip.empty? + raise ArgumentError, "invalid url for OTLP::Exporter #{endpoint}" + else + URI(endpoint) + end + rescue URI::InvalidURIError + raise ArgumentError, "invalid url for OTLP::Exporter #{endpoint}" + end + def parse_headers(raw) entries = raw.split(',') raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if entries.empty? diff --git a/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb b/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb index 0281a6e975..7f4e639abf 100644 --- a/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb +++ b/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb @@ -11,11 +11,13 @@ let(:success) { OpenTelemetry::SDK::Trace::Export::SUCCESS } let(:export_failure) { OpenTelemetry::SDK::Trace::Export::FAILURE } + DEFAULT_USER_AGENT = OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter::DEFAULT_USER_AGENT + describe '#initialize' do it 'initializes with defaults' do exp = OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.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' @@ -27,6 +29,16 @@ _(http.port).must_equal 4318 end + it 'provides a useful, spec-compliant default user agent header' do + version = OpenTelemetry::Exporter::OTLP::HTTP::VERSION + # 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::HTTP::TraceExporter.new(endpoint: 'not a url') @@ -72,7 +84,7 @@ 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.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' @@ -98,7 +110,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' @@ -110,12 +122,79 @@ _(http.port).must_equal 4321 end + it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT has a trailing slash' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/' + ) do + OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new + end + _(exp.instance_variable_get(:@path)).must_equal '/v1/traces' + end + + it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT does not have a trailing slash' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234' + ) do + OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new + end + _(exp.instance_variable_get(:@path)).must_equal '/v1/traces' + end + + it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT does have a path without a trailing slash' do + exp = OpenTelemetry::TestHelpers.with_env( + # simulate OTLP endpoints built on top of an exiting API + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/api/v2/otlp' + ) do + OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new + end + _(exp.instance_variable_get(:@path)).must_equal '/api/v2/otlp/v1/traces' + end + + it 'does not join endpoint with v1/traces if endpoint is set and is equal to OTEL_EXPORTER_OTLP_ENDPOINT' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/custom/path' + ) do + OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new(endpoint: 'https://localhost:1234/custom/path') + end + _(exp.instance_variable_get(:@path)).must_equal '/custom/path' + end + + it 'does not append v1/traces if OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_TRACES_ENDPOINT both equal' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/custom/path', + 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' => 'https://localhost:1234/custom/path' + ) do + OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new + end + _(exp.instance_variable_get(:@path)).must_equal '/custom/path' + end + + it 'uses OTEL_EXPORTER_OTLP_TRACES_ENDPOINT over OTEL_EXPORTER_OTLP_ENDPOINT' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/non/specific/custom/path', + 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' => 'https://localhost:1234/specific/custom/path' + ) do + OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new + end + _(exp.instance_variable_get(:@path)).must_equal '/specific/custom/path' + end + + it 'uses endpoint over OTEL_EXPORTER_OTLP_TRACES_ENDPOINT and OTEL_EXPORTER_OTLP_ENDPOINT' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/non/specific/custom/path', + 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' => 'https://localhost:1234/specific/custom/path' + ) do + OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new(endpoint: 'https://localhost:1234/endpoint/custom/path') + end + _(exp.instance_variable_get(:@path)).must_equal '/endpoint/custom/path' + end + it 'restricts explicit headers to a String or Hash' do exp = OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.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::HTTP::TraceExporter.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 = _ do exp = OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new(headers: Object.new) @@ -124,58 +203,68 @@ _(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::HTTP::TraceExporter.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::HTTP::TraceExporter.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::HTTP::TraceExporter.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::HTTP::TraceExporter.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::HTTP::TraceExporter.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::HTTP::TraceExporter.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::HTTP::TraceExporter.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::HTTP::TraceExporter.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::HTTP::TraceExporter.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 '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::HTTP::TraceExporter.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 @@ -287,6 +376,16 @@ _(result).must_equal(success) end + it 'records metrics' do + metrics_reporter = Minitest::Mock.new + exporter = OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new(metrics_reporter: metrics_reporter) + stub_request(:post, 'http://localhost:4318/v1/traces').to_timeout.then.to_return(status: 200) + metrics_reporter.expect(:record_value, nil) { |m, _, _| m == 'otel.otlp_exporter.message.uncompressed_size' } + metrics_reporter.expect(:record_value, nil) { |m, _, _| m == 'otel.otlp_exporter.message.compressed_size' } + metrics_reporter.expect(:add_to_counter, nil) { |m, _, _| m == 'otel.otlp_exporter.failure' } + exporter.export([OpenTelemetry::TestHelpers.create_span_data]) + end + it 'retries on timeout' do stub_request(:post, 'http://localhost:4318/v1/traces').to_timeout.then.to_return(status: 200) span_data = OpenTelemetry::TestHelpers.create_span_data @@ -418,6 +517,25 @@ OpenTelemetry.logger = logger end + it 'logs a specific message when there is a 404' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/traces').to_return(status: 404, body: "Not Found\n") + span_data = OpenTelemetry::TestHelpers.create_span_data + + result = exporter.export([span_data]) + + _(log_stream.string).must_match( + %r{ERROR -- : OpenTelemetry error: OTLP exporter received http\.code=404 for uri='http://localhost:4318/v1/traces'} + ) + + _(result).must_equal(export_failure) + ensure + OpenTelemetry.logger = logger + end + it 'handles Zlib gzip compression errors' do stub_request(:post, 'http://localhost:4318/v1/traces').to_raise(Zlib::DataError.new('data error')) span_data = OpenTelemetry::TestHelpers.create_span_data From 5db2fc4be4e3f78b74ea35537a334bee6ee03f25 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Tue, 26 May 2026 09:56:12 +0530 Subject: [PATCH 2/2] feat(otlp-http): remove metrics reporter and simplify trace exporter - Remove metrics_reporter parameter and all internal metrics recording - Remove private_constant declarations for KEEP_ALIVE_TIMEOUT, RETRY_COUNT, and ERROR_MESSAGE_INVALID_HEADERS - Simplify compression logic using ternary operator - Remove measure_request_duration method and inline request execution - Replace metrics recording with standard error logging in backoff - Remove metrics-related test case - Fix typo This change removes the internal metrics instrumentation from the OTLP HTTP trace exporter, simplifying the codebase and reducing dependencies. Signed-off-by: Arjun Rajappa --- .../exporter/otlp/http/trace_exporter.rb | 43 +++++-------------- .../exporter/otlp/http/trace_exporter_test.rb | 16 +------ 2 files changed, 11 insertions(+), 48 deletions(-) diff --git a/exporter/otlp-http/lib/opentelemetry/exporter/otlp/http/trace_exporter.rb b/exporter/otlp-http/lib/opentelemetry/exporter/otlp/http/trace_exporter.rb index 09d70f7b50..fb7a49dd10 100644 --- a/exporter/otlp-http/lib/opentelemetry/exporter/otlp/http/trace_exporter.rb +++ b/exporter/otlp-http/lib/opentelemetry/exporter/otlp/http/trace_exporter.rb @@ -25,12 +25,10 @@ class TraceExporter # rubocop:disable Metrics/ClassLength # Default timeouts in seconds. KEEP_ALIVE_TIMEOUT = 30 RETRY_COUNT = 5 - private_constant(:KEEP_ALIVE_TIMEOUT, :RETRY_COUNT) 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::HTTP::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})".freeze + DEFAULT_USER_AGENT = "OTel-OTLP-Exporter-Ruby/#{OpenTelemetry::Exporter::OTLP::HTTP::VERSION}".freeze def initialize(endpoint: nil, certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'), @@ -39,8 +37,7 @@ def initialize(endpoint: nil, ssl_verify_mode: fetch_ssl_verify_mode, headers: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', default: {}), compression: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION', default: 'gzip'), - timeout: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10), - metrics_reporter: nil) + timeout: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_TRACES_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10)) raise ArgumentError, "unsupported compression key #{compression}" unless compression.nil? || %w[gzip none].include?(compression) @uri = prepare_endpoint(endpoint) @@ -51,7 +48,6 @@ def initialize(endpoint: nil, @headers = prepare_headers(headers) @timeout = timeout.to_f @compression = compression - @metrics_reporter = metrics_reporter || OpenTelemetry::SDK::Trace::Export::MetricsReporter @shutdown = false end @@ -125,20 +121,17 @@ def around_request def send_bytes(bytes, timeout:) # rubocop:disable Metrics/MethodLength return FAILURE if bytes.nil? - @metrics_reporter.record_value('otel.otlp_exporter.message.uncompressed_size', value: bytes.bytesize) - retry_count = 0 timeout ||= @timeout start_time = OpenTelemetry::Common::Utilities.timeout_timestamp around_request do # rubocop:disable Metrics/BlockLength request = Net::HTTP::Post.new(@path) - if @compression == 'gzip' - request.add_field('Content-Encoding', 'gzip') - body = Zlib.gzip(bytes) - @metrics_reporter.record_value('otel.otlp_exporter.message.compressed_size', value: body.bytesize) - else - body = bytes - end + body = if @compression == 'gzip' + request.add_field('Content-Encoding', 'gzip') + Zlib.gzip(bytes) + else + bytes + end request.body = body request.add_field('Content-Type', 'application/x-protobuf') @headers.each { |key, value| request.add_field(key, value) } @@ -150,7 +143,7 @@ def send_bytes(bytes, timeout:) # rubocop:disable Metrics/MethodLength @http.read_timeout = remaining_timeout @http.write_timeout = remaining_timeout @http.start unless @http.started? - response = measure_request_duration { @http.request(request) } + response = @http.request(request) case response when Net::HTTPOK @@ -169,7 +162,6 @@ def send_bytes(bytes, timeout:) # rubocop:disable Metrics/MethodLength FAILURE when Net::HTTPBadRequest, Net::HTTPClientError, Net::HTTPServerError log_status(response.body) - @metrics_reporter.add_to_counter('otel.otlp_exporter.failure', labels: { 'reason' => response.code }) FAILURE when Net::HTTPRedirection @http.finish @@ -200,7 +192,6 @@ def send_bytes(bytes, timeout:) # rubocop:disable Metrics/MethodLength return FAILURE rescue StandardError => e OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#send_bytes') - @metrics_reporter.add_to_counter('otel.otlp_exporter.failure', labels: { 'reason' => e.class.to_s }) return FAILURE end ensure @@ -227,24 +218,10 @@ def log_status(body) def log_request_failure(response_code) OpenTelemetry.handle_error(message: "OTLP exporter received http.code=#{response_code} for uri='#{@uri}' in OTLP::Exporter#send_bytes") - @metrics_reporter.add_to_counter('otel.otlp_exporter.failure', labels: { 'reason' => response_code }) - end - - def measure_request_duration - start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - begin - response = yield - ensure - stop = Process.clock_gettime(Process::CLOCK_MONOTONIC) - duration_ms = 1000.0 * (stop - start) - @metrics_reporter.record_value('otel.otlp_exporter.request_duration', - value: duration_ms, - labels: { 'status' => response&.code || 'unknown' }) - end end def backoff?(retry_count:, reason:, retry_after: nil) - @metrics_reporter.add_to_counter('otel.otlp_exporter.failure', labels: { 'reason' => reason }) + OpenTelemetry.handle_error(message: "OTLP exporter backing off due to: #{reason}") return false if retry_count > RETRY_COUNT sleep_interval = nil diff --git a/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb b/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb index 7f4e639abf..2b1f42bc0a 100644 --- a/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb +++ b/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb @@ -33,10 +33,6 @@ version = OpenTelemetry::Exporter::OTLP::HTTP::VERSION # 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 @@ -142,7 +138,7 @@ it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT does have a path without a trailing slash' do exp = OpenTelemetry::TestHelpers.with_env( - # simulate OTLP endpoints built on top of an exiting API + # simulate OTLP endpoints built on top of an existing API 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/api/v2/otlp' ) do OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new @@ -376,16 +372,6 @@ _(result).must_equal(success) end - it 'records metrics' do - metrics_reporter = Minitest::Mock.new - exporter = OpenTelemetry::Exporter::OTLP::HTTP::TraceExporter.new(metrics_reporter: metrics_reporter) - stub_request(:post, 'http://localhost:4318/v1/traces').to_timeout.then.to_return(status: 200) - metrics_reporter.expect(:record_value, nil) { |m, _, _| m == 'otel.otlp_exporter.message.uncompressed_size' } - metrics_reporter.expect(:record_value, nil) { |m, _, _| m == 'otel.otlp_exporter.message.compressed_size' } - metrics_reporter.expect(:add_to_counter, nil) { |m, _, _| m == 'otel.otlp_exporter.failure' } - exporter.export([OpenTelemetry::TestHelpers.create_span_data]) - end - it 'retries on timeout' do stub_request(:post, 'http://localhost:4318/v1/traces').to_timeout.then.to_return(status: 200) span_data = OpenTelemetry::TestHelpers.create_span_data