Skip to content

Commit acac4fd

Browse files
feat: Use Semconv Naming For ActionPack (#1224)
* feat: Use Semconv Naming For ActionPack Aligns the span name closer to the specification instead of using Ruby class/method naming syntax Fixes #961 * squash: remove conditional logic on rails version * squash: add leading slash * squash: fix gem version references for 3.0 * squash: relax the test a little * squash: added some docs and refactored the code a little * squash: tests and more docs * squash: spelling errors * squash: Re-add assertions
1 parent 1147bbc commit acac4fd

File tree

7 files changed

+166
-38
lines changed

7 files changed

+166
-38
lines changed

instrumentation/action_pack/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ See the table below for details of what [Rails Framework Hook Events](https://gu
4242
| - | - | - | - |
4343
| `process_action.action_controller` | :white_check_mark: | :x: | It modifies the existing Rack span |
4444

45+
## Semantic Conventions
46+
47+
This instrumentation generally uses [HTTP server semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/http-spans/) to update the existing Rack span.
48+
49+
For Rails 7.1+, the span name is updated to match the HTTP method and route that was matched for the request using [`ActionDispatch::Request#route_uri_pattern`](https://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-route_uri_pattern), e.g.: `GET /users/:id`
50+
51+
For older versions of Rails the span name is updated to match the HTTP method, controller, and action name that was the target of the request, e.g.: `GET /example/index`
52+
53+
> ![NOTE]: Users may override the `span_naming` option to default to Legacy Span Naming Behavior that uses the controller's class name and action in Ruby documentation syntax, e.g. `ExampleController#index`.
54+
55+
This instrumentation does not emit any custom attributes.
56+
57+
| Attribute Name | Type | Notes |
58+
| - | - | - |
59+
| `code.namespace` | String | `ActionController` class name |
60+
| `code.function` | String | `ActionController` action name e.g. `index`, `show`, `edit`, etc... |
61+
| `http.route` | String | (Rails 7.1+) the route that was matched for the request |
62+
| `http.target` | String | The `request.filtered_path` |
63+
4564
### Error Handling for Action Controller
4665

4766
If an error is triggered by Action Controller (such as a 500 internal server error), Action Pack will typically employ the default `ActionDispatch::PublicExceptions.new(Rails.public_path)` as the `exceptions_app`, as detailed in the [documentation](https://guides.rubyonrails.org/configuring.html#config-exceptions-app).

instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/handlers/action_controller.rb

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class ActionController
1313
# @param config [Hash] of instrumentation options
1414
def initialize(config)
1515
@config = config
16+
@span_naming = config.fetch(:span_naming)
1617
end
1718

1819
# Invoked by ActiveSupport::Notifications at the start of the instrumentation block
@@ -22,20 +23,11 @@ def initialize(config)
2223
# @param payload [Hash] the payload passed as a method argument
2324
# @return [Hash] the payload passed as a method argument
2425
def start(_name, _id, payload)
25-
rack_span = OpenTelemetry::Instrumentation::Rack.current_span
26+
span_name, attributes = to_span_name_and_attributes(payload)
2627

27-
request = payload[:request]
28-
29-
rack_span.name = "#{payload[:controller]}##{payload[:action]}" unless request.env['action_dispatch.exception']
30-
31-
attributes_to_append = {
32-
OpenTelemetry::SemanticConventions::Trace::CODE_NAMESPACE => String(payload[:controller]),
33-
OpenTelemetry::SemanticConventions::Trace::CODE_FUNCTION => String(payload[:action])
34-
}
35-
36-
attributes_to_append[OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET] = request.filtered_path if request.filtered_path != request.fullpath
37-
38-
rack_span.add_attributes(attributes_to_append)
28+
span = OpenTelemetry::Instrumentation::Rack.current_span
29+
span.name = span_name
30+
span.add_attributes(attributes)
3931
rescue StandardError => e
4032
OpenTelemetry.handle_error(exception: e)
4133
end
@@ -47,11 +39,37 @@ def start(_name, _id, payload)
4739
# @param payload [Hash] the payload passed as a method argument
4840
# @return [Hash] the payload passed as a method argument
4941
def finish(_name, _id, payload)
50-
rack_span = OpenTelemetry::Instrumentation::Rack.current_span
51-
rack_span.record_exception(payload[:exception_object]) if payload[:exception_object]
42+
span = OpenTelemetry::Instrumentation::Rack.current_span
43+
span.record_exception(payload[:exception_object]) if payload[:exception_object]
5244
rescue StandardError => e
5345
OpenTelemetry.handle_error(exception: e)
5446
end
47+
48+
private
49+
50+
# Extracts the span name and attributes from the payload
51+
#
52+
# @param payload [Hash] the payload passed from ActiveSupport::Notifications
53+
# @return [Array<String, Hash>] the span name and attributes
54+
def to_span_name_and_attributes(payload)
55+
request = payload[:request]
56+
http_route = request.route_uri_pattern if request.respond_to?(:route_uri_pattern)
57+
58+
attributes = {
59+
OpenTelemetry::SemanticConventions::Trace::CODE_NAMESPACE => String(payload[:controller]),
60+
OpenTelemetry::SemanticConventions::Trace::CODE_FUNCTION => String(payload[:action])
61+
}
62+
attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_ROUTE] = http_route if http_route
63+
attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET] = request.filtered_path if request.filtered_path != request.fullpath
64+
65+
if @span_naming == :semconv
66+
return ["#{request.method} #{http_route.gsub('(.:format)', '')}", attributes] if http_route
67+
68+
return ["#{request.method} /#{payload.dig(:params, :controller)}/#{payload.dig(:params, :action)}", attributes]
69+
end
70+
71+
["#{payload[:controller]}##{payload[:action]}", attributes]
72+
end
5573
end
5674
end
5775
end

instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/instrumentation.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,31 @@
77
module OpenTelemetry
88
module Instrumentation
99
module ActionPack
10-
# The Instrumentation class contains logic to detect and install the ActionPack instrumentation
10+
# The {OpenTelemetry::Instrumentation::ActionPack::Instrumentation} class contains logic to detect and install the ActionPack instrumentation
11+
#
12+
# Installation and configuration of this instrumentation is done within the
13+
# {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry/SDK#configure-instance_method OpenTelemetry::SDK#configure}
14+
# block, calling {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use use()}
15+
# or {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use_all use_all()}.
16+
#
17+
# ## Configuration keys and options
18+
#
19+
# ### `:span_naming`
20+
#
21+
# Specifies how the span names are set. Can be one of:
22+
#
23+
# - `:semconv` **(default)** - The span name will use HTTP semantic conventions '{method http.route}', for example `GET /users/:id`
24+
# - `:class` - The span name will appear as '<ActionController class name>#<action>',
25+
# for example `UsersController#show`.
26+
#
27+
# @example An explicit default configuration
28+
# OpenTelemetry::SDK.configure do |c|
29+
# c.use_all({
30+
# 'OpenTelemetry::Instrumentation::ActionPack' => {
31+
# span_naming: :class
32+
# },
33+
# })
34+
# end
1135
class Instrumentation < OpenTelemetry::Instrumentation::Base
1236
MINIMUM_VERSION = Gem::Version.new('6.1.0')
1337

@@ -17,6 +41,8 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
1741
patch
1842
end
1943

44+
option :span_naming, default: :semconv, validate: %i[semconv class]
45+
2046
present do
2147
defined?(::ActionController)
2248
end

instrumentation/action_pack/opentelemetry-instrumentation-action_pack.gemspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ Gem::Specification.new do |spec|
3434
spec.add_development_dependency 'minitest', '~> 5.0'
3535
spec.add_development_dependency 'opentelemetry-sdk', '~> 1.1'
3636
spec.add_development_dependency 'opentelemetry-test-helpers', '~> 0.3'
37-
spec.add_development_dependency 'rails', '>= 6.1'
3837
spec.add_development_dependency 'rake', '~> 13.0'
3938
spec.add_development_dependency 'rubocop', '~> 1.68.0'
4039
spec.add_development_dependency 'rubocop-performance', '~> 1.22.0'

instrumentation/action_pack/test/opentelemetry/instrumentation/action_pack/handlers/action_controller_test.rb

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737
_(last_response.body).must_equal 'actually ok'
3838
_(last_response.ok?).must_equal true
39-
_(span.name).must_equal 'ExampleController#ok'
4039
_(span.kind).must_equal :server
4140
_(span.status.ok?).must_equal true
4241

@@ -65,7 +64,7 @@
6564

6665
_(last_response.body).must_equal 'created new item'
6766
_(last_response.ok?).must_equal true
68-
_(span.name).must_equal 'ExampleController#new_item'
67+
_(span.name).must_match(/^GET/)
6968
_(span.kind).must_equal :server
7069
_(span.status.ok?).must_equal true
7170

@@ -82,24 +81,25 @@
8281
_(span.attributes['code.function']).must_equal 'new_item'
8382
end
8483

85-
it 'sets the span name when the controller raises an exception' do
86-
get 'internal_server_error'
84+
describe 'when encountering server side errors' do
85+
it 'sets semconv attributes' do
86+
get 'internal_server_error'
8787

88-
_(span.name).must_equal 'ExampleController#internal_server_error'
89-
_(span.kind).must_equal :server
90-
_(span.status.ok?).must_equal false
88+
_(span.kind).must_equal :server
89+
_(span.status.ok?).must_equal false
9190

92-
_(span.instrumentation_library.name).must_equal 'OpenTelemetry::Instrumentation::Rack'
93-
_(span.instrumentation_library.version).must_equal OpenTelemetry::Instrumentation::Rack::VERSION
91+
_(span.instrumentation_library.name).must_equal 'OpenTelemetry::Instrumentation::Rack'
92+
_(span.instrumentation_library.version).must_equal OpenTelemetry::Instrumentation::Rack::VERSION
9493

95-
_(span.attributes['http.method']).must_equal 'GET'
96-
_(span.attributes['http.host']).must_equal 'example.org'
97-
_(span.attributes['http.scheme']).must_equal 'http'
98-
_(span.attributes['http.target']).must_equal '/internal_server_error'
99-
_(span.attributes['http.status_code']).must_equal 500
100-
_(span.attributes['http.user_agent']).must_be_nil
101-
_(span.attributes['code.namespace']).must_equal 'ExampleController'
102-
_(span.attributes['code.function']).must_equal 'internal_server_error'
94+
_(span.attributes['http.method']).must_equal 'GET'
95+
_(span.attributes['http.host']).must_equal 'example.org'
96+
_(span.attributes['http.scheme']).must_equal 'http'
97+
_(span.attributes['http.target']).must_equal '/internal_server_error'
98+
_(span.attributes['http.status_code']).must_equal 500
99+
_(span.attributes['http.user_agent']).must_be_nil
100+
_(span.attributes['code.namespace']).must_equal 'ExampleController'
101+
_(span.attributes['code.function']).must_equal 'internal_server_error'
102+
end
103103
end
104104

105105
it 'does not set the span name when an exception is raised in middleware' do
@@ -139,13 +139,79 @@
139139
_(span.attributes['code.function']).must_be_nil
140140
end
141141

142+
describe 'span naming' do
143+
describe 'when using the default span_naming configuration' do
144+
describe 'successful requests' do
145+
describe 'Rails Version < 7.1' do
146+
it 'uses the http method controller and action name' do
147+
skip "Rails #{Rails.gem_version} uses ActionDispatch::Request#route_uri_pattern" if Rails.gem_version >= Gem::Version.new('7.1')
148+
get '/ok'
149+
150+
_(span.name).must_equal 'GET /example/ok'
151+
end
152+
153+
it 'excludes route params' do
154+
skip "Rails #{Rails.gem_version} uses ActionDispatch::Request#route_uri_pattern" if Rails.gem_version >= Gem::Version.new('7.1')
155+
get '/items/1234'
156+
157+
_(span.name).must_equal 'GET /example/item'
158+
end
159+
end
160+
161+
describe 'Rails Version >= 7.1' do
162+
it 'uses the Rails route' do
163+
skip "Rails #{Rails.gem_version} does not define ActionDispatch::Request#route_uri_pattern" if Rails.gem_version < Gem::Version.new('7.1')
164+
get '/ok'
165+
166+
_(span.name).must_equal 'GET /ok'
167+
end
168+
169+
it 'includes route params' do
170+
skip "Rails #{Rails.gem_version} does not define ActionDispatch::Request#route_uri_pattern" if Rails.gem_version < Gem::Version.new('7.1')
171+
get '/items/1234'
172+
173+
_(span.name).must_equal 'GET /items/:id'
174+
end
175+
end
176+
end
177+
178+
describe 'server errors' do
179+
it 'uses the http method controller and action name for server side errors' do
180+
skip "Rails #{Rails.gem_version} uses ActionDispatch::Request#route_uri_pattern" if Rails.gem_version >= Gem::Version.new('7.1')
181+
182+
get 'internal_server_error'
183+
184+
_(span.name).must_equal 'GET /example/internal_server_error'
185+
end
186+
187+
it 'uses the Rails route for server side errors' do
188+
skip "Rails #{Rails.gem_version} uses ActionDispatch::Request#route_uri_pattern" if Rails.gem_version < Gem::Version.new('7.1')
189+
190+
get 'internal_server_error'
191+
192+
_(span.name).must_equal 'GET /internal_server_error'
193+
end
194+
end
195+
end
196+
197+
describe 'when using the class span_naming' do
198+
let(:config) { { span_naming: :class } }
199+
200+
it 'uses the http method and controller name' do
201+
get '/ok'
202+
203+
_(span.name).must_equal 'ExampleController#ok'
204+
end
205+
end
206+
end
207+
142208
describe 'when the application has exceptions_app configured' do
143209
let(:rails_app) { AppConfig.initialize_app(use_exceptions_app: true) }
144210

145211
it 'does not overwrite the span name from the controller that raised' do
146212
get 'internal_server_error'
147213

148-
_(span.name).must_equal 'ExampleController#internal_server_error'
214+
_(span.name).must_match(/^GET/)
149215
_(span.kind).must_equal :server
150216
_(span.status.ok?).must_equal false
151217

instrumentation/action_pack/test/test_helpers/controllers/example_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ def new_item
2020
end
2121

2222
def internal_server_error
23-
raise :internal_server_error
23+
raise 'internal_server_error'
2424
end
2525
end

instrumentation/rails/test/instrumentation/opentelemetry/instrumentation/rails/patches/action_controller/metal_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
# Clear captured spans
1818
before { exporter.reset }
1919

20-
it 'sets the span name to the format: HTTP_METHOD /rails/route(.:format)' do
20+
it 'sets the span name to the format: HTTP_METHOD /rails/route' do
2121
get '/ok'
2222

2323
_(last_response.body).must_equal 'actually ok'
2424
_(last_response.ok?).must_equal true
25-
_(span.name).must_equal 'ExampleController#ok'
25+
_(span.name).must_match %r{GET.*/ok}
2626
_(span.kind).must_equal :server
2727
_(span.status.ok?).must_equal true
2828

0 commit comments

Comments
 (0)