Skip to content

Commit aa186f2

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 aa186f2

File tree

5 files changed

+128
-108
lines changed

5 files changed

+128
-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: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

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

56
module Appsignal
67
module Integrations
@@ -19,48 +20,25 @@ def self.init
1920

2021
return unless Appsignal.active?
2122

23+
hanami_app_config.middleware.use(
24+
::Rack::Events,
25+
[Appsignal::Rack::EventHandler.new]
26+
)
27+
hanami_app_config.middleware.use(Appsignal::Rack::HanamiMiddleware)
28+
2229
::Hanami::Action.prepend Appsignal::Integrations::HanamiIntegration
2330
end
2431
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-
)
3632

37-
transaction = Appsignal::Transaction.create(
38-
SecureRandom.uuid,
39-
Appsignal::Transaction::HTTP_REQUEST,
40-
request
41-
)
33+
module HanamiIntegration
34+
def call(env)
35+
super
36+
ensure
37+
transaction = env[::Appsignal::Rack::APPSIGNAL_TRANSACTION]
38+
return unless transaction
4239

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
40+
transaction.set_action_if_nil(self.class.name)
5041
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!
6442
end
6543
end
6644
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
module Appsignal
4+
module Rack
5+
class HanamiMiddleware < AbstractMiddleware
6+
def initialize(app, options = {})
7+
options[:request_class] ||= ::Hanami::Action::Request
8+
options[:params_method] ||= :params
9+
options[:instrument_span_name] ||= "process_action.hanami"
10+
super
11+
end
12+
13+
private
14+
15+
def params_for(request)
16+
super&.to_h
17+
end
18+
19+
def request_for(env)
20+
params = ::Hanami::Action.params_class.new(env)
21+
@request_class.new(
22+
:env => env,
23+
:params => params,
24+
:sessions_enabled => true
25+
)
26+
end
27+
end
28+
end
29+
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)