Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,29 @@ 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)

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}".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'),
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)
raise ArgumentError, "invalid url for OTLP::Exporter #{endpoint}" unless OpenTelemetry::Common::Utilities.valid_url?(endpoint)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why remove this validation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 = 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
@shutdown = false
end

Expand Down Expand Up @@ -138,12 +126,13 @@ def send_bytes(bytes, timeout:) # rubocop:disable Metrics/MethodLength
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
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) }

Expand All @@ -154,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
Expand All @@ -168,9 +157,11 @@ 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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is no metrics support, OpenTelemetry.handle_error seems enough.

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
Expand Down Expand Up @@ -201,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
Expand All @@ -226,21 +216,12 @@ def log_status(body)
OpenTelemetry.handle_error(exception: e, message: 'unexpected error decoding rpc.Status in OTLP::Exporter#log_status')
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
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")
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
Expand All @@ -265,6 +246,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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for adding protections for edge cases!

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?
Expand Down
Loading
Loading