Skip to content

Commit cb883ed

Browse files
feat: Custom Metrics Reporter Support for Zipkin (#1382)
Long overdue follow up task to make it possible to add a custom metrics reporter via the SDK Configurator. This will make initializing Zipkin exporter and the OTLP Exporter symmetric and reduce complexity in #1110
1 parent 008d297 commit cb883ed

File tree

3 files changed

+96
-3
lines changed

3 files changed

+96
-3
lines changed

exporter/zipkin/lib/opentelemetry/exporter/zipkin/exporter.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class Exporter # rubocop:disable Metrics/ClassLength
2929

3030
def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_ZIPKIN_ENDPOINT', default: 'http://localhost:9411/api/v2/spans'),
3131
headers: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_ZIPKIN_TRACES_HEADERS', 'OTEL_EXPORTER_ZIPKIN_HEADERS'),
32-
timeout: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_ZIPKIN_TRACES_TIMEOUT', 'OTEL_EXPORTER_ZIPKIN_TIMEOUT', default: 10))
32+
timeout: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_ZIPKIN_TRACES_TIMEOUT', 'OTEL_EXPORTER_ZIPKIN_TIMEOUT', default: 10),
33+
metrics_reporter: nil)
3334
raise ArgumentError, "invalid url for Zipkin::Exporter #{endpoint}" unless OpenTelemetry::Common::Utilities.valid_url?(endpoint)
3435
raise ArgumentError, 'headers must be comma-separated k=v pairs or a Hash' unless valid_headers?(headers)
3536

@@ -50,6 +51,7 @@ def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPOR
5051
when Hash then headers
5152
end
5253

54+
@metrics_reporter = metrics_reporter || OpenTelemetry::SDK::Trace::Export::MetricsReporter
5355
@shutdown = false
5456
end
5557

@@ -66,6 +68,7 @@ def export(span_data, timeout: nil)
6668
zipkin_spans = encode_spans(span_data)
6769
send_spans(zipkin_spans, timeout: timeout)
6870
rescue StandardError => e
71+
@metrics_reporter.add_to_counter('otel.zipkin_exporter.failure', labels: { 'reason' => e.class.to_s })
6972
OpenTelemetry.handle_error(exception: e, message: 'unexpected error in Zipkin::Exporter#export')
7073
FAILURE
7174
end
@@ -130,7 +133,7 @@ def send_spans(zipkin_spans, timeout: nil) # rubocop:disable Metrics/CyclomaticC
130133
@http.write_timeout = remaining_timeout if WRITE_TIMEOUT_SUPPORTED
131134
@http.start unless @http.started?
132135

133-
response = @http.request(request)
136+
response = measure_request_duration { @http.request(request) }
134137
response.body # Read and discard body
135138
# in opentelemetry-js 200-399 is succcess, in opentelemetry-collector zipkin exporter,200-299 is a success
136139
# zipkin api docs list 202 as default success code
@@ -197,6 +200,19 @@ def backoff?(retry_after: nil, retry_count:, reason:)
197200
sleep(sleep_interval)
198201
true
199202
end
203+
204+
def measure_request_duration
205+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
206+
begin
207+
response = yield
208+
ensure
209+
stop = Process.clock_gettime(Process::CLOCK_MONOTONIC)
210+
duration_ms = 1000.0 * (stop - start)
211+
@metrics_reporter.record_value('otel.zipkin_exporter.request_duration',
212+
value: duration_ms,
213+
labels: { 'status' => response&.code || 'unknown' })
214+
end
215+
end
200216
end
201217
end
202218
end

exporter/zipkin/test/opentelemetry/exporters/zipkin/exporter_test.rb

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,18 @@
6666
_(http.address).must_equal 'localhost'
6767
_(http.port).must_equal 4321
6868
end
69+
70+
it 'accepts a custom metrics reporter' do
71+
custom_metrics_reporter = Object.new
72+
exp = OpenTelemetry::Exporter::Zipkin::Exporter.new(metrics_reporter: custom_metrics_reporter)
73+
_(exp.instance_variable_get(:@metrics_reporter)).must_equal(custom_metrics_reporter)
74+
end
6975
end
7076

7177
describe '#export' do
72-
let(:exporter) { OpenTelemetry::Exporter::Zipkin::Exporter.new }
78+
let(:metrics_reporter) { nil }
79+
80+
let(:exporter) { OpenTelemetry::Exporter::Zipkin::Exporter.new(metrics_reporter: metrics_reporter) }
7381

7482
before do
7583
OpenTelemetry.tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new
@@ -142,6 +150,45 @@
142150
OpenTelemetry.tracer_provider.shutdown
143151
assert_requested(stub_post)
144152
end
153+
154+
describe 'given a custom metrics reporter' do
155+
let(:metrics_reporter) { InMemoryMetricsReporter.new }
156+
157+
describe 'on successful exports' do
158+
it 'provides the metric reporter with the HTTP request duration for an export' do
159+
stub_request(:post, DEFAULT_ZIPKIN_COLLECTOR_ENDPOINT).to_return(status: 202)
160+
span_data = create_resource_span_data
161+
result = exporter.export([span_data], timeout: nil)
162+
_(result).must_equal(OpenTelemetry::SDK::Trace::Export::SUCCESS)
163+
164+
request_duration = metrics_reporter.records.first
165+
_(request_duration.fetch(:metric)).must_equal('otel.zipkin_exporter.request_duration')
166+
_(request_duration.fetch(:value)).must_be :>, 0
167+
_(request_duration.fetch(:labels)).must_equal('status' => '202')
168+
end
169+
end
170+
171+
describe 'when an error occurs' do
172+
it 'notifies the metric reporter when an export fails' do
173+
stub_request(:post, DEFAULT_ZIPKIN_COLLECTOR_ENDPOINT).to_raise(StandardError)
174+
span_data = create_resource_span_data
175+
result = exporter.export([span_data], timeout: nil)
176+
_(result).must_equal(OpenTelemetry::SDK::Trace::Export::FAILURE)
177+
178+
expect(metrics_reporter).wont_be :empty?
179+
180+
failed_count = metrics_reporter.counters.first
181+
_(failed_count.fetch(:metric)).must_equal('otel.zipkin_exporter.failure')
182+
_(failed_count.fetch(:value)).must_equal(1)
183+
_(failed_count.fetch(:labels)).must_equal('reason' => 'StandardError')
184+
185+
request_duration = metrics_reporter.records.first
186+
_(request_duration.fetch(:metric)).must_equal('otel.zipkin_exporter.request_duration')
187+
_(request_duration.fetch(:value)).must_be :>, 0
188+
_(request_duration.fetch(:labels)).must_equal('status' => 'unknown')
189+
end
190+
end
191+
end
145192
end
146193
end
147194

exporter/zipkin/test/test_helper.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
require 'minitest/autorun'
1313
require 'webmock/minitest'
1414

15+
OpenTelemetry.logger = Logger.new(File::NULL)
16+
1517
def create_span_data(status: nil, kind: nil, attributes: nil, total_recorded_attributes: 0, events: nil, total_recorded_events: 0, links: nil, total_recorded_links: 0, instrumentation_scope: OpenTelemetry::SDK::InstrumentationScope.new('vendorlib', '0.0.0'), trace_id: OpenTelemetry::Trace.generate_trace_id, trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT, tracestate: nil)
1618
OpenTelemetry::SDK::Trace::SpanData.new(
1719
'',
@@ -34,3 +36,31 @@ def create_span_data(status: nil, kind: nil, attributes: nil, total_recorded_att
3436
tracestate
3537
)
3638
end
39+
40+
# Test helper for Zipkin Exporter
41+
class InMemoryMetricsReporter
42+
include OpenTelemetry::SDK::Trace::Export::MetricsReporter
43+
attr_reader :counters, :records, :observes
44+
45+
def initialize
46+
@counters = []
47+
@records = []
48+
@observes = []
49+
end
50+
51+
def record_value(metric, value:, labels: {})
52+
@records << { metric: metric, value: value, labels: labels }
53+
end
54+
55+
def observe_value(metric, value:, labels: {})
56+
@observes << { metric: metric, value: value, labels: labels }
57+
end
58+
59+
def add_to_counter(metric, increment: 1, labels: {})
60+
@counters << { metric: metric, value: increment, labels: labels }
61+
end
62+
63+
def empty?
64+
@counters.empty? && @records.empty? && @observes.empty?
65+
end
66+
end

0 commit comments

Comments
 (0)