appsignal 3.4.12 → 3.4.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Integrations
5
+ module DryMonitorIntegration
6
+ def instrument(event_id, payload = {}, &block)
7
+ Appsignal::Transaction.current.start_event
8
+
9
+ super
10
+ ensure
11
+ title, body, body_format = Appsignal::EventFormatter.format("#{event_id}.dry", payload)
12
+
13
+ Appsignal::Transaction.current.finish_event(
14
+ title || event_id.to_s,
15
+ title,
16
+ body,
17
+ body_format
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
@@ -36,9 +36,14 @@ module Appsignal
36
36
 
37
37
  Appsignal.start
38
38
 
39
- if Appsignal.config[:enable_rails_error_reporter] && Rails.respond_to?(:error) # rubocop:disable Style/GuardClause
40
- Rails.error.subscribe(Appsignal::Integrations::RailsErrorReporterSubscriber)
41
- end
39
+ initialize_error_reporter
40
+ end
41
+
42
+ def self.initialize_error_reporter
43
+ return unless Appsignal.config[:enable_rails_error_reporter]
44
+ return unless Rails.respond_to?(:error)
45
+
46
+ Rails.error.subscribe(Appsignal::Integrations::RailsErrorReporterSubscriber)
42
47
  end
43
48
  end
44
49
 
@@ -9,7 +9,7 @@ module Appsignal
9
9
  #
10
10
  # @api private
11
11
  class SidekiqErrorHandler
12
- def call(exception, sidekiq_context)
12
+ def call(exception, sidekiq_context, _sidekiq_config = nil)
13
13
  transaction =
14
14
  if Appsignal::Transaction.current?
15
15
  Appsignal::Transaction.current
@@ -78,9 +78,9 @@ module Appsignal
78
78
  redis_info = adapter.redis_info
79
79
  return unless redis_info
80
80
 
81
- gauge "connection_count", redis_info.fetch("connected_clients")
82
- gauge "memory_usage", redis_info.fetch("used_memory")
83
- gauge "memory_usage_rss", redis_info.fetch("used_memory_rss")
81
+ gauge "connection_count", redis_info["connected_clients"]
82
+ gauge "memory_usage", redis_info["used_memory"]
83
+ gauge "memory_usage_rss", redis_info["used_memory_rss"]
84
84
  end
85
85
 
86
86
  def track_stats
@@ -112,6 +112,8 @@ module Appsignal
112
112
 
113
113
  # Track a gauge metric with the `sidekiq_` prefix
114
114
  def gauge(key, value, tags = {})
115
+ return if value.nil?
116
+
115
117
  tags[:hostname] = hostname if hostname
116
118
  Appsignal.set_gauge "sidekiq_#{key}", value, tags
117
119
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "3.4.12"
4
+ VERSION = "3.4.14"
5
5
  end
@@ -436,7 +436,8 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
436
436
  "boot" => { "started" => { "result" => true } },
437
437
  "host" => {
438
438
  "uid" => { "result" => Process.uid },
439
- "gid" => { "result" => Process.gid }
439
+ "gid" => { "result" => Process.gid },
440
+ "running_in_container" => { "result" => Appsignal::Extension.running_in_container? }
440
441
  },
441
442
  "config" => { "valid" => { "result" => true } },
442
443
  "logger" => { "started" => { "result" => true } },
@@ -781,6 +782,9 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
781
782
  "file" => {},
782
783
  "env" => {},
783
784
  "override" => { "send_session_data" => true }
785
+ },
786
+ "modifiers" => {
787
+ "APPSIGNAL_INACTIVE_ON_CONFIG_FILE_ERROR" => ""
784
788
  }
785
789
  )
786
790
  end
@@ -912,6 +916,28 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
912
916
  end
913
917
  end
914
918
 
919
+ describe "modifiers" do
920
+ before do
921
+ ENV["APPSIGNAL_INACTIVE_ON_CONFIG_FILE_ERROR"] = "1"
922
+ run
923
+ end
924
+
925
+ it "outputs config modifiers" do
926
+ expect(output).to include(
927
+ "Configuration modifiers\n" \
928
+ " APPSIGNAL_INACTIVE_ON_CONFIG_FILE_ERROR: \"1\""
929
+ )
930
+ end
931
+
932
+ it "transmits config modifiers in report" do
933
+ expect(received_report["config"]).to include(
934
+ "modifiers" => {
935
+ "APPSIGNAL_INACTIVE_ON_CONFIG_FILE_ERROR" => "1"
936
+ }
937
+ )
938
+ end
939
+ end
940
+
915
941
  it "transmits config in report" do
916
942
  run
917
943
  additional_initial_config = {}
@@ -935,6 +961,9 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
935
961
  "file" => hash_with_string_keys(config.file_config),
936
962
  "env" => {},
937
963
  "override" => { "send_session_data" => true }
964
+ },
965
+ "modifiers" => {
966
+ "APPSIGNAL_INACTIVE_ON_CONFIG_FILE_ERROR" => ""
938
967
  }
939
968
  )
940
969
  end
@@ -963,6 +992,9 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
963
992
  "file" => hash_with_string_keys(config.file_config),
964
993
  "env" => {},
965
994
  "override" => { "send_session_data" => true }
995
+ },
996
+ "modifiers" => {
997
+ "APPSIGNAL_INACTIVE_ON_CONFIG_FILE_ERROR" => ""
966
998
  }
967
999
  )
968
1000
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Appsignal::EventFormatter::Rom::SqlFormatter do
4
+ let(:klass) { described_class }
5
+ let(:formatter) { klass.new }
6
+
7
+ it "registers the sql event formatter" do
8
+ expect(Appsignal::EventFormatter.registered?("sql.dry", klass)).to be_truthy
9
+ end
10
+
11
+ describe "#format" do
12
+ let(:payload) do
13
+ {
14
+ :name => "postgres",
15
+ :query => "SELECT * FROM users"
16
+ }
17
+ end
18
+ subject { formatter.format(payload) }
19
+
20
+ it { is_expected.to eq ["query.postgres", "SELECT * FROM users", 1] }
21
+ end
22
+ end
@@ -22,6 +22,12 @@ describe Appsignal::Hooks::ActiveSupportNotificationsHook do
22
22
 
23
23
  it_behaves_like "activesupport instrument override"
24
24
 
25
+ if defined?(::ActiveSupport::Notifications::Fanout::Handle)
26
+ require_relative "./active_support_notifications/start_finish_shared_examples"
27
+
28
+ it_behaves_like "activesupport start finish override"
29
+ end
30
+
25
31
  if ::ActiveSupport::Notifications::Instrumenter.method_defined?(:start)
26
32
  require_relative "./active_support_notifications/start_finish_shared_examples"
27
33
 
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ if DependencyHelper.dry_monitor_present?
4
+ require "dry-monitor"
5
+
6
+ describe Appsignal::Hooks::DryMonitorHook do
7
+ describe "#dependencies_present?" do
8
+ subject { described_class.new.dependencies_present? }
9
+
10
+ context "when Dry::Monitor::Notifications constant is found" do
11
+ before { stub_const "Dry::Monitor::Notifications", Class.new }
12
+
13
+ it { is_expected.to be_truthy }
14
+ end
15
+
16
+ context "when Dry::Monitor::Notifications constant is not found" do
17
+ before { hide_const "Dry::Monitor::Notifications" }
18
+
19
+ it { is_expected.to be_falsy }
20
+ end
21
+ end
22
+ end
23
+
24
+ describe "#install" do
25
+ it "installs the dry-monitor hook" do
26
+ start_agent
27
+
28
+ expect(Dry::Monitor::Notifications.included_modules).to include(
29
+ Appsignal::Integrations::DryMonitorIntegration
30
+ )
31
+ end
32
+ end
33
+
34
+ describe "Dry Monitor Integration" do
35
+ before :context do
36
+ start_agent
37
+ end
38
+
39
+ let!(:transaction) do
40
+ Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
41
+ end
42
+
43
+ let(:notifications) { Dry::Monitor::Notifications.new(:test) }
44
+
45
+ context "when is a dry-sql event" do
46
+ let(:event_id) { :sql }
47
+ let(:payload) do
48
+ {
49
+ :name => "postgres",
50
+ :query => "SELECT * FROM users"
51
+ }
52
+ end
53
+
54
+ it "creates an sql event" do
55
+ notifications.instrument(event_id, payload)
56
+ expect(transaction.to_h["events"]).to match([
57
+ {
58
+ "allocation_count" => kind_of(Integer),
59
+ "body" => "SELECT * FROM users",
60
+ "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
61
+ "child_allocation_count" => kind_of(Integer),
62
+ "child_duration" => kind_of(Float),
63
+ "child_gc_duration" => kind_of(Float),
64
+ "count" => 1,
65
+ "duration" => kind_of(Float),
66
+ "gc_duration" => kind_of(Float),
67
+ "name" => "query.postgres",
68
+ "start" => kind_of(Float),
69
+ "title" => "query.postgres"
70
+ }
71
+ ])
72
+ end
73
+ end
74
+
75
+ context "when is an unregistered formatter event" do
76
+ let(:event_id) { :foo }
77
+ let(:payload) do
78
+ {
79
+ :name => "foo"
80
+ }
81
+ end
82
+
83
+ it "creates a generic event" do
84
+ notifications.instrument(event_id, payload)
85
+ expect(transaction.to_h["events"]).to match([
86
+ {
87
+ "allocation_count" => kind_of(Integer),
88
+ "body" => "",
89
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
90
+ "child_allocation_count" => kind_of(Integer),
91
+ "child_duration" => kind_of(Float),
92
+ "child_gc_duration" => kind_of(Float),
93
+ "count" => 1,
94
+ "duration" => kind_of(Float),
95
+ "gc_duration" => kind_of(Float),
96
+ "name" => "foo",
97
+ "start" => kind_of(Float),
98
+ "title" => ""
99
+ }
100
+ ])
101
+ end
102
+ end
103
+ end
104
+ end
@@ -2,6 +2,10 @@ if DependencyHelper.rails_present?
2
2
  require "action_mailer"
3
3
 
4
4
  describe Appsignal::Integrations::Railtie do
5
+ include RailsHelper
6
+
7
+ after { clear_rails_error_reporter! }
8
+
5
9
  context "after initializing the app" do
6
10
  it "should call initialize_appsignal" do
7
11
  expect(Appsignal::Integrations::Railtie).to receive(:initialize_appsignal)
@@ -125,17 +129,16 @@ if DependencyHelper.rails_present?
125
129
 
126
130
  if Rails.respond_to?(:error)
127
131
  describe "Rails error reporter" do
128
- before do
129
- Appsignal::Integrations::Railtie.initialize_appsignal(app)
130
- start_agent
131
- end
132
+ before { start_agent }
132
133
  around { |example| keep_transactions { example.run } }
133
134
 
134
135
  context "when error is not handled (reraises the error)" do
135
136
  it "does nothing" do
136
- expect do
137
- Rails.error.record { raise ExampleStandardError }
138
- end.to raise_error(ExampleStandardError)
137
+ with_rails_error_reporter do
138
+ expect do
139
+ Rails.error.record { raise ExampleStandardError }
140
+ end.to raise_error(ExampleStandardError)
141
+ end
139
142
 
140
143
  expect(created_transactions).to be_empty
141
144
  end
@@ -151,26 +154,28 @@ if DependencyHelper.rails_present?
151
154
  :duplicated_tag => "duplicated value"
152
155
  )
153
156
 
154
- with_current_transaction current_transaction do
155
- Rails.error.handle { raise ExampleStandardError }
156
-
157
- transaction = last_transaction
158
- transaction_hash = transaction.to_h
159
- expect(transaction_hash).to include(
160
- "action" => "CustomAction",
161
- "namespace" => "custom",
162
- "error" => {
163
- "name" => "ExampleStandardError",
164
- "message" => "ExampleStandardError",
165
- "backtrace" => kind_of(String)
166
- },
167
- "sample_data" => hash_including(
168
- "tags" => {
169
- "duplicated_tag" => "duplicated value",
170
- "severity" => "warning"
171
- }
157
+ with_rails_error_reporter do
158
+ with_current_transaction current_transaction do
159
+ Rails.error.handle { raise ExampleStandardError }
160
+
161
+ transaction = last_transaction
162
+ transaction_hash = transaction.to_h
163
+ expect(transaction_hash).to include(
164
+ "action" => "CustomAction",
165
+ "namespace" => "custom",
166
+ "error" => {
167
+ "name" => "ExampleStandardError",
168
+ "message" => "ExampleStandardError",
169
+ "backtrace" => kind_of(String)
170
+ },
171
+ "sample_data" => hash_including(
172
+ "tags" => hash_including(
173
+ "duplicated_tag" => "duplicated value",
174
+ "severity" => "warning"
175
+ )
176
+ )
172
177
  )
173
- )
178
+ end
174
179
  end
175
180
  end
176
181
 
@@ -178,48 +183,52 @@ if DependencyHelper.rails_present?
178
183
  current_transaction = http_request_transaction
179
184
  current_transaction.set_tags(:tag1 => "duplicated value")
180
185
 
181
- with_current_transaction current_transaction do
182
- given_context = { :tag1 => "value1", :tag2 => "value2" }
183
- Rails.error.handle(:context => given_context) { raise ExampleStandardError }
184
-
185
- transaction = last_transaction
186
- transaction_hash = transaction.to_h
187
- expect(transaction_hash).to include(
188
- "sample_data" => hash_including(
189
- "tags" => {
190
- "tag1" => "value1",
191
- "tag2" => "value2",
192
- "severity" => "warning"
193
- }
186
+ with_rails_error_reporter do
187
+ with_current_transaction current_transaction do
188
+ given_context = { :tag1 => "value1", :tag2 => "value2" }
189
+ Rails.error.handle(:context => given_context) { raise ExampleStandardError }
190
+
191
+ transaction = last_transaction
192
+ transaction_hash = transaction.to_h
193
+ expect(transaction_hash).to include(
194
+ "sample_data" => hash_including(
195
+ "tags" => hash_including(
196
+ "tag1" => "value1",
197
+ "tag2" => "value2",
198
+ "severity" => "warning"
199
+ )
200
+ )
194
201
  )
195
- )
202
+ end
196
203
  end
197
204
  end
198
205
 
199
206
  it "sends tags stored in :appsignal -> :custom_data as custom data" do
200
207
  current_transaction = http_request_transaction
201
208
 
202
- with_current_transaction current_transaction do
203
- given_context = {
204
- :appsignal => {
205
- :custom_data => {
206
- :array => [1, 2],
207
- :hash => { :one => 1, :two => 2 }
209
+ with_rails_error_reporter do
210
+ with_current_transaction current_transaction do
211
+ given_context = {
212
+ :appsignal => {
213
+ :custom_data => {
214
+ :array => [1, 2],
215
+ :hash => { :one => 1, :two => 2 }
216
+ }
208
217
  }
209
218
  }
210
- }
211
- Rails.error.handle(:context => given_context) { raise ExampleStandardError }
212
-
213
- transaction = last_transaction
214
- transaction_hash = transaction.to_h
215
- expect(transaction_hash).to include(
216
- "sample_data" => hash_including(
217
- "custom_data" => {
218
- "array" => [1, 2],
219
- "hash" => { "one" => 1, "two" => 2 }
220
- }
219
+ Rails.error.handle(:context => given_context) { raise ExampleStandardError }
220
+
221
+ transaction = last_transaction
222
+ transaction_hash = transaction.to_h
223
+ expect(transaction_hash).to include(
224
+ "sample_data" => hash_including(
225
+ "custom_data" => {
226
+ "array" => [1, 2],
227
+ "hash" => { "one" => 1, "two" => 2 }
228
+ }
229
+ )
221
230
  )
222
- )
231
+ end
223
232
  end
224
233
  end
225
234
 
@@ -228,18 +237,20 @@ if DependencyHelper.rails_present?
228
237
  current_transaction.set_namespace "custom"
229
238
  current_transaction.set_action "CustomAction"
230
239
 
231
- with_current_transaction current_transaction do
232
- given_context = {
233
- :appsignal => { :namespace => "context", :action => "ContextAction" }
234
- }
235
- Rails.error.handle(:context => given_context) { raise ExampleStandardError }
240
+ with_rails_error_reporter do
241
+ with_current_transaction current_transaction do
242
+ given_context = {
243
+ :appsignal => { :namespace => "context", :action => "ContextAction" }
244
+ }
245
+ Rails.error.handle(:context => given_context) { raise ExampleStandardError }
236
246
 
237
- transaction = last_transaction
238
- transaction_hash = transaction.to_h
239
- expect(transaction_hash).to include(
240
- "namespace" => "context",
241
- "action" => "ContextAction"
242
- )
247
+ transaction = last_transaction
248
+ transaction_hash = transaction.to_h
249
+ expect(transaction_hash).to include(
250
+ "namespace" => "context",
251
+ "action" => "ContextAction"
252
+ )
253
+ end
243
254
  end
244
255
  end
245
256
  end
@@ -267,7 +278,9 @@ if DependencyHelper.rails_present?
267
278
  it "fetches the action from the controller in the context" do
268
279
  # The controller key is set by Rails when raised in a controller
269
280
  given_context = { :controller => ExampleRailsControllerMock.new }
270
- Rails.error.handle(:context => given_context) { raise ExampleStandardError }
281
+ with_rails_error_reporter do
282
+ Rails.error.handle(:context => given_context) { raise ExampleStandardError }
283
+ end
271
284
 
272
285
  transaction = last_transaction
273
286
  transaction_hash = transaction.to_h
@@ -278,7 +291,9 @@ if DependencyHelper.rails_present?
278
291
 
279
292
  it "sets no action if no execution context is present" do
280
293
  # The controller key is set by Rails when raised in a controller
281
- Rails.error.handle { raise ExampleStandardError }
294
+ with_rails_error_reporter do
295
+ Rails.error.handle { raise ExampleStandardError }
296
+ end
282
297
 
283
298
  transaction = last_transaction
284
299
  transaction_hash = transaction.to_h
@@ -296,17 +311,19 @@ if DependencyHelper.rails_present?
296
311
  :tag1 => "value1",
297
312
  :tag2 => "value2"
298
313
  }
299
- Rails.error.handle(:context => given_context) { raise ExampleStandardError }
314
+ with_rails_error_reporter do
315
+ Rails.error.handle(:context => given_context) { raise ExampleStandardError }
316
+ end
300
317
 
301
318
  transaction = last_transaction
302
319
  transaction_hash = transaction.to_h
303
320
  expect(transaction_hash).to include(
304
321
  "sample_data" => hash_including(
305
- "tags" => {
322
+ "tags" => hash_including(
306
323
  "tag1" => "value1",
307
324
  "tag2" => "value2",
308
325
  "severity" => "warning"
309
- }
326
+ )
310
327
  )
311
328
  )
312
329
  end
@@ -266,13 +266,16 @@ describe Appsignal::Integrations::SidekiqMiddleware, :with_yaml_parse_error => f
266
266
 
267
267
  if DependencyHelper.rails7_present?
268
268
  context "with Rails error reporter" do
269
+ include RailsHelper
270
+
269
271
  it "reports the worker name as the action, copies the namespace and tags" do
270
- Appsignal::Integrations::Railtie.initialize_appsignal(MyApp::Application.new)
271
272
  Appsignal.config = project_fixture_config("production")
272
- perform_job do
273
- Appsignal.tag_job("test_tag" => "value")
274
- Rails.error.handle do
275
- raise error, "uh oh"
273
+ with_rails_error_reporter do
274
+ perform_job do
275
+ Appsignal.tag_job("test_tag" => "value")
276
+ Rails.error.handle do
277
+ raise ExampleStandardError, "uh oh"
278
+ end
276
279
  end
277
280
  end
278
281
 
@@ -363,7 +366,9 @@ if DependencyHelper.active_job_present?
363
366
  require "sidekiq/testing"
364
367
 
365
368
  describe "Sidekiq ActiveJob integration" do
369
+ include RailsHelper
366
370
  include ActiveJobHelpers
371
+
367
372
  let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
368
373
  let(:time) { Time.parse("2001-01-01 10:00:00UTC") }
369
374
  let(:log) { StringIO.new }
@@ -416,7 +421,7 @@ if DependencyHelper.active_job_present?
416
421
  ["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"]
417
422
  end
418
423
  end
419
- before do
424
+ around do |example|
420
425
  start_agent
421
426
  Appsignal.logger = test_logger(log)
422
427
  ActiveJob::Base.queue_adapter = :sidekiq
@@ -441,11 +446,11 @@ if DependencyHelper.active_job_present?
441
446
  Sidekiq::Testing.server_middleware do |chain|
442
447
  chain.add Appsignal::Integrations::SidekiqMiddleware
443
448
  end
444
- end
445
- around do |example|
446
- keep_transactions do
447
- Sidekiq::Testing.fake! do
448
- example.run
449
+ with_rails_error_reporter do
450
+ keep_transactions do
451
+ Sidekiq::Testing.fake! do
452
+ example.run
453
+ end
449
454
  end
450
455
  end
451
456
  end
@@ -73,6 +73,20 @@ describe Appsignal::Probes::SidekiqProbe do
73
73
  module Sidekiq7Mock
74
74
  VERSION = "7.0.0".freeze
75
75
 
76
+ def self.redis_info_data=(info)
77
+ @redis_info_data = info
78
+ end
79
+
80
+ def self.redis_info_data
81
+ return @redis_info_data if defined?(@redis_info_data)
82
+
83
+ {
84
+ "connected_clients" => 2,
85
+ "used_memory" => 1024,
86
+ "used_memory_rss" => 512
87
+ }
88
+ end
89
+
76
90
  def self.redis
77
91
  yield Client.new
78
92
  end
@@ -83,11 +97,7 @@ describe Appsignal::Probes::SidekiqProbe do
83
97
  end
84
98
 
85
99
  def info
86
- {
87
- "connected_clients" => 2,
88
- "used_memory" => 1024,
89
- "used_memory_rss" => 512
90
- }
100
+ Sidekiq7Mock.redis_info_data
91
101
  end
92
102
  end
93
103
 
@@ -227,6 +237,19 @@ describe Appsignal::Probes::SidekiqProbe do
227
237
  probe.call
228
238
  probe.call
229
239
  end
240
+
241
+ context "when redis info doesn't contain requested keys" do
242
+ before { Sidekiq7Mock.redis_info_data = {} }
243
+
244
+ it "doesn't create metrics for nil values" do
245
+ expect_gauge("connection_count").never
246
+ expect_gauge("memory_usage").never
247
+ expect_gauge("memory_usage_rss").never
248
+ # Call probe twice so we can calculate the delta for some gauge values
249
+ probe.call
250
+ probe.call
251
+ end
252
+ end
230
253
  end
231
254
 
232
255
  context "with Sidekiq 6" do
@@ -301,7 +324,7 @@ describe Appsignal::Probes::SidekiqProbe do
301
324
  end
302
325
  end
303
326
 
304
- def expect_gauge(key, value, tags = {})
327
+ def expect_gauge(key, value = anything, tags = {})
305
328
  expect(Appsignal).to receive(:set_gauge)
306
329
  .with("sidekiq_#{key}", value, expected_default_tags.merge(tags))
307
330
  .and_call_original
@@ -115,6 +115,10 @@ module DependencyHelper
115
115
  dependency_present? "hanami"
116
116
  end
117
117
 
118
+ def dry_monitor_present?
119
+ dependency_present? "dry-monitor"
120
+ end
121
+
118
122
  def hanami2_present?
119
123
  hanami_present? && Gem.loaded_specs["hanami"].version >= Gem::Version.new("2.0")
120
124
  end