Skip to content

Commit e14d6b0

Browse files
feat!: Custom ActiveSupport Span Names (#1014)
* feat!: Custom ActiveSupport Span Names The current implementation of ActiveSupport instrumentation sets the span name to the reverse tokenized name, e.g. `render_template.action_view` is converted to `action_view render_template` This default behavior can sometimes seem counter intuitive for users who use ActiveSupport Notifications to instrument their own code or users who are familiar with Rails instrumentation names. This change does a few things to address the issues listed above: 1. Uses the notification name by default as oppossed to the legacy span name 2. Allows users to provide a custom span name formatter lambda 3. Provides a proc with backward compatible span name formatter `OpenTelemetry::Instrumentation::ActiveSupport::LEGACY_NAME_FORMATTER` See #957 * squash: Bolt on a few things * squash: would be great if the tests passed * squash: Linter
1 parent d4f5ca1 commit e14d6b0

File tree

5 files changed

+107
-29
lines changed

5 files changed

+107
-29
lines changed

instrumentation/active_support/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# OpenTelemetry ActiveSupport Instrumentation
2+
23
The Active Support instrumentation is a community-maintained instrumentation for the Active Support portion of the [Ruby on Rails][rails-home] web-application framework.
34

45
## How do I get started?
56

67
Install the gem using:
78

8-
```
9+
```console
10+
911
gem install opentelemetry-instrumentation-active_support
12+
1013
```
1114

1215
Or, if you use [bundler][bundler-home], include `opentelemetry-instrumentation-active_support` in your `Gemfile`.
@@ -17,21 +20,24 @@ To use the instrumentation, call `use` with the name of the instrumentation and
1720
to desired ActiveSupport notification:
1821

1922
```ruby
23+
2024
OpenTelemetry::SDK.configure do |c|
2125
c.use 'OpenTelemetry::Instrumentation::ActiveSupport'
2226
end
2327

24-
2528
tracer = OpenTelemetry.tracer_provider.tracer('my_app_or_gem', '0.1.0')
2629
::OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, 'bar.foo')
30+
2731
```
2832

2933
Alternatively, you can also call `use_all` to install all the available instrumentation.
3034

3135
```ruby
36+
3237
OpenTelemetry::SDK.configure do |c|
3338
c.use_all
3439
end
40+
3541
```
3642

3743
## Examples

instrumentation/active_support/lib/opentelemetry/instrumentation/active_support/span_subscriber.rb

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ module OpenTelemetry
88
module Instrumentation
99
# rubocop:disable Style/Documentation
1010
module ActiveSupport
11+
LEGACY_NAME_FORMATTER = ->(name) { name.split('.')[0..1].reverse.join(' ') }
12+
13+
# rubocop:disable Metrics/ParameterLists
1114
# The SpanSubscriber is a special ActiveSupport::Notification subscription
1215
# handler which turns notifications into generic spans, taking care to handle
1316
# context appropriately.
@@ -19,15 +22,17 @@ def self.subscribe(
1922
tracer,
2023
pattern,
2124
notification_payload_transform = nil,
22-
disallowed_notification_payload_keys = [],
23-
kind: nil
25+
disallowed_notification_payload_keys = nil,
26+
kind: nil,
27+
span_name_formatter: nil
2428
)
2529
subscriber = OpenTelemetry::Instrumentation::ActiveSupport::SpanSubscriber.new(
2630
name: pattern,
2731
tracer: tracer,
2832
notification_payload_transform: notification_payload_transform,
2933
disallowed_notification_payload_keys: disallowed_notification_payload_keys,
30-
kind: kind
34+
kind: kind,
35+
span_name_formatter: span_name_formatter
3136
)
3237

3338
subscriber_object = ::ActiveSupport::Notifications.subscribe(pattern, subscriber)
@@ -54,16 +59,19 @@ def self.subscribe(
5459
subscriber_object
5560
end
5661

62+
# rubocop:enable Metrics/ParameterLists
5763
class SpanSubscriber
5864
ALWAYS_VALID_PAYLOAD_TYPES = [TrueClass, FalseClass, String, Numeric, Symbol].freeze
5965

60-
def initialize(name:, tracer:, notification_payload_transform: nil, disallowed_notification_payload_keys: [], kind: nil)
61-
@span_name = name.split('.')[0..1].reverse.join(' ').freeze
66+
# rubocop:disable Metrics/ParameterLists
67+
def initialize(name:, tracer:, notification_payload_transform: nil, disallowed_notification_payload_keys: nil, kind: nil, span_name_formatter: nil)
68+
@span_name = safe_span_name_for(span_name_formatter, name).dup.freeze
6269
@tracer = tracer
6370
@notification_payload_transform = notification_payload_transform
64-
@disallowed_notification_payload_keys = disallowed_notification_payload_keys
71+
@disallowed_notification_payload_keys = Array(disallowed_notification_payload_keys)
6572
@kind = kind || :internal
6673
end
74+
# rubocop:enable Metrics/ParameterLists
6775

6876
def start(name, id, payload)
6977
span = @tracer.start_span(@span_name, kind: @kind)
@@ -128,6 +136,16 @@ def sanitized_value(value)
128136
value
129137
end
130138
end
139+
140+
# Helper method to try an shield the span name formatter from errors
141+
#
142+
# It wraps the user supplied formatter in a rescue block and returns the original name if a StandardError is raised by the formatter
143+
def safe_span_name_for(span_name_formatter, name)
144+
span_name_formatter&.call(name) || name
145+
rescue StandardError => e
146+
OpenTelemetry.handle_error(exception: e, message: 'Error calling span_name_formatter. Using default span name.')
147+
name
148+
end
131149
end
132150
end
133151
end

instrumentation/active_support/opentelemetry-instrumentation-active_support.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
3838
spec.add_development_dependency 'pry-byebug'
3939
spec.add_development_dependency 'rails', '>= 6.1'
4040
spec.add_development_dependency 'rake', '~> 13.0'
41+
spec.add_development_dependency 'rspec-mocks'
4142
spec.add_development_dependency 'rubocop', '~> 1.64.0'
4243
spec.add_development_dependency 'rubocop-performance', '~> 1.20'
4344
spec.add_development_dependency 'simplecov', '~> 0.17.1'

instrumentation/active_support/test/opentelemetry/instrumentation/active_support/span_subscriber_test.rb

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
let(:exporter) { EXPORTER }
1313
let(:last_span) { exporter.finished_spans.last }
1414
let(:span_kind) { nil }
15+
let(:notification_name) { 'bar.foo' }
1516
let(:subscriber) do
1617
OpenTelemetry::Instrumentation::ActiveSupport::SpanSubscriber.new(
17-
name: 'bar.foo',
18+
name: notification_name,
1819
tracer: tracer,
1920
kind: span_kind
2021
)
@@ -36,7 +37,7 @@ def finish(name, id, payload)
3637

3738
it 'memoizes the span name' do
3839
span, = subscriber.start('oh.hai', 'abc', {})
39-
_(span.name).must_equal('foo bar')
40+
_(span.name).must_equal(notification_name)
4041
end
4142

4243
it 'uses the provided tracer' do
@@ -115,7 +116,7 @@ def finish(name, id, payload)
115116
describe 'instrumentation option - disallowed_notification_payload_keys' do
116117
let(:subscriber) do
117118
OpenTelemetry::Instrumentation::ActiveSupport::SpanSubscriber.new(
118-
name: 'bar.foo',
119+
name: notification_name,
119120
tracer: tracer,
120121
notification_payload_transform: nil,
121122
disallowed_notification_payload_keys: [:foo]
@@ -153,7 +154,7 @@ def finish(name, id, payload)
153154
let(:transformer_proc) { ->(v) { v.transform_values { 'optimus prime' } } }
154155
let(:subscriber) do
155156
OpenTelemetry::Instrumentation::ActiveSupport::SpanSubscriber.new(
156-
name: 'bar.foo',
157+
name: notification_name,
157158
tracer: tracer,
158159
notification_payload_transform: transformer_proc,
159160
disallowed_notification_payload_keys: [:foo]
@@ -205,58 +206,109 @@ def finish(name, id, payload)
205206

206207
describe 'instrument' do
207208
before do
208-
ActiveSupport::Notifications.unsubscribe('bar.foo')
209+
ActiveSupport::Notifications.unsubscribe(notification_name)
209210
end
210211

211212
it 'does not trace an event by default' do
212-
ActiveSupport::Notifications.subscribe('bar.foo') do
213+
ActiveSupport::Notifications.subscribe(notification_name) do
213214
# pass
214215
end
215-
ActiveSupport::Notifications.instrument('bar.foo', extra: 'context')
216+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
216217
_(last_span).must_be_nil
217218
end
218219

219220
it 'traces an event when a span subscriber is used' do
220-
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, 'bar.foo')
221-
ActiveSupport::Notifications.instrument('bar.foo', extra: 'context')
221+
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, notification_name)
222+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
222223

223224
_(last_span).wont_be_nil
224-
_(last_span.name).must_equal('foo bar')
225+
_(last_span.name).must_equal(notification_name)
225226
_(last_span.attributes['extra']).must_equal('context')
226227
_(last_span.kind).must_equal(:internal)
227228
end
228229

230+
describe 'when using a custom span name formatter' do
231+
describe 'when using the LEGACY_NAME_FORMATTER' do
232+
let(:span_name_formatter) { OpenTelemetry::Instrumentation::ActiveSupport::LEGACY_NAME_FORMATTER }
233+
it 'uses the user supplied formatter' do
234+
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, notification_name, nil, nil, span_name_formatter: span_name_formatter)
235+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
236+
237+
_(last_span).wont_be_nil
238+
_(last_span.name).must_equal('foo bar')
239+
_(last_span.attributes['extra']).must_equal('context')
240+
end
241+
end
242+
243+
describe 'when using a custom formatter' do
244+
let(:span_name_formatter) { ->(name) { "custom.#{name}" } }
245+
246+
it 'uses the user supplied formatter' do
247+
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, notification_name, nil, nil, span_name_formatter: span_name_formatter)
248+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
249+
250+
_(last_span).wont_be_nil
251+
_(last_span.name).must_equal('custom.bar.foo')
252+
_(last_span.attributes['extra']).must_equal('context')
253+
end
254+
end
255+
256+
describe 'when using a invalid formatter' do
257+
it 'defaults to the notification name' do
258+
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, notification_name, nil, nil, span_name_formatter: ->(_) {})
259+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
260+
261+
_(last_span).wont_be_nil
262+
_(last_span.name).must_equal(notification_name)
263+
_(last_span.attributes['extra']).must_equal('context')
264+
end
265+
end
266+
267+
describe 'when using a unstable formatter' do
268+
it 'defaults to the notification name' do
269+
allow(OpenTelemetry).to receive(:handle_error).with(exception: RuntimeError, message: String)
270+
271+
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, notification_name, nil, nil, span_name_formatter: ->(_) { raise 'boom' })
272+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
273+
274+
_(last_span).wont_be_nil
275+
_(last_span.name).must_equal(notification_name)
276+
_(last_span.attributes['extra']).must_equal('context')
277+
end
278+
end
279+
end
280+
229281
it 'finishes spans even when block subscribers blow up' do
230-
ActiveSupport::Notifications.subscribe('bar.foo') { raise 'boom' }
231-
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, 'bar.foo')
282+
ActiveSupport::Notifications.subscribe(notification_name) { raise 'boom' }
283+
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, notification_name)
232284

233285
expect do
234-
ActiveSupport::Notifications.instrument('bar.foo', extra: 'context')
286+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
235287
end.must_raise RuntimeError
236288

237289
_(last_span).wont_be_nil
238-
_(last_span.name).must_equal('foo bar')
290+
_(last_span.name).must_equal(notification_name)
239291
_(last_span.attributes['extra']).must_equal('context')
240292
end
241293

242294
it 'finishes spans even when complex subscribers blow up' do
243-
ActiveSupport::Notifications.subscribe('bar.foo', CrashingEndSubscriber.new)
244-
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, 'bar.foo')
295+
ActiveSupport::Notifications.subscribe(notification_name, CrashingEndSubscriber.new)
296+
OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, notification_name)
245297

246298
expect do
247-
ActiveSupport::Notifications.instrument('bar.foo', extra: 'context')
299+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
248300
end.must_raise RuntimeError
249301

250302
_(last_span).wont_be_nil
251-
_(last_span.name).must_equal('foo bar')
303+
_(last_span.name).must_equal(notification_name)
252304
_(last_span.attributes['extra']).must_equal('context')
253305
end
254306

255307
it 'supports unsubscribe' do
256-
obj = OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, 'bar.foo')
308+
obj = OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, notification_name)
257309
ActiveSupport::Notifications.unsubscribe(obj)
258310

259-
ActiveSupport::Notifications.instrument('bar.foo', extra: 'context')
311+
ActiveSupport::Notifications.instrument(notification_name, extra: 'context')
260312

261313
_(obj.class).must_equal(ActiveSupport::Notifications::Fanout::Subscribers::Evented)
262314
_(last_span).must_be_nil
@@ -267,7 +319,7 @@ def finish(name, id, payload)
267319
ActiveSupport::Notifications.instrument('bar.foo', extra: 'context')
268320

269321
_(last_span).wont_be_nil
270-
_(last_span.name).must_equal('foo bar')
322+
_(last_span.name).must_equal('bar.foo')
271323
_(last_span.attributes['extra']).must_equal('context')
272324
_(last_span.kind).must_equal(:client)
273325
end

instrumentation/active_support/test/test_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require 'opentelemetry-instrumentation-active_support'
1313

1414
require 'minitest/autorun'
15+
require 'rspec/mocks/minitest_integration'
1516
require 'webmock/minitest'
1617

1718
# global opentelemetry-sdk setup:

0 commit comments

Comments
 (0)