Skip to content

Commit e79d427

Browse files
committed
Update Hanami to use Rack middleware
As part of #329, update the Hanami integration to use Rack middleware and the EventHandler to instrument requests made to Hanami apps. This standardizes the instrumentation as much as possible between Rack apps and minimizes our reliance on monkeypatches. The only monkeypatch that remains is setting the action name to the Action class name. I have found no other way yet to fetch this metadata from the request metadata, environment or the Hanami router. Part of #329 Mostly solves #911
1 parent b65d667 commit e79d427

File tree

5 files changed

+130
-108
lines changed

5 files changed

+130
-108
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
bump: patch
3+
type: add
4+
---
5+
6+
Improve instrumentation of Hanami requests by making sure the transaction is always closed.
7+
It will also report a `response_status` tag and metric for Hanami requests.

lib/appsignal/integrations/hanami.rb

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# frozen_string_literal: true
22

33
require "appsignal"
4+
require "appsignal/rack/hanami_middleware"
45

56
module Appsignal
67
module Integrations
8+
# @api private
79
module HanamiPlugin
810
def self.init
911
Appsignal.internal_logger.debug("Loading Hanami integration")
@@ -19,48 +21,25 @@ def self.init
1921

2022
return unless Appsignal.active?
2123

24+
hanami_app_config.middleware.use(
25+
::Rack::Events,
26+
[Appsignal::Rack::EventHandler.new]
27+
)
28+
hanami_app_config.middleware.use(Appsignal::Rack::HanamiMiddleware)
29+
2230
::Hanami::Action.prepend Appsignal::Integrations::HanamiIntegration
2331
end
2432
end
25-
end
26-
end
27-
28-
module Appsignal::Integrations::HanamiIntegration
29-
def call(env)
30-
params = ::Hanami::Action::BaseParams.new(env)
31-
request = ::Hanami::Action::Request.new(
32-
:env => env,
33-
:params => params,
34-
:sessions_enabled => true
35-
)
3633

37-
transaction = Appsignal::Transaction.create(
38-
SecureRandom.uuid,
39-
Appsignal::Transaction::HTTP_REQUEST,
40-
request
41-
)
34+
# @api private
35+
module HanamiIntegration
36+
def call(env)
37+
super
38+
ensure
39+
transaction = env[::Appsignal::Rack::APPSIGNAL_TRANSACTION]
4240

43-
begin
44-
Appsignal.instrument("process_action.hanami") do
45-
super.tap do |response|
46-
# TODO: update to response_status or remove:
47-
# https://github.com/appsignal/appsignal-ruby/issues/183
48-
transaction.set_metadata("status", response.status.to_s)
49-
end
41+
transaction&.set_action_if_nil(self.class.name)
5042
end
51-
rescue Exception => error # rubocop:disable Lint/RescueException
52-
transaction.set_error(error)
53-
# TODO: update to response_status or remove:
54-
# https://github.com/appsignal/appsignal-ruby/issues/183
55-
transaction.set_metadata("status", "500")
56-
raise error
57-
ensure
58-
transaction.set_params_if_nil(request.params.to_h)
59-
transaction.set_action_if_nil(self.class.name)
60-
transaction.set_metadata("path", request.path)
61-
transaction.set_metadata("method", request.request_method)
62-
transaction.set_http_or_background_queue_start
63-
Appsignal::Transaction.complete_current!
6443
end
6544
end
6645
end
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+
module Appsignal
4+
module Rack
5+
# @api private
6+
class HanamiMiddleware < AbstractMiddleware
7+
def initialize(app, options = {})
8+
options[:request_class] ||= ::Hanami::Action::Request
9+
options[:params_method] ||= :params
10+
options[:instrument_span_name] ||= "process_action.hanami"
11+
super
12+
end
13+
14+
private
15+
16+
def params_for(request)
17+
super&.to_h
18+
end
19+
20+
def request_for(env)
21+
params = ::Hanami::Action.params_class.new(env)
22+
@request_class.new(
23+
:env => env,
24+
:params => params,
25+
:sessions_enabled => true
26+
)
27+
end
28+
end
29+
end
30+
end

spec/lib/appsignal/integrations/hanami_spec.rb

Lines changed: 28 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,28 @@
1111
Appsignal::Integrations::HanamiPlugin.init
1212
end
1313

14-
it "prepends the integration to Hanami" do
14+
it "prepends the integration to Hanami::Action" do
1515
allow(Appsignal).to receive(:active?).and_return(true)
1616
Appsignal::Integrations::HanamiPlugin.init
1717
expect(::Hanami::Action.included_modules)
1818
.to include(Appsignal::Integrations::HanamiIntegration)
1919
end
2020

21+
it "adds middleware to the Hanami app" do
22+
allow(Appsignal).to receive(:active?).and_return(true)
23+
Appsignal::Integrations::HanamiPlugin.init
24+
25+
expect(::Hanami.app.config.middleware.stack[::Hanami::Router::DEFAULT_PREFIX])
26+
.to include(
27+
[Rack::Events, [[kind_of(Appsignal::Rack::EventHandler)]], nil],
28+
[Appsignal::Rack::HanamiMiddleware, [], nil]
29+
)
30+
end
31+
2132
context "when not active" do
2233
before { allow(Appsignal).to receive(:active?).and_return(false) }
2334

24-
it "does not prepend the integration" do
35+
it "does not prepend the integration to Hanami::Action" do
2536
Appsignal::Integrations::HanamiPlugin.init
2637
expect(::Hanami::Action).to_not receive(:prepend)
2738
.with(Appsignal::Integrations::HanamiIntegration)
@@ -49,19 +60,10 @@
4960
end
5061
end
5162

52-
describe "Hanami Actions" do
53-
let(:env) do
54-
Rack::MockRequest.env_for(
55-
"/books",
56-
"router.params" => router_params,
57-
:method => "GET"
58-
)
59-
end
60-
let(:router_params) { { "foo" => "bar", "baz" => "qux" } }
63+
describe Appsignal::Integrations::HanamiIntegration do
64+
let(:transaction) { http_request_transaction }
6165
around { |example| keep_transactions { example.run } }
62-
before :context do
63-
start_agent
64-
end
66+
before(:context) { start_agent }
6567
before do
6668
allow(Appsignal).to receive(:active?).and_return(true)
6769
Appsignal::Integrations::HanamiPlugin.init
@@ -73,72 +75,26 @@ def make_request(env, app: HanamiApp::Actions::Books::Index)
7375
end
7476

7577
describe "#call" do
76-
it "sets params" do
77-
make_request(env)
78-
79-
expect(last_transaction.to_h).to include(
80-
"sample_data" => hash_including(
81-
"params" => router_params
82-
)
83-
)
84-
end
85-
86-
it "sets the namespace and action name" do
87-
make_request(env)
88-
89-
expect(last_transaction.to_h).to include(
90-
"namespace" => Appsignal::Transaction::HTTP_REQUEST,
91-
"action" => "HanamiApp::Actions::Books::Index"
92-
)
93-
end
94-
95-
it "sets the metadata" do
96-
make_request(env)
97-
98-
expect(last_transaction.to_h).to include(
99-
"metadata" => hash_including(
100-
"status" => "200",
101-
"path" => "/books",
102-
"method" => "GET"
103-
)
104-
)
105-
end
78+
context "without an active transaction" do
79+
let(:env) { {} }
10680

107-
context "with queue start header" do
108-
let(:queue_start_time) { fixed_time * 1_000 }
109-
before do
110-
env["HTTP_X_REQUEST_START"] = "t=#{queue_start_time.to_i}" # in milliseconds
111-
end
112-
113-
it "sets the queue start" do
81+
it "does not set the action name" do
11482
make_request(env)
11583

116-
expect(last_transaction.ext.queue_start).to eq(queue_start_time)
84+
expect(transaction.to_h).to include(
85+
"action" => nil
86+
)
11787
end
11888
end
11989

120-
context "with error" do
121-
before do
122-
expect do
123-
make_request(env, :app => HanamiApp::Actions::Books::Error)
124-
end.to raise_error(ExampleException)
125-
end
90+
context "with an active transaction" do
91+
let(:env) { { Appsignal::Rack::APPSIGNAL_TRANSACTION => transaction } }
12692

127-
it "records the exception" do
128-
expect(last_transaction.to_h).to include(
129-
"error" => {
130-
"name" => "ExampleException",
131-
"message" => "exception message",
132-
"backtrace" => kind_of(String)
133-
}
134-
)
135-
end
93+
it "sets action name on the transaction" do
94+
make_request(env)
13695

137-
it "sets the status to 500" do
138-
expect(last_transaction.to_h).to include(
139-
"metadata" => hash_including(
140-
"status" => "500"
141-
)
96+
expect(transaction.to_h).to include(
97+
"action" => "HanamiApp::Actions::Books::Index"
14298
)
14399
end
144100
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
require "appsignal/rack/hanami_middleware"
2+
3+
if DependencyHelper.hanami2_present?
4+
describe Appsignal::Rack::HanamiMiddleware do
5+
let(:app) { double(:call => true) }
6+
let(:router_params) { { "param1" => "value1", "param2" => "value2" } }
7+
let(:env) do
8+
Rack::MockRequest.env_for(
9+
"/some/path",
10+
"router.params" => router_params
11+
)
12+
end
13+
let(:middleware) { Appsignal::Rack::HanamiMiddleware.new(app, {}) }
14+
15+
before(:context) { start_agent }
16+
around { |example| keep_transactions { example.run } }
17+
18+
def make_request(env)
19+
middleware.call(env)
20+
end
21+
22+
context "with params" do
23+
it "sets request parameters on the transaction" do
24+
make_request(env)
25+
26+
expect(last_transaction.to_h).to include(
27+
"sample_data" => hash_including(
28+
"params" => { "param1" => "value1", "param2" => "value2" }
29+
)
30+
)
31+
end
32+
end
33+
34+
it "reports a process_action.hanami event" do
35+
make_request(env)
36+
37+
expect(last_transaction.to_h).to include(
38+
"events" => [
39+
hash_including(
40+
"body" => "",
41+
"body_format" => Appsignal::EventFormatter::DEFAULT,
42+
"count" => 1,
43+
"name" => "process_action.hanami",
44+
"title" => ""
45+
)
46+
]
47+
)
48+
end
49+
end
50+
end

0 commit comments

Comments
 (0)