Skip to content

Commit c92de1a

Browse files
authored
Grape support (#562)
* Foundations for Grape support * Only instrument endpoint_run and pass app to Grape.start * Test Grape.start * Remove Grape app constant after test * Add documentation for app parameter to Grape.start * Add documentation for getting started with Grape * Add documentation for configuring Grape * Test passing config values to Grape.start * Resolve rubocop complaints * Add Grape.start documentation to API docs * Make method name clearer * Grape should be tested as a framework in Jenkins * Fix spacing * Correct documentation of config options order * Support installing and testing against multiple frameworks * Update framework info in transaction context for Grape * Add rails and grape spec * Grape github repo name has a different pattern than the other frameworks * Only test grape master on newest ruby * Add message that grape test is skipped * Test Grape and Rails together * No need for SpecLogger in rails-grape spec * Fix formatting * Remove unncessary test settings * Move requiring gemspec in Gemfile so framework modules are required automatically * Remove one more unnecessary require * Revert docs change about order that config options are applied * Move changelog entry to asciidoc file * Be explicit that scenario is grape and rack without rails
1 parent 3354604 commit c92de1a

17 files changed

+421
-19
lines changed

.ci/.jenkins_exclude.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,40 @@ exclude:
6161
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.1-7-jdk
6262
FRAMEWORK: sinatra-master
6363

64+
- RUBY_VERSION: ruby:2.5
65+
FRAMEWORK: grape-master
66+
- RUBY_VERSION: ruby:2.4
67+
FRAMEWORK: grape-master
68+
- RUBY_VERSION: ruby:2.3
69+
FRAMEWORK: grape-master
70+
- RUBY_VERSION: jruby:9.2
71+
FRAMEWORK: grape-master
72+
- RUBY_VERSION: jruby:9.1
73+
FRAMEWORK: grape-master
74+
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.2-12-jdk
75+
FRAMEWORK: grape-master
76+
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.2-11-jdk
77+
FRAMEWORK: grape-master
78+
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.2-8-jdk
79+
FRAMEWORK: grape-master
80+
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.1-7-jdk
81+
FRAMEWORK: grape-master
82+
83+
- RUBY_VERSION: ruby:2.5
84+
FRAMEWORK: grape-1.2,rails-6.0
85+
- RUBY_VERSION: ruby:2.4
86+
FRAMEWORK: grape-1.2,rails-6.0
87+
- RUBY_VERSION: ruby:2.3
88+
FRAMEWORK: grape-1.2,rails-6.0
89+
- RUBY_VERSION: jruby:9.2
90+
FRAMEWORK: grape-1.2,rails-6.0
91+
- RUBY_VERSION: jruby:9.1
92+
FRAMEWORK: grape-1.2,rails-6.0
93+
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.2-12-jdk
94+
FRAMEWORK: grape-1.2,rails-6.0
95+
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.2-11-jdk
96+
FRAMEWORK: grape-1.2,rails-6.0
97+
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.2-8-jdk
98+
FRAMEWORK: grape-1.2,rails-6.0
99+
- RUBY_VERSION: docker.elastic.co/observability-ci/jruby:9.1-7-jdk
100+
FRAMEWORK: grape-1.2,rails-6.0

.ci/.jenkins_framework.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ FRAMEWORK:
77

88
- sinatra-2.0
99
- sinatra-1.4
10+
11+
- grape-1.2
12+
13+
- grape-1.2,rails-6.0

.ci/.jenkins_master_framework.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
FRAMEWORK:
22
- rails-master
33
- sinatra-master
4+
- grape-master

CHANGELOG.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ endif::[]
1717

1818
[float]
1919
===== Added
20-
- Feature {pull}2526[#2526]
20+
- Add Grape support ([#562](https://github.com/elastic/apm-agent-ruby/pull/562))
2121

2222
[float]
2323
===== Changed

Gemfile

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ source 'https://rubygems.org'
44

55
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
66

7-
gemspec
8-
97
gem 'rack-test'
108
gem 'rspec'
119
gem 'rspec-its'
@@ -33,25 +31,37 @@ else
3331
gem 'sqlite3'
3432
end
3533

36-
framework, *version = ENV.fetch('FRAMEWORK', 'rails').split('-')
37-
version = version.join('-')
34+
## Install Framework
35+
GITHUB_REPOS = { 'grape' => 'ruby-grape/grape'}
3836

39-
case version
40-
when 'master'
41-
gem framework, github: "#{framework}/#{framework}"
42-
when /.+/
43-
gem framework, "~> #{version}.0"
44-
else
45-
gem framework
37+
frameworks = ENV.fetch('FRAMEWORK', 'rails').split(',')
38+
frameworks_versions = frameworks.inject({}) do |frameworks, str|
39+
framework, *version = str.split('-')
40+
frameworks.merge(framework => version.join('-'))
4641
end
4742

48-
gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby
43+
frameworks_versions.each do |framework, version|
44+
case version
45+
when 'master'
46+
gem framework, github: (GITHUB_REPOS[framework] || "#{framework}/#{framework}")
47+
when /.+/
48+
gem framework, "~> #{version}.0"
49+
else
50+
gem framework
51+
end
52+
end
4953

50-
unless version =~ /^(master|6)/
51-
gem 'delayed_job', require: nil
54+
if frameworks_versions.key?('rails')
55+
unless frameworks_versions['rails'] =~ /^(master|6)/
56+
gem 'delayed_job', require: nil
57+
end
5258
end
5359

60+
gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby
61+
5462
group :bench do
5563
gem 'ruby-prof', require: nil, platforms: %i[ruby]
5664
gem 'stackprof', require: nil, platforms: %i[ruby]
5765
end
66+
67+
gemspec

docs/api.asciidoc

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ Returns the built context.
193193

194194
[float]
195195
[[rails-start]]
196-
=== Manually hooking into Rails
196+
=== Rails
197197

198-
Start the agent and hook into Rails explicitly. This is useful if you skip requiring
198+
Start the agent and hook into Rails manually. This is useful if you skip requiring
199199
the gem and using the `Railtie`.
200200

201201
[source,ruby]
@@ -212,6 +212,16 @@ Start the agent and hook into Sinatra.
212212
[source,ruby]
213213
----
214214
ElasticAPM::Sinatra.start(MySinatraApp, server_url: 'http://localhost:8200')
215+
216+
[float]
217+
[[grape-start]]
218+
=== Grape
219+
220+
Start the agent and hook into Grape.
221+
222+
[source,ruby]
223+
----
224+
ElasticAPM::Grape.start(MyGrapeApp, server_url: 'http://localhost:8200')
215225
----
216226
217227
[float]

docs/configuration.asciidoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,23 @@ ElasticAPM::Sinatra.start(
6666

6767
See <<getting-started-rack>>.
6868

69+
[float]
70+
=== Grape and Rack
71+
72+
When using APM with Grape and Rack (without Rails), you can configure it when starting
73+
the agent:
74+
75+
[source,ruby]
76+
----
77+
# config.ru or similar
78+
ElasticAPM::Grape.start(
79+
MyApp,
80+
service_name: 'SomeOtherName'
81+
)
82+
----
83+
84+
See <<getting-started-rack>>.
85+
6986
[float]
7087
=== Options
7188

docs/getting-started-rack.asciidoc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,27 @@ run MySinatraApp
7272
at_exit { ElasticAPM.stop }
7373
----
7474

75+
[float]
76+
[[getting-started-grape]]
77+
==== Grape example
78+
79+
[source,ruby]
80+
----
81+
# Example config.ru
82+
83+
require 'grape'
84+
85+
module Twitter
86+
class API < Grape::API
87+
use ElasticAPM::Middleware
88+
89+
# ...
90+
end
91+
end
92+
93+
# Start the agent and hook in your app
94+
ElasticAPM::Grape.start(Twitter::API, config)
95+
96+
run Twitter::API
97+
98+
----

lib/elastic_apm.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
require 'elastic_apm/railtie' if defined?(::Rails::Railtie)
1717
require 'elastic_apm/sinatra' if defined?(::Sinatra)
18+
require 'elastic_apm/grape' if defined?(::Grape)
1819

1920
# ElasticAPM
2021
module ElasticAPM # rubocop:disable Metrics/ModuleLength

lib/elastic_apm/grape.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require 'elastic_apm/subscriber'
4+
require 'elastic_apm/normalizers/grape'
5+
6+
module ElasticAPM
7+
# Module for starting the ElasticAPM agent and hooking into Grape.
8+
module Grape
9+
extend self
10+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
11+
# Start the ElasticAPM agent and hook into Grape.
12+
#
13+
# @param app [Grape::API] A Grape app.
14+
# @param config [Config, Hash] An instance of Config or a Hash config.
15+
# @return [true, nil] true if the agent was started, nil otherwise.
16+
def start(app, config = {})
17+
config = Config.new(config) unless config.is_a?(Config)
18+
configure_app(app, config)
19+
20+
ElasticAPM.start(config).tap do |agent|
21+
attach_subscriber(agent)
22+
end
23+
ElasticAPM.running?
24+
rescue StandardError => e
25+
config.logger.error format('Failed to start: %s', e.message)
26+
config.logger.debug "Backtrace:\n" + e.backtrace.join("\n")
27+
end
28+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
29+
30+
private
31+
32+
def configure_app(app, config)
33+
config.service_name ||= app.name
34+
config.framework_name ||= 'Grape'
35+
config.framework_version ||= ::Grape::VERSION
36+
config.logger ||= app.logger
37+
config.__root_path ||= Dir.pwd
38+
end
39+
40+
def attach_subscriber(agent)
41+
return unless agent
42+
43+
agent.instrumenter.subscriber = ElasticAPM::Subscriber.new(agent)
44+
end
45+
end
46+
end

lib/elastic_apm/middleware.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def initialize(app)
1313
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
1414
def call(env)
1515
begin
16-
if running? && !path_ignored?(env)
16+
if running? && !path_ignored?(env) && !ElasticAPM.current_transaction
1717
transaction = start_transaction(env)
1818
end
1919

lib/elastic_apm/normalizers/grape.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
%w[endpoint_run].each do |lib|
4+
require "elastic_apm/normalizers/grape/#{lib}"
5+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
module ElasticAPM
4+
module Normalizers
5+
module Grape
6+
# @api private
7+
class EndpointRun < Normalizer
8+
register 'endpoint_run.grape'
9+
10+
TYPE = 'app'
11+
SUBTYPE = 'resource'
12+
13+
FRAMEWORK_NAME = 'Grape'
14+
15+
def normalize(transaction, _name, payload)
16+
transaction.name = endpoint(payload[:env])
17+
unless transaction.config.framework_name == FRAMEWORK_NAME
18+
transaction.context.set_service(framework_name: FRAMEWORK_NAME,
19+
framework_version: ::Grape::VERSION)
20+
end
21+
[transaction.name, TYPE, SUBTYPE, nil, nil]
22+
end
23+
24+
private
25+
26+
def endpoint(env)
27+
route_name = env['api.endpoint']&.routes&.first&.pattern&.origin ||
28+
env['REQUEST_PATH']
29+
[env['REQUEST_METHOD'], route_name].join(' ')
30+
end
31+
end
32+
end
33+
end
34+
end

spec/elastic_apm/grape_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
if defined?(Grape)
4+
RSpec.describe Grape do
5+
describe '.start' do
6+
before do
7+
class GrapeTestApp < ::Grape::API
8+
use ElasticAPM::Middleware
9+
end
10+
ElasticAPM::Grape.start(GrapeTestApp, config)
11+
end
12+
13+
after do
14+
ElasticAPM.stop
15+
Object.send(:remove_const, :GrapeTestApp)
16+
end
17+
18+
context 'with no overridden config settings' do
19+
let(:config) { {} }
20+
it 'starts the agent' do
21+
expect(ElasticAPM::Agent).to be_running
22+
end
23+
end
24+
25+
context 'a config with settings' do
26+
let(:config) { { service_name: 'Other Name' } }
27+
28+
it 'sets the options' do
29+
expect(ElasticAPM.agent.config.options[:service_name].value)
30+
.to eq('Other Name')
31+
end
32+
end
33+
end
34+
end
35+
end

0 commit comments

Comments
 (0)