appsignal 2.10.6 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +138 -62
  4. data/CHANGELOG.md +56 -0
  5. data/README.md +4 -4
  6. data/Rakefile +16 -4
  7. data/appsignal.gemspec +1 -1
  8. data/build_matrix.yml +20 -9
  9. data/ext/Rakefile +2 -0
  10. data/ext/agent.yml +19 -19
  11. data/ext/appsignal_extension.c +10 -1
  12. data/ext/base.rb +22 -4
  13. data/ext/extconf.rb +2 -0
  14. data/gemfiles/padrino.gemfile +2 -2
  15. data/gemfiles/rails-4.2.gemfile +9 -2
  16. data/gemfiles/rails-5.0.gemfile +1 -0
  17. data/gemfiles/rails-5.1.gemfile +1 -0
  18. data/gemfiles/rails-5.2.gemfile +1 -0
  19. data/gemfiles/rails-6.0.gemfile +1 -0
  20. data/gemfiles/resque-1.gemfile +7 -0
  21. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  22. data/lib/appsignal.rb +22 -1
  23. data/lib/appsignal/auth_check.rb +4 -2
  24. data/lib/appsignal/capistrano.rb +2 -0
  25. data/lib/appsignal/cli/diagnose.rb +1 -1
  26. data/lib/appsignal/config.rb +85 -16
  27. data/lib/appsignal/environment.rb +126 -0
  28. data/lib/appsignal/extension.rb +6 -5
  29. data/lib/appsignal/extension/jruby.rb +16 -5
  30. data/lib/appsignal/hooks.rb +25 -0
  31. data/lib/appsignal/hooks/active_job.rb +137 -0
  32. data/lib/appsignal/hooks/net_http.rb +10 -13
  33. data/lib/appsignal/hooks/puma.rb +1 -58
  34. data/lib/appsignal/hooks/redis.rb +2 -0
  35. data/lib/appsignal/hooks/resque.rb +60 -0
  36. data/lib/appsignal/hooks/sequel.rb +2 -0
  37. data/lib/appsignal/hooks/sidekiq.rb +18 -192
  38. data/lib/appsignal/integrations/delayed_job_plugin.rb +16 -3
  39. data/lib/appsignal/integrations/object.rb +4 -0
  40. data/lib/appsignal/integrations/que.rb +1 -1
  41. data/lib/appsignal/integrations/resque.rb +9 -12
  42. data/lib/appsignal/integrations/resque_active_job.rb +9 -24
  43. data/lib/appsignal/probes.rb +7 -0
  44. data/lib/appsignal/probes/puma.rb +61 -0
  45. data/lib/appsignal/probes/sidekiq.rb +104 -0
  46. data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
  47. data/lib/appsignal/system.rb +0 -6
  48. data/lib/appsignal/transaction.rb +32 -7
  49. data/lib/appsignal/utils/deprecation_message.rb +6 -2
  50. data/lib/appsignal/version.rb +1 -1
  51. data/lib/puma/plugin/appsignal.rb +2 -1
  52. data/spec/lib/appsignal/auth_check_spec.rb +23 -0
  53. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  54. data/spec/lib/appsignal/capistrano3_spec.rb +1 -1
  55. data/spec/lib/appsignal/cli/diagnose_spec.rb +44 -1
  56. data/spec/lib/appsignal/config_spec.rb +44 -1
  57. data/spec/lib/appsignal/environment_spec.rb +167 -0
  58. data/spec/lib/appsignal/extension/jruby_spec.rb +31 -28
  59. data/spec/lib/appsignal/extension_install_failure_spec.rb +23 -0
  60. data/spec/lib/appsignal/hooks/activejob_spec.rb +591 -0
  61. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +187 -166
  62. data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
  63. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  64. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +297 -549
  65. data/spec/lib/appsignal/hooks_spec.rb +57 -0
  66. data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
  67. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  68. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -137
  69. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  70. data/spec/lib/appsignal/marker_spec.rb +1 -1
  71. data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
  72. data/spec/lib/appsignal/probes/sidekiq_spec.rb +204 -0
  73. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
  74. data/spec/lib/appsignal/system_spec.rb +0 -36
  75. data/spec/lib/appsignal/transaction_spec.rb +35 -20
  76. data/spec/lib/appsignal_spec.rb +22 -0
  77. data/spec/lib/puma/appsignal_spec.rb +1 -1
  78. data/spec/spec_helper.rb +5 -0
  79. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  80. data/spec/support/helpers/config_helpers.rb +3 -2
  81. data/spec/support/helpers/dependency_helper.rb +12 -0
  82. data/spec/support/helpers/env_helpers.rb +1 -1
  83. data/spec/support/helpers/environment_metdata_helper.rb +16 -0
  84. data/spec/support/helpers/transaction_helpers.rb +6 -0
  85. data/spec/support/stubs/sidekiq/api.rb +2 -2
  86. data/spec/support/testing.rb +19 -19
  87. metadata +31 -9
  88. data/lib/appsignal/integrations/net_http.rb +0 -16
@@ -78,6 +78,63 @@ describe Appsignal::Hooks do
78
78
  expect(Appsignal::Hooks.hooks[:mock_error_hook].installed?).to be_falsy
79
79
  Appsignal::Hooks.hooks.delete(:mock_error_hook)
80
80
  end
81
+
82
+ describe "missing constants" do
83
+ let(:err_stream) { std_stream }
84
+ let(:stderr) { err_stream.read }
85
+ let(:log_stream) { std_stream }
86
+ let(:log) { log_contents(log_stream) }
87
+ before do
88
+ Appsignal.logger = test_logger(log_stream)
89
+ end
90
+
91
+ def call_constant(&block)
92
+ capture_std_streams(std_stream, err_stream, &block)
93
+ end
94
+
95
+ describe "SidekiqProbe" do
96
+ it "logs a deprecation message and returns the new constant" do
97
+ constant = call_constant { Appsignal::Hooks::SidekiqProbe }
98
+
99
+ expect(constant).to eql(Appsignal::Probes::SidekiqProbe)
100
+ expect(constant.name).to eql("Appsignal::Probes::SidekiqProbe")
101
+
102
+ deprecation_message =
103
+ "The constant Appsignal::Hooks::SidekiqProbe has been deprecated. " \
104
+ "Please update the constant name to Appsignal::Probes::SidekiqProbe " \
105
+ "in the following file to remove this message.\n#{__FILE__}:"
106
+ expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
107
+ expect(log).to contains_log :warn, deprecation_message
108
+ end
109
+ end
110
+
111
+ describe "PumaProbe" do
112
+ it "logs a deprecation message and returns the new constant" do
113
+ constant = call_constant { Appsignal::Hooks::PumaProbe }
114
+
115
+ expect(constant).to eql(Appsignal::Probes::PumaProbe)
116
+ expect(constant.name).to eql("Appsignal::Probes::PumaProbe")
117
+
118
+ deprecation_message =
119
+ "The constant Appsignal::Hooks::PumaProbe has been deprecated. " \
120
+ "Please update the constant name to Appsignal::Probes::PumaProbe " \
121
+ "in the following file to remove this message.\n#{__FILE__}:"
122
+ expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
123
+ expect(log).to contains_log :warn, deprecation_message
124
+ end
125
+ end
126
+
127
+ describe "other constant" do
128
+ it "raises a NameError like Ruby normally does" do
129
+ expect do
130
+ call_constant { Appsignal::Hooks::Unknown }
131
+ end.to raise_error(NameError)
132
+
133
+ expect(stderr).to be_empty
134
+ expect(log).to be_empty
135
+ end
136
+ end
137
+ end
81
138
  end
82
139
 
83
140
  describe Appsignal::Hooks::Helpers do
@@ -142,7 +142,7 @@ if DependencyHelper.padrino_present?
142
142
  expect_a_transaction_to_be_created
143
143
  # Uses path for action name
144
144
  expect(transaction).to receive(:set_action_if_nil).with("PadrinoTestApp#unknown")
145
- expect(response).to match_response(404, "<h1>Not Found</h1>")
145
+ expect(response).to match_response(404, "GET /404")
146
146
  end
147
147
  end
148
148
 
@@ -14,7 +14,6 @@ if DependencyHelper.que_present?
14
14
  :error_count => 0
15
15
  }
16
16
  end
17
-
18
17
  let(:env) do
19
18
  {
20
19
  :class => "MyQueJob",
@@ -29,7 +28,6 @@ if DependencyHelper.que_present?
29
28
  :params => %w[1 birds]
30
29
  }
31
30
  end
32
-
33
31
  let(:job) do
34
32
  Class.new(::Que::Job) do
35
33
  def run(*args)
@@ -37,7 +35,6 @@ if DependencyHelper.que_present?
37
35
  end
38
36
  end
39
37
  let(:instance) { job.new(job_attrs) }
40
-
41
38
  before do
42
39
  allow(Que).to receive(:execute)
43
40
 
@@ -46,10 +43,14 @@ if DependencyHelper.que_present?
46
43
  end
47
44
  around { |example| keep_transactions { example.run } }
48
45
 
46
+ def perform_job(job)
47
+ job._run
48
+ end
49
+
49
50
  context "success" do
50
51
  it "creates a transaction for a job" do
51
52
  expect do
52
- instance._run
53
+ perform_job(instance)
53
54
  end.to change { created_transactions.length }.by(1)
54
55
 
55
56
  expect(last_transaction).to be_completed
@@ -95,7 +96,7 @@ if DependencyHelper.que_present?
95
96
 
96
97
  expect do
97
98
  expect do
98
- instance._run
99
+ perform_job(instance)
99
100
  end.to raise_error(ExampleException)
100
101
  end.to change { created_transactions.length }.by(1)
101
102
 
@@ -130,7 +131,7 @@ if DependencyHelper.que_present?
130
131
  it "reports errors and not re-raise them" do
131
132
  allow(instance).to receive(:run).and_raise(error)
132
133
 
133
- expect { instance._run }.to change { created_transactions.length }.by(1)
134
+ expect { perform_job(instance) }.to change { created_transactions.length }.by(1)
134
135
 
135
136
  expect(last_transaction).to be_completed
136
137
  transaction_hash = last_transaction.to_h
@@ -156,6 +157,24 @@ if DependencyHelper.que_present?
156
157
  )
157
158
  end
158
159
  end
160
+
161
+ context "when action set in job" do
162
+ let(:job) do
163
+ Class.new(::Que::Job) do
164
+ def run(*_args)
165
+ Appsignal.set_action("MyCustomJob#perform")
166
+ end
167
+ end
168
+ end
169
+
170
+ it "uses the custom action" do
171
+ perform_job(instance)
172
+
173
+ expect(last_transaction).to be_completed
174
+ transaction_hash = last_transaction.to_h
175
+ expect(transaction_hash).to include("action" => "MyCustomJob#perform")
176
+ end
177
+ end
159
178
  end
160
179
  end
161
180
  end
@@ -1,145 +1,28 @@
1
- if DependencyHelper.resque_present? && DependencyHelper.active_job_present?
2
- require "active_job"
1
+ require "appsignal/integrations/resque_active_job"
3
2
 
4
- describe Appsignal::Integrations::ResqueActiveJobPlugin do
5
- let(:file) { File.expand_path("lib/appsignal/integrations/resque_active_job.rb") }
6
- let(:args) { "argument" }
7
- let(:job) { TestActiveJob.new(args) }
8
- before do
9
- load file
10
- start_agent
3
+ describe "Legacy Resque ActiveJob integration" do
4
+ let(:err_stream) { std_stream }
5
+ let(:stderr) { err_stream.read }
6
+ let(:log_stream) { std_stream }
7
+ let(:log) { log_contents(log_stream) }
11
8
 
12
- class TestActiveJob < ActiveJob::Base
13
- include Appsignal::Integrations::ResqueActiveJobPlugin
14
-
15
- def perform(_)
16
- end
17
- end
18
- end
19
-
20
- def perform
21
- keep_transactions do
22
- job.perform_now
23
- end
24
- end
25
-
26
- context "without error" do
27
- it "creates a new transaction" do
28
- expect { perform }.to change { created_transactions.length }.by(1)
29
-
30
- expect(last_transaction.to_h).to include(
31
- "namespace" => Appsignal::Transaction::BACKGROUND_JOB,
32
- "action" => "TestActiveJob#perform",
33
- "error" => nil,
34
- "events" => [
35
- hash_including(
36
- "name" => "perform_job.resque",
37
- "title" => "",
38
- "body" => "",
39
- "body_format" => Appsignal::EventFormatter::DEFAULT,
40
- "count" => 1,
41
- "duration" => kind_of(Float)
42
- )
43
- ],
44
- "sample_data" => hash_including(
45
- "params" => ["argument"],
46
- "metadata" => {
47
- "id" => kind_of(String),
48
- "queue" => "default"
49
- }
50
- )
51
- )
52
- end
53
- end
54
-
55
- context "with error" do
56
- let(:job) do
57
- class BrokenTestActiveJob < ActiveJob::Base
58
- include Appsignal::Integrations::ResqueActiveJobPlugin
9
+ it "logs and prints a deprecation message on extend" do
10
+ Appsignal.logger = test_logger(log_stream)
59
11
 
60
- def perform(_)
61
- raise ExampleException, "my error message"
62
- end
63
- end
64
-
65
- BrokenTestActiveJob.new(args)
66
- end
67
-
68
- it "creates a new transaction with an error" do
69
- expect do
70
- expect { perform }.to raise_error(ExampleException, "my error message")
71
- end.to change { created_transactions.length }.by(1)
72
-
73
- expect(last_transaction.to_h).to include(
74
- "namespace" => Appsignal::Transaction::BACKGROUND_JOB,
75
- "action" => "BrokenTestActiveJob#perform",
76
- "error" => {
77
- "name" => "ExampleException",
78
- "message" => "my error message",
79
- "backtrace" => kind_of(String)
80
- },
81
- "sample_data" => hash_including(
82
- "params" => ["argument"],
83
- "metadata" => {
84
- "id" => kind_of(String),
85
- "queue" => "default"
86
- }
87
- )
88
- )
12
+ capture_std_streams(std_stream, err_stream) do
13
+ Class.new do
14
+ include Appsignal::Integrations::ResqueActiveJobPlugin
89
15
  end
90
16
  end
91
17
 
92
- context "with complex arguments" do
93
- context "with too long values" do
94
- let(:args) do
95
- {
96
- :foo => "Foo",
97
- :bar => "a" * 2001
98
- }
99
- end
100
-
101
- it "truncates large argument values" do
102
- perform
103
- expect(last_transaction.to_h).to include(
104
- "namespace" => Appsignal::Transaction::BACKGROUND_JOB,
105
- "action" => "TestActiveJob#perform",
106
- "error" => nil,
107
- "sample_data" => hash_including(
108
- "params" => ["foo" => "Foo", "bar" => "#{"a" * 2000}..."],
109
- "metadata" => {
110
- "id" => kind_of(String),
111
- "queue" => "default"
112
- }
113
- )
114
- )
115
- end
116
- end
117
-
118
- context "with parameter filtering" do
119
- let(:args) do
120
- {
121
- :foo => "Foo",
122
- :bar => "Bar"
123
- }
124
- end
125
- before { Appsignal.config[:filter_parameters] = ["foo"] }
126
-
127
- it "filters selected arguments" do
128
- perform
129
- expect(last_transaction.to_h).to include(
130
- "namespace" => Appsignal::Transaction::BACKGROUND_JOB,
131
- "action" => "TestActiveJob#perform",
132
- "error" => nil,
133
- "sample_data" => hash_including(
134
- "params" => ["foo" => "[FILTERED]", "bar" => "Bar"],
135
- "metadata" => {
136
- "id" => kind_of(String),
137
- "queue" => "default"
138
- }
139
- )
140
- )
141
- end
142
- end
143
- end
18
+ deprecation_message =
19
+ "The AppSignal ResqueActiveJobPlugin is deprecated and does " \
20
+ "nothing on extend. In this version of the AppSignal Ruby gem " \
21
+ "the integration with Resque is automatic on all Resque workers. " \
22
+ "Please remove the following line from this file to remove this " \
23
+ "message: include Appsignal::Integrations::ResqueActiveJobPlugin\n" \
24
+ "#{__FILE__}:"
25
+ expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
26
+ expect(log).to contains_log :warn, deprecation_message
144
27
  end
145
28
  end
@@ -1,93 +1,28 @@
1
- if DependencyHelper.resque_present?
2
- describe "Resque integration" do
3
- let(:file) { File.expand_path("lib/appsignal/integrations/resque.rb") }
1
+ require "appsignal/integrations/resque"
4
2
 
5
- context "with resque" do
6
- before do
7
- load file
8
- start_agent
3
+ describe "Legacy Resque integration" do
4
+ let(:err_stream) { std_stream }
5
+ let(:stderr) { err_stream.read }
6
+ let(:log_stream) { std_stream }
7
+ let(:log) { log_contents(log_stream) }
9
8
 
10
- class TestJob
11
- extend Appsignal::Integrations::ResquePlugin
9
+ it "logs and prints a deprecation message on extend" do
10
+ Appsignal.logger = test_logger(log_stream)
12
11
 
13
- def self.perform
14
- end
15
- end
16
-
17
- class BrokenTestJob
18
- extend Appsignal::Integrations::ResquePlugin
19
-
20
- def self.perform
21
- raise ExampleException, "my error message"
22
- end
23
- end
24
- end
25
-
26
- describe :around_perform_resque_plugin do
27
- let(:job) { ::Resque::Job.new("default", "class" => "TestJob") }
28
- before { expect(Appsignal).to receive(:stop) }
29
-
30
- context "without exception" do
31
- it "creates a new transaction" do
32
- expect do
33
- keep_transactions { job.perform }
34
- end.to change { created_transactions.length }.by(1)
35
-
36
- expect(last_transaction).to be_completed
37
- expect(last_transaction.to_h).to include(
38
- "namespace" => Appsignal::Transaction::BACKGROUND_JOB,
39
- "action" => "TestJob#perform",
40
- "error" => nil,
41
- "events" => [
42
- hash_including(
43
- "name" => "perform_job.resque",
44
- "title" => "",
45
- "body" => "",
46
- "body_format" => Appsignal::EventFormatter::DEFAULT,
47
- "count" => 1,
48
- "duration" => kind_of(Float)
49
- )
50
- ]
51
- )
52
- end
53
- end
54
-
55
- context "with exception" do
56
- let(:job) { ::Resque::Job.new("default", "class" => "BrokenTestJob") }
57
-
58
- def perform
59
- keep_transactions do
60
- expect do
61
- job.perform
62
- end.to raise_error(ExampleException, "my error message")
63
- end
64
- end
65
-
66
- it "sets the exception on the transaction" do
67
- expect do
68
- perform
69
- end.to change { created_transactions.length }.by(1)
70
-
71
- expect(last_transaction).to be_completed
72
- expect(last_transaction.to_h).to include(
73
- "namespace" => Appsignal::Transaction::BACKGROUND_JOB,
74
- "action" => "BrokenTestJob#perform",
75
- "error" => {
76
- "name" => "ExampleException",
77
- "message" => "my error message",
78
- "backtrace" => kind_of(String)
79
- }
80
- )
81
- end
82
- end
12
+ capture_std_streams(std_stream, err_stream) do
13
+ Class.new do
14
+ extend Appsignal::Integrations::ResquePlugin
83
15
  end
84
16
  end
85
17
 
86
- context "without resque" do
87
- before(:context) { Object.send(:remove_const, :Resque) }
88
-
89
- it { expect { ::Resque }.to raise_error(NameError) }
90
- it { expect { load file }.to_not raise_error }
91
- end
18
+ deprecation_message =
19
+ "The AppSignal ResquePlugin is deprecated and does " \
20
+ "nothing on extend. In this version of the AppSignal Ruby gem " \
21
+ "the integration with Resque is automatic on all Resque workers. " \
22
+ "Please remove the following line from this file to remove this " \
23
+ "message: extend Appsignal::Integrations::ResquePlugin\n" \
24
+ "#{__FILE__}:"
25
+ expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
26
+ expect(log).to contains_log :warn, deprecation_message
92
27
  end
93
28
  end
@@ -4,7 +4,7 @@ describe Appsignal::Marker do
4
4
  described_class.new(
5
5
  {
6
6
  :revision => "503ce0923ed177a3ce000005",
7
- :repository => "master",
7
+ :repository => "main",
8
8
  :user => "batman",
9
9
  :rails_env => "production"
10
10
  },
@@ -0,0 +1,180 @@
1
+ require "appsignal/probes/puma"
2
+
3
+ describe Appsignal::Probes::PumaProbe do
4
+ before(:context) do
5
+ Appsignal.config = project_fixture_config
6
+ end
7
+ after(:context) do
8
+ Appsignal.config = nil
9
+ end
10
+
11
+ let(:probe) { described_class.new }
12
+
13
+ describe "hostname" do
14
+ it "returns the socket hostname" do
15
+ expect(probe.send(:hostname)).to eql(Socket.gethostname)
16
+ end
17
+
18
+ context "with overridden hostname" do
19
+ around do |sample|
20
+ Appsignal.config[:hostname] = "frontend1"
21
+ sample.run
22
+ Appsignal.config[:hostname] = nil
23
+ end
24
+ it "returns the configured host" do
25
+ expect(probe.send(:hostname)).to eql("frontend1")
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "#call" do
31
+ let(:expected_default_tags) { { :hostname => Socket.gethostname } }
32
+
33
+ context "with multiple worker stats" do
34
+ before(:context) do
35
+ class Puma
36
+ def self.stats
37
+ {
38
+ "workers" => 2,
39
+ "booted_workers" => 2,
40
+ "old_workers" => 0,
41
+ "worker_status" => [
42
+ {
43
+ "last_status" => {
44
+ "backlog" => 0,
45
+ "running" => 5,
46
+ "pool_capacity" => 5,
47
+ "max_threads" => 5
48
+ }
49
+ },
50
+ {
51
+ "last_status" => {
52
+ "backlog" => 0,
53
+ "running" => 5,
54
+ "pool_capacity" => 5,
55
+ "max_threads" => 5
56
+ }
57
+ }
58
+ ]
59
+ }.to_json
60
+ end
61
+ end
62
+ end
63
+ after(:context) { Object.send(:remove_const, :Puma) }
64
+
65
+ it "calls `puma_gauge` with the (summed) worker metrics" do
66
+ expect_gauge(:workers, 2, :type => :count)
67
+ expect_gauge(:workers, 2, :type => :booted)
68
+ expect_gauge(:workers, 0, :type => :old)
69
+
70
+ expect_gauge(:connection_backlog, 0)
71
+ expect_gauge(:pool_capacity, 10)
72
+ expect_gauge(:threads, 10, :type => :running)
73
+ expect_gauge(:threads, 10, :type => :max)
74
+
75
+ probe.call
76
+ end
77
+ end
78
+
79
+ context "with single worker stats" do
80
+ before(:context) do
81
+ class Puma
82
+ def self.stats
83
+ {
84
+ "backlog" => 0,
85
+ "running" => 5,
86
+ "pool_capacity" => 5,
87
+ "max_threads" => 5
88
+ }.to_json
89
+ end
90
+ end
91
+ end
92
+ after(:context) { Object.send(:remove_const, :Puma) }
93
+
94
+ it "calls `puma_gauge` with the (summed) worker metrics" do
95
+ expect_gauge(:connection_backlog, 0)
96
+ expect_gauge(:pool_capacity, 5)
97
+ expect_gauge(:threads, 5, :type => :running)
98
+ expect_gauge(:threads, 5, :type => :max)
99
+ probe.call
100
+ end
101
+ end
102
+
103
+ context "without stats" do
104
+ before(:context) do
105
+ class Puma
106
+ def self.stats
107
+ end
108
+ end
109
+ end
110
+ after(:context) { Object.send(:remove_const, :Puma) }
111
+
112
+ context "when it returns nil" do
113
+ it "does not track metrics" do
114
+ expect(probe).to_not receive(:puma_gauge)
115
+ probe.call
116
+ end
117
+ end
118
+
119
+ # Puma.stats raises a NoMethodError on a nil object on the first call.
120
+ context "when it returns a NoMethodError on the first call" do
121
+ let(:log) { StringIO.new }
122
+
123
+ it "ignores the first call and tracks the second call" do
124
+ use_logger_with log do
125
+ expect(Puma).to receive(:stats)
126
+ .and_raise(NoMethodError.new("undefined method `stats' for nil:NilClass"))
127
+ probe.call
128
+
129
+ expect(Puma).to receive(:stats).and_return({
130
+ "backlog" => 1,
131
+ "running" => 5,
132
+ "pool_capacity" => 4,
133
+ "max_threads" => 6
134
+ }.to_json)
135
+
136
+ expect_gauge(:connection_backlog, 1)
137
+ expect_gauge(:pool_capacity, 4)
138
+ expect_gauge(:threads, 5, :type => :running)
139
+ expect_gauge(:threads, 6, :type => :max)
140
+ probe.call
141
+ end
142
+
143
+ expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
144
+ end
145
+ end
146
+
147
+ context "when it does not have a complete stats payload" do
148
+ let(:log) { StringIO.new }
149
+
150
+ it "tracks whatever metrics we do have" do
151
+ use_logger_with log do
152
+ expect(Puma).to receive(:stats).and_return({
153
+ "backlog" => 1,
154
+ "running" => 5
155
+ }.to_json)
156
+
157
+ expect_gauge(:connection_backlog, 1)
158
+ expect_no_gauge(:pool_capacity)
159
+ expect_gauge(:threads, 5, :type => :running)
160
+ expect_no_gauge(:threads, :type => :max)
161
+ probe.call
162
+ end
163
+
164
+ expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
165
+ end
166
+ end
167
+ end
168
+
169
+ def expect_gauge(key, value, tags = {})
170
+ expect(Appsignal).to receive(:set_gauge)
171
+ .with("puma_#{key}", value, expected_default_tags.merge(tags))
172
+ .and_call_original
173
+ end
174
+
175
+ def expect_no_gauge(key, tags = {})
176
+ expect(Appsignal).to_not receive(:set_gauge)
177
+ .with("puma_#{key}", anything, tags)
178
+ end
179
+ end
180
+ end