Skip to content

Commit 3f033c5

Browse files
author
Muriel Picone Farías
committed
Add instrumentation
1 parent 5826715 commit 3f033c5

File tree

12 files changed

+709
-69
lines changed

12 files changed

+709
-69
lines changed

instrumentation/grape/README.md

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
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 mostly 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`
412

513
## How do I get started?
614

@@ -22,6 +30,8 @@ OpenTelemetry::SDK.configure do |c|
2230
end
2331
```
2432

33+
Since Grape is "designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra", we recommend using it along with the Rack, Rails and/or Sinatra instrumentations.
34+
2535
Alternatively, you can also call `use_all` to install all the available instrumentation.
2636

2737
```ruby
@@ -30,6 +40,32 @@ OpenTelemetry::SDK.configure do |c|
3040
end
3141
```
3242

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

3571
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

+9-13
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')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 all custom subscriber classes that implement the ActiveSupport::Subscriber interface
11+
# Custom subscribers are needed to create a span at the start of an event, for example.
12+
module CustomSubscribers
13+
# Implements the ActiveSupport::Subscriber interface to instrument the start and finish of the endpoint_run event
14+
class EndpointRun
15+
# Runs at the start of the event that triggers the ActiveSupport::Notification
16+
def start(name, id, payload)
17+
EventHandler.endpoint_run_start(name, id, payload)
18+
end
19+
20+
# Runs at the end of the event that triggers the ActiveSupport::Notification
21+
def finish(name, id, payload)
22+
EventHandler.endpoint_run_finish(name, id, payload)
23+
end
24+
end
25+
end
26+
end
27+
end
28+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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!(
21+
__opentelemetry_span: span,
22+
__opentelemetry_ctx_token: token
23+
)
24+
end
25+
26+
# Handles the end of the endpoint_run.grape event (the parent event), where the context is detached
27+
def endpoint_run_finish(_name, _id, payload)
28+
span = payload.delete(:__opentelemetry_span)
29+
token = payload.delete(:__opentelemetry_ctx_token)
30+
return unless span && token
31+
32+
if payload[:exception_object]
33+
handle_error(span, payload[:exception_object])
34+
else
35+
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, payload[:endpoint].status)
36+
end
37+
38+
span.finish
39+
OpenTelemetry::Context.detach(token)
40+
end
41+
42+
# Handles the endpoint_render.grape event
43+
def endpoint_render(_name, start, _finish, _id, payload)
44+
name = span_name(payload[:endpoint])
45+
attributes = {
46+
'component' => 'template',
47+
'operation' => 'endpoint_render'
48+
}
49+
tracer.in_span(name, attributes: attributes, start_timestamp: start, kind: :server) do |span|
50+
handle_error(span, payload[:exception_object]) if payload[:exception_object]
51+
end
52+
end
53+
54+
# Handles the endpoint_run_filters.grape events
55+
def endpoint_run_filters(_name, start, finish, _id, payload)
56+
filters = payload[:filters]
57+
type = payload[:type]
58+
59+
# Prevent submitting empty filters
60+
zero_length = (finish - start).zero?
61+
return if (!filters || filters.empty?) || !type || zero_length
62+
63+
name = span_name(payload[:endpoint])
64+
attributes = {
65+
'component' => 'web',
66+
'operation' => 'endpoint_run_filters',
67+
'grape.filter.type' => type.to_s
68+
}
69+
tracer.in_span(name, attributes: attributes, start_timestamp: start, kind: :server) do |span|
70+
handle_error(span, payload[:exception_object]) if payload[:exception_object]
71+
end
72+
end
73+
74+
private
75+
76+
def tracer
77+
Grape::Instrumentation.instance.tracer
78+
end
79+
80+
def span_name(endpoint)
81+
"#{api_instance(endpoint)} #{request_method(endpoint)} #{path(endpoint)}"
82+
end
83+
84+
def run_attributes(payload)
85+
endpoint = payload[:endpoint]
86+
path = path(endpoint)
87+
{
88+
'component' => 'web',
89+
'operation' => 'endpoint_run',
90+
'grape.route.endpoint' => api_instance(endpoint),
91+
'grape.route.path' => path,
92+
'grape.route.method' => endpoint.options[:method].first,
93+
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => request_method(endpoint),
94+
OpenTelemetry::SemanticConventions::Trace::HTTP_ROUTE => path
95+
}
96+
end
97+
98+
def handle_error(span, exception)
99+
span.record_exception(exception)
100+
span.status = OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{exception.class}")
101+
return unless exception.respond_to?('status')
102+
103+
span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, exception.status)
104+
end
105+
106+
def api_instance(endpoint)
107+
endpoint.options[:for].base.to_s
108+
end
109+
110+
def request_method(endpoint)
111+
endpoint.options.fetch(:method).first
112+
end
113+
114+
def path(endpoint)
115+
namespace = endpoint.routes.first.namespace
116+
version = endpoint.routes.first.options[:version] || ''
117+
prefix = endpoint.routes.first.options[:prefix].to_s || ''
118+
parts = [prefix, version] + namespace.split('/') + endpoint.options[:path]
119+
parts.reject { |p| p.blank? || p.eql?('/') }.join('/').prepend('/')
120+
end
121+
end
122+
end
123+
end
124+
end
125+
end

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

-44
This file was deleted.

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

+6-11
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,13 @@ def gem_version
4134
end
4235

4336
def require_dependencies
44-
require_relative 'handler'
37+
require_relative 'subscriber'
38+
require_relative 'event_handler'
39+
require_relative 'custom_subscribers/endpoint_run'
4540
end
4641

4742
def subscribe
48-
Handler.subscribe
43+
Subscriber.subscribe
4944
end
5045
end
5146
end

0 commit comments

Comments
 (0)