Skip to content

Commit 3468923

Browse files
authored
Add spans
2 parents 5826715 + 3a38bc7 commit 3468923

File tree

12 files changed

+775
-69
lines changed

12 files changed

+775
-69
lines changed

instrumentation/grape/README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# OpenTelemetry Grape Instrumentation
22

3-
Todo: Add a description.
3+
The Grape instrumentation is a community-maintained instrumentation for [Grape](https://github.com/ruby-grape/grape), a REST-like API framework for Ruby.
4+
5+
It relies on the Grape built-in support for `ActiveSupport::Notifications` (more info [here](https://github.com/ruby-grape/grape#active-support-instrumentation)).
6+
7+
It currently supports the following events:
8+
9+
- `endpoint_run.grape`
10+
- `endpoint_render.grape`
11+
- `endpoint_run_filters.grape`
12+
- `format_response.grape`
413

514
## How do I get started?
615

@@ -22,6 +31,8 @@ OpenTelemetry::SDK.configure do |c|
2231
end
2332
```
2433

34+
Since Grape is "designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra", it is recommended to use this instrumentation along with the Rack, Rails and/or Sinatra instrumentations.
35+
2536
Alternatively, you can also call `use_all` to install all the available instrumentation.
2637

2738
```ruby
@@ -30,6 +41,32 @@ OpenTelemetry::SDK.configure do |c|
3041
end
3142
```
3243

44+
### Configuration options
45+
46+
#### `:ignored_events` (array)
47+
48+
Indicate if any events should not produce spans.
49+
50+
- Accepted values: `:endpoint_render`, `:endpoint_run_filters`, `:format_response`.
51+
- Defaults to `[]` (no ignored events).
52+
53+
Example:
54+
55+
```ruby
56+
OpenTelemetry::SDK.configure do |c|
57+
c.use 'OpenTelemetry::Instrumentation::Grape', { ignored_events: [:endpoint_run_filters] }
58+
end
59+
```
60+
61+
Note that the `endpoint_run` event cannot be ignored. If you need to disable the instrumentation, set `:enabled` to `false`:
62+
63+
```ruby
64+
OpenTelemetry::SDK.configure do |c|
65+
config = { 'OpenTelemetry::Instrumentation::Grape' => { enabled: false } }
66+
c.use_all(config)
67+
end
68+
```
69+
3370
## Examples
3471

3572
Example usage can be seen in the `./example/trace_demonstration.rb` file [here](https://github.com/open-telemetry/opentelemetry-ruby-contrib/blob/main/instrumentation/grape/example/trace_demonstration.rb)

instrumentation/grape/example/trace_demonstration.rb

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
require 'opentelemetry-instrumentation-grape'
1010
require 'grape'
1111

12-
# Export traces to console by default
12+
# Export traces to console
1313
ENV['OTEL_TRACES_EXPORTER'] ||= 'console'
1414

1515
OpenTelemetry::SDK.configure do |c|
16+
c.service_name = 'trace_demonstration'
1617
c.use 'OpenTelemetry::Instrumentation::Grape'
1718
end
1819

19-
# A basic Grape endpoint example
20+
# A basic Grape API example
2021
class ExampleAPI < Grape::API
2122
format :json
2223

@@ -27,12 +28,8 @@ class ExampleAPI < Grape::API
2728

2829
desc 'Return information about a user'
2930
# Filters
30-
before do
31-
sleep(0.01)
32-
end
33-
after do
34-
sleep(0.01)
35-
end
31+
before { sleep(0.01) }
32+
after { sleep(0.01) }
3633
params do
3734
requires :id, type: Integer, desc: 'User ID'
3835
end
@@ -42,9 +39,8 @@ class ExampleAPI < Grape::API
4239
end
4340

4441
# Set up fake Rack application
45-
builder = Rack::Builder.app do
46-
run ExampleAPI
47-
end
42+
builder = Rack::Builder.app { run ExampleAPI }
43+
app = Rack::MockRequest.new(builder)
4844

49-
Rack::MockRequest.new(builder).get('/hello')
50-
Rack::MockRequest.new(builder).get('/users/1')
45+
app.get('/hello')
46+
app.get('/users/1')
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
module Grape
10+
# Contains custom subscribers that implement the ActiveSupport::Notifications::Fanout::Subscribers::Evented
11+
# interface. Custom subscribers are needed to create a span at the start of an event, for example.
12+
#
13+
# Reference: https://github.com/rails/rails/blob/05cb63abdaf6101e6c8fb43119e2c0d08e543c28/activesupport/lib/active_support/notifications/fanout.rb#L320-L322
14+
module CustomSubscribers
15+
# Implements the ActiveSupport::Subscriber interface to instrument the start and finish of the endpoint_run event
16+
class EndpointRun
17+
# Runs at the start of the event that triggers the ActiveSupport::Notification
18+
def start(name, id, payload)
19+
EventHandler.endpoint_run_start(name, id, payload)
20+
end
21+
22+
# Runs at the end of the event that triggers the ActiveSupport::Notification
23+
def finish(name, id, payload)
24+
EventHandler.endpoint_run_finish(name, id, payload)
25+
end
26+
end
27+
end
28+
end
29+
end
30+
end
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
module Grape
10+
# Handles the events instrumented with ActiveSupport notifications.
11+
# These handlers contain all the logic needed to create and connect spans.
12+
class EventHandler
13+
class << self
14+
# Handles the start of the endpoint_run.grape event (the parent event), where the context is attached
15+
def endpoint_run_start(_name, _id, payload)
16+
name = span_name(payload[:endpoint])
17+
span = tracer.start_span(name, attributes: run_attributes(payload), kind: :server)
18+
token = OpenTelemetry::Context.attach(OpenTelemetry::Trace.context_with_span(span))
19+
20+
payload.merge!(__opentelemetry_span: span, __opentelemetry_ctx_token: token)
21+
end
22+
23+
# Handles the end of the endpoint_run.grape event (the parent event), where the context is detached
24+
def endpoint_run_finish(_name, _id, payload)
25+
span = payload.delete(:__opentelemetry_span)
26+
token = payload.delete(:__opentelemetry_ctx_token)
27+
return unless span && token
28+
29+
handle_payload_exception(span, payload[:exception_object]) if payload[:exception_object]
30+
31+
span.finish
32+
OpenTelemetry::Context.detach(token)
33+
end
34+
35+
# Handles the endpoint_render.grape event
36+
def endpoint_render(_name, start, _finish, _id, payload)
37+
name = span_name(payload[:endpoint])
38+
attributes = {
39+
'component' => 'template',
40+
'operation' => 'endpoint_render'
41+
}
42+
tracer.in_span(name, attributes: attributes, start_timestamp: start, kind: :server) do |span|
43+
handle_payload_exception(span, payload[:exception_object]) if payload[:exception_object]
44+
end
45+
end
46+
47+
# Handles the endpoint_run_filters.grape events
48+
def endpoint_run_filters(_name, start, finish, _id, payload)
49+
filters = payload[:filters]
50+
type = payload[:type]
51+
52+
# Prevent submitting empty filters
53+
return if (!filters || filters.empty?) || !type || (finish - start).zero?
54+
55+
name = span_name(payload[:endpoint])
56+
attributes = {
57+
'component' => 'web',
58+
'operation' => 'endpoint_run_filters',
59+
'grape.filter.type' => type.to_s
60+
}
61+
tracer.in_span(name, attributes: attributes, start_timestamp: start, kind: :server) do |span|
62+
handle_payload_exception(span, payload[:exception_object]) if payload[:exception_object]
63+
end
64+
end
65+
66+
# Handles the format_response.grape event
67+
def format_response(_name, start, _finish, _id, payload)
68+
endpoint = payload[:env]['api.endpoint']
69+
name = span_name(endpoint)
70+
attributes = {
71+
'component' => 'template',
72+
'operation' => 'format_response',
73+
'grape.formatter.type' => formatter_type(payload[:formatter])
74+
}
75+
tracer.in_span(name, attributes: attributes, start_timestamp: start, kind: :server) do |span|
76+
handle_payload_exception(span, payload[:exception_object]) if payload[:exception_object]
77+
end
78+
end
79+
80+
private
81+
82+
def tracer
83+
Grape::Instrumentation.instance.tracer
84+
end
85+
86+
def span_name(endpoint)
87+
"#{request_method(endpoint)} #{path(endpoint)}"
88+
end
89+
90+
def run_attributes(payload)
91+
endpoint = payload[:endpoint]
92+
path = path(endpoint)
93+
{
94+
'component' => 'web',
95+
'operation' => 'endpoint_run',
96+
'grape.route.endpoint' => endpoint.options[:for]&.base.to_s,
97+
'grape.route.path' => path,
98+
'grape.route.method' => endpoint.options[:method].first,
99+
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => request_method(endpoint),
100+
OpenTelemetry::SemanticConventions::Trace::HTTP_ROUTE => path
101+
}
102+
end
103+
104+
# ActiveSupport::Notifications will attach a `:exception_object` to the payload if there was
105+
# an error raised during the execution of the &block associated to the Notification.
106+
# This can be safely added to the span for tracing.
107+
def handle_payload_exception(span, exception)
108+
span.record_exception(exception)
109+
span.status = OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{exception.class}")
110+
end
111+
112+
def request_method(endpoint)
113+
endpoint.options[:method]&.first
114+
end
115+
116+
def path(endpoint)
117+
namespace = endpoint.routes.first.namespace
118+
version = endpoint.routes.first.options[:version] || ''
119+
prefix = endpoint.routes.first.options[:prefix]&.to_s || ''
120+
parts = [prefix, version] + namespace.split('/') + endpoint.options[:path]
121+
parts.reject { |p| p.blank? || p.eql?('/') }.join('/').prepend('/')
122+
end
123+
124+
def formatter_type(formatter)
125+
basename = formatter.name.split('::').last
126+
# Convert from CamelCase to snake_case
127+
basename.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
128+
end
129+
end
130+
end
131+
end
132+
end
133+
end

instrumentation/grape/lib/opentelemetry/instrumentation/grape/handler.rb

Lines changed: 0 additions & 44 deletions
This file was deleted.

instrumentation/grape/lib/opentelemetry/instrumentation/grape/instrumentation.rb

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Instrumentation
99
module Grape
1010
# The Instrumentation class contains logic to detect and install the Grape instrumentation
1111
class Instrumentation < OpenTelemetry::Instrumentation::Base
12+
# Minimum Grape version needed for compatibility with this instrumentation
1213
MINIMUM_VERSION = Gem::Version.new('1.2.0')
1314

1415
install do |_config|
@@ -21,18 +22,10 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
2122
end
2223

2324
compatible do
24-
# ActiveSupport::Notifications were introduced in Grape 0.13.0
25-
# https://github.com/ruby-grape/grape/blob/master/CHANGELOG.md#0130-2015810
2625
gem_version >= MINIMUM_VERSION
2726
end
2827

29-
# Config options available in ddog Grape instrumentation
30-
option :enabled, default: true, validate: :boolean
31-
option :error_statuses, default: [], validate: :array
32-
# Config options necessary for OpenTelemetry::Instrumentation::ActiveSupport.subscribe called in railtie
33-
# instrumentation/active_support/lib/opentelemetry/instrumentation/active_support/span_subscriber.rb#L23
34-
# Used to validate if any of the payload keys are invalid
35-
option :disallowed_notification_payload_keys, default: [], validate: :array
28+
option :ignored_events, default: [], validate: :array
3629

3730
private
3831

@@ -41,11 +34,11 @@ def gem_version
4134
end
4235

4336
def require_dependencies
44-
require_relative 'handler'
37+
require_relative 'subscriber'
4538
end
4639

4740
def subscribe
48-
Handler.subscribe
41+
Subscriber.subscribe
4942
end
5043
end
5144
end

0 commit comments

Comments
 (0)