appsignal 3.0.3-java → 3.0.7-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.semaphore/semaphore.yml +64 -199
  3. data/CHANGELOG.md +47 -19
  4. data/README.md +9 -3
  5. data/appsignal.gemspec +16 -3
  6. data/build_matrix.yml +16 -24
  7. data/ext/agent.yml +17 -17
  8. data/ext/base.rb +12 -1
  9. data/gemfiles/capistrano2.gemfile +0 -1
  10. data/gemfiles/capistrano3.gemfile +0 -1
  11. data/gemfiles/grape.gemfile +0 -1
  12. data/gemfiles/no_dependencies.gemfile +4 -1
  13. data/gemfiles/rails-3.2.gemfile +2 -0
  14. data/gemfiles/rails-4.2.gemfile +6 -0
  15. data/gemfiles/resque-2.gemfile +0 -4
  16. data/gemfiles/sequel-435.gemfile +0 -1
  17. data/gemfiles/sequel.gemfile +0 -1
  18. data/gemfiles/sinatra.gemfile +0 -1
  19. data/lib/appsignal/config.rb +1 -0
  20. data/lib/appsignal/hooks.rb +2 -1
  21. data/lib/appsignal/hooks/excon.rb +19 -0
  22. data/lib/appsignal/hooks/puma.rb +1 -16
  23. data/lib/appsignal/integrations/excon.rb +20 -0
  24. data/lib/appsignal/integrations/padrino.rb +1 -1
  25. data/lib/appsignal/integrations/railtie.rb +1 -1
  26. data/lib/appsignal/integrations/redis.rb +8 -5
  27. data/lib/appsignal/integrations/sinatra.rb +1 -1
  28. data/lib/appsignal/probes.rb +0 -1
  29. data/lib/appsignal/version.rb +1 -1
  30. data/lib/puma/plugin/appsignal.rb +146 -17
  31. data/mono.yml +16 -0
  32. data/spec/lib/appsignal/cli/diagnose_spec.rb +1 -0
  33. data/spec/lib/appsignal/hooks/excon_spec.rb +74 -0
  34. data/spec/lib/appsignal/hooks/puma_spec.rb +0 -46
  35. data/spec/lib/appsignal/hooks/redis_spec.rb +34 -10
  36. data/spec/lib/appsignal/hooks_spec.rb +4 -1
  37. data/spec/lib/puma/appsignal_spec.rb +244 -68
  38. data/support/install_deps +9 -8
  39. metadata +8 -6
  40. data/lib/appsignal/probes/puma.rb +0 -61
  41. data/spec/lib/appsignal/probes/puma_spec.rb +0 -180
@@ -35,29 +35,6 @@ describe Appsignal::Hooks::PumaHook do
35
35
  # Does not error on call
36
36
  Appsignal::Hooks::PumaHook.new.install
37
37
  end
38
-
39
- context "with APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
40
- before do
41
- # Set in lib/puma/appsignal.rb
42
- APPSIGNAL_PUMA_PLUGIN_LOADED = true
43
- end
44
- after { Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED }
45
-
46
- it "does not add the Puma minutely probe" do
47
- Appsignal::Hooks::PumaHook.new.install
48
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
49
- end
50
- end
51
-
52
- context "without APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
53
- it "adds the Puma minutely probe" do
54
- expect(defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)).to be_nil
55
-
56
- Appsignal::Hooks::PumaHook.new.install
57
- probe = Appsignal::Minutely.probes[:puma]
58
- expect(probe).to eql(Appsignal::Probes::PumaProbe)
59
- end
60
- end
61
38
  end
62
39
 
63
40
  context "when in clustered mode" do
@@ -81,29 +58,6 @@ describe Appsignal::Hooks::PumaHook do
81
58
  cluster.stop_workers
82
59
  expect(cluster.instance_variable_get(:@called)).to be(true)
83
60
  end
84
-
85
- context "with APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
86
- before do
87
- # Set in lib/puma/appsignal.rb
88
- APPSIGNAL_PUMA_PLUGIN_LOADED = true
89
- end
90
- after { Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED }
91
-
92
- it "does not add the Puma minutely probe" do
93
- Appsignal::Hooks::PumaHook.new.install
94
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
95
- end
96
- end
97
-
98
- context "without APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
99
- it "adds the Puma minutely probe" do
100
- expect(defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)).to be_nil
101
-
102
- Appsignal::Hooks::PumaHook.new.install
103
- probe = Appsignal::Minutely.probes[:puma]
104
- expect(probe).to eql(Appsignal::Probes::PumaProbe)
105
- end
106
- end
107
61
  end
108
62
  end
109
63
  end
@@ -33,16 +33,17 @@ describe Appsignal::Hooks::RedisHook do
33
33
 
34
34
  context "instrumentation" do
35
35
  before do
36
+ start_agent
36
37
  # Stub Redis::Client class so that it doesn't perform an actual
37
38
  # Redis query. This class will be included (prepended) with the
38
39
  # AppSignal Redis integration.
39
40
  stub_const("Redis::Client", Class.new do
40
41
  def id
41
- :stub_id
42
+ "stub_id"
42
43
  end
43
44
 
44
- def process(_commands)
45
- :stub_process
45
+ def write(_commands)
46
+ "stub_write"
46
47
  end
47
48
  end)
48
49
  # Load the integration again for the stubbed Redis::Client class.
@@ -50,17 +51,40 @@ describe Appsignal::Hooks::RedisHook do
50
51
  # track if it was installed already or not.
51
52
  Appsignal::Hooks::RedisHook.new.install
52
53
  end
54
+ let!(:transaction) do
55
+ Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
56
+ end
57
+ around { |example| keep_transactions { example.run } }
53
58
 
54
59
  it "instrument a redis call" do
55
- Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
56
- expect(Appsignal::Transaction.current).to receive(:start_event)
57
- .at_least(:once)
58
- expect(Appsignal::Transaction.current).to receive(:finish_event)
59
- .at_least(:once)
60
- .with("query.redis", :stub_id, "get ?", 0)
60
+ client = Redis::Client.new
61
+ expect(client.write([:get, "key"])).to eql("stub_write")
61
62
 
63
+ transaction_hash = transaction.to_h
64
+ expect(transaction_hash["events"]).to include(
65
+ hash_including(
66
+ "name" => "query.redis",
67
+ "body" => "get ?",
68
+ "title" => "stub_id"
69
+ )
70
+ )
71
+ end
72
+
73
+ it "instrument a redis script call" do
62
74
  client = Redis::Client.new
63
- expect(client.process([[:get, "key"]])).to eql(:stub_process)
75
+ script = "return redis.call('set',KEYS[1],ARGV[1])"
76
+ keys = ["foo"]
77
+ argv = ["bar"]
78
+ expect(client.write([:eval, script, keys.size, keys, argv])).to eql("stub_write")
79
+
80
+ transaction_hash = transaction.to_h
81
+ expect(transaction_hash["events"]).to include(
82
+ hash_including(
83
+ "name" => "query.redis",
84
+ "body" => "#{script} ? ?",
85
+ "title" => "stub_id"
86
+ )
87
+ )
64
88
  end
65
89
  end
66
90
  end
@@ -68,7 +68,10 @@ describe Appsignal::Hooks do
68
68
  expect(Appsignal::Hooks.hooks[:mock_error_hook].installed?).to be_falsy
69
69
 
70
70
  expect(Appsignal.logger).to receive(:error).with("Error while installing mock_error_hook hook: error").once
71
- expect(Appsignal.logger).to receive(:debug).once do |message|
71
+ expect(Appsignal.logger).to receive(:debug).ordered do |message|
72
+ expect(message).to eq("Installing mock_error_hook hook")
73
+ end
74
+ expect(Appsignal.logger).to receive(:debug).ordered do |message|
72
75
  # Start of the error backtrace as debug log
73
76
  expect(message).to start_with(File.expand_path("../../../../", __FILE__))
74
77
  end
@@ -1,6 +1,4 @@
1
1
  RSpec.describe "Puma plugin" do
2
- include WaitForHelper
3
-
4
2
  class MockPumaLauncher
5
3
  def events
6
4
  return @events if defined?(@events)
@@ -10,110 +8,288 @@ RSpec.describe "Puma plugin" do
10
8
  end
11
9
 
12
10
  class MockPumaEvents
13
- def on_booted(&block)
14
- @on_booted = block if block_given?
15
- @on_booted if defined?(@on_booted)
11
+ attr_reader :logs
12
+
13
+ def initialize
14
+ @logs = []
15
+ end
16
+
17
+ def log(message)
18
+ @logs << [:log, message]
19
+ end
20
+
21
+ def debug(message)
22
+ @logs << [:debug, message]
23
+ end
24
+
25
+ def error(message)
26
+ @logs << [:error, message]
27
+ end
28
+ end
29
+
30
+ # StatsD server used for these tests.
31
+ # Open a UDPSocket and listen for messages sent by the AppSignal Puma plugin.
32
+ class StatsdServer
33
+ def start
34
+ stop
35
+ @socket = UDPSocket.new
36
+ @socket.bind("127.0.0.1", 8125)
37
+
38
+ loop do
39
+ begin
40
+ # Listen for messages and track them on the messages Array.
41
+ packet = @socket.recvfrom(1024)
42
+ track_message packet.first
43
+ rescue Errno::EBADF # rubocop:disable Lint/HandleExceptions
44
+ # Ignore error for JRuby 9.1.17.0 specifically, it doesn't appear to
45
+ # happen on 9.2.18.0. It doesn't break the tests themselves, ignoring
46
+ # this error. It's probably a timing issue where it tries to read
47
+ # from the socket after it's closed.
48
+ end
49
+ end
50
+ end
51
+
52
+ def stop
53
+ @socket && @socket.close
54
+ ensure
55
+ @socket = nil
56
+ end
57
+
58
+ def messages
59
+ @messages ||= []
60
+ end
61
+
62
+ private
63
+
64
+ def track_message(message)
65
+ @messages_mutex ||= Mutex.new
66
+ @messages_mutex.synchronize { messages << message }
16
67
  end
17
68
  end
18
69
 
19
70
  let(:probe) { MockProbe.new }
20
71
  let(:launcher) { MockPumaLauncher.new }
72
+ let(:hostname) { Socket.gethostname }
73
+ let(:expected_default_tags) { { "hostname" => hostname } }
74
+ let(:stats_data) { { :backlog => 1 } }
21
75
  before do
22
76
  module Puma
23
77
  def self.stats
78
+ JSON.dump(@_stats_data)
79
+ end
80
+
81
+ def self.stats_hash
82
+ @_stats_data
24
83
  end
25
84
 
26
- def self.run
27
- # Capture threads running before application is preloaded
28
- before = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
29
-
30
- # An abbreviated version of what happens in Puma::Cluster#run
31
- launcher = MockPumaLauncher.new
32
- plugin = Plugin.plugin.new
33
- plugin.start(launcher)
34
- launcher.events.on_booted.call
35
-
36
- # Wait for minutely probe thread to finish starting
37
- sleep 0.005
38
-
39
- # Capture any new threads running after application is preloaded.
40
- # Any threads created during the preloading phase will not be
41
- # carried over into the forked workers. Puma warns about these
42
- # but the minutely probe thread should only exist in the main process.
43
- after = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
44
- $stdout.puts "! WARNING: Detected #{after.size - before.size} Thread(s) started in app boot" if after.size > before.size
85
+ def self._set_stats=(data)
86
+ @_stats_data = data
45
87
  end
46
88
 
47
89
  class Plugin
48
90
  class << self
49
- attr_reader :plugin
91
+ attr_reader :appsignal_plugin
50
92
 
51
93
  def create(&block)
52
- @plugin = Class.new(::Puma::Plugin)
53
- @plugin.class_eval(&block)
94
+ @appsignal_plugin = Class.new(::Puma::Plugin)
95
+ @appsignal_plugin.class_eval(&block)
54
96
  end
55
97
  end
56
- end
57
- end
58
98
 
59
- Appsignal::Minutely.probes.clear
60
- ENV["APPSIGNAL_ENABLE_MINUTELY_PROBES"] = "true"
61
- Appsignal.config = project_fixture_config
62
- # Speed up test time
63
- allow(Appsignal::Minutely).to receive(:initial_wait_time).and_return(0.001)
64
- allow(Appsignal::Minutely).to receive(:wait_time).and_return(0.001)
99
+ attr_reader :in_background_block
65
100
 
66
- Appsignal::Minutely.probes.register :my_probe, probe
101
+ def in_background(&block)
102
+ @in_background_block = block
103
+ end
104
+ end
105
+ end
106
+ Puma._set_stats = stats_data
67
107
  load File.expand_path("../lib/puma/plugin/appsignal.rb", APPSIGNAL_SPEC_DIR)
108
+
109
+ @statsd = StatsdServer.new
110
+ @server_thread = Thread.new { @statsd.start }
111
+ @server_thread.abort_on_exception = true
68
112
  end
69
113
  after do
70
- Appsignal.config = nil
71
- Object.send :remove_const, :Puma
72
- Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED
114
+ @statsd = nil
115
+
116
+ Object.send(:remove_const, :Puma)
117
+ Object.send(:remove_const, :AppsignalPumaPlugin)
118
+ end
119
+
120
+ def run(plugin)
121
+ @client_thread = Thread.new { start_plugin(plugin) }
122
+ @client_thread.abort_on_exception = true
123
+ sleep 0.03
124
+ ensure
125
+ stop_all
73
126
  end
74
127
 
75
- it "registers the PumaProbe" do
76
- expect(Appsignal::Minutely.probes[:my_probe]).to eql(probe)
77
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
78
- plugin = Puma::Plugin.plugin.new
79
- expect(launcher.events.on_booted).to be_nil
128
+ def appsignal_plugin
129
+ Puma::Plugin.appsignal_plugin
130
+ end
80
131
 
132
+ def start_plugin(plugin_class)
133
+ plugin = plugin_class.new
134
+ # Speed up test by not waiting for 60 seconds initial wait time and loop
135
+ # interval.
136
+ allow(plugin).to receive(:sleep_time).and_return(0.01)
81
137
  plugin.start(launcher)
82
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
83
- expect(launcher.events.on_booted).to_not be_nil
138
+ plugin.in_background_block.call
139
+ end
140
+
141
+ # Stop all threads in test and stop listening on the UDPSocket
142
+ def stop_all
143
+ @client_thread.kill if defined?(@client_thread) && @client_thread
144
+ @server_thread.kill if defined?(@server_thread) && @server_thread
145
+ @statsd.stop if defined?(@statsd) && @statsd
146
+ @client_thread = nil
147
+ @server_thread = nil
148
+ end
149
+
150
+ def logs
151
+ launcher.events.logs
152
+ end
153
+
154
+ def messages
155
+ @statsd.messages.map do |message|
156
+ metric, type, tags_string = message.split("|")
157
+ metric_name, metric_value = metric.split(":")
158
+ tags = {}
159
+ tags_string[1..-1].split(",").each do |tag|
160
+ key, value = tag.split(":")
161
+ tags[key] = value
162
+ end
163
+ {
164
+ :name => metric_name,
165
+ :value => metric_value.to_i,
166
+ :type => type,
167
+ :tags => tags
168
+ }
169
+ end
170
+ end
171
+
172
+ def expect_gauge(metric_name, metric_value, tags_hash = {})
173
+ expect(messages).to include(
174
+ :name => "puma_#{metric_name}",
175
+ :value => metric_value,
176
+ :type => "g",
177
+ :tags => expected_default_tags.merge(tags_hash)
178
+ )
179
+ end
180
+
181
+ context "with multiple worker stats" do
182
+ let(:stats_data) do
183
+ {
184
+ :workers => 2,
185
+ :booted_workers => 2,
186
+ :old_workers => 0,
187
+ :worker_status => [
188
+ {
189
+ :last_status => {
190
+ :backlog => 0,
191
+ :running => 5,
192
+ :pool_capacity => 5,
193
+ :max_threads => 5
194
+ }
195
+ },
196
+ {
197
+ :last_status => {
198
+ :backlog => 0,
199
+ :running => 5,
200
+ :pool_capacity => 5,
201
+ :max_threads => 5
202
+ }
203
+ }
204
+ ]
205
+ }
206
+ end
207
+
208
+ it "collects puma stats as guage metrics with the (summed) worker metrics" do
209
+ run(appsignal_plugin)
210
+
211
+ expect(logs).to_not include([:error, kind_of(String)])
212
+ expect_gauge(:workers, 2, "type" => "count")
213
+ expect_gauge(:workers, 2, "type" => "booted")
214
+ expect_gauge(:workers, 0, "type" => "old")
215
+ expect_gauge(:connection_backlog, 0)
216
+ expect_gauge(:pool_capacity, 10)
217
+ expect_gauge(:threads, 10, "type" => "running")
218
+ expect_gauge(:threads, 10, "type" => "max")
219
+ end
220
+ end
221
+
222
+ context "with single worker stats" do
223
+ let(:stats_data) do
224
+ {
225
+ :backlog => 0,
226
+ :running => 5,
227
+ :pool_capacity => 5,
228
+ :max_threads => 5
229
+ }
230
+ end
231
+
232
+ it "calls `puma_gauge` with the (summed) worker metrics" do
233
+ run(appsignal_plugin)
234
+
235
+ expect(logs).to_not include([:error, kind_of(String)])
236
+ expect_gauge(:connection_backlog, 0)
237
+ expect_gauge(:pool_capacity, 5)
238
+ expect_gauge(:threads, 5, "type" => "running")
239
+ expect_gauge(:threads, 5, "type" => "max")
240
+ end
241
+ end
84
242
 
85
- launcher.events.on_booted.call
86
- expect(Appsignal::Minutely.probes[:puma]).to eql(Appsignal::Probes::PumaProbe)
243
+ context "when using APPSIGNAL_HOSTNAME" do
244
+ let(:hostname) { "my-host-name" }
245
+ before { ENV["APPSIGNAL_HOSTNAME"] = hostname }
246
+ after { ENV.delete("APPSIGNAL_HOSTNAME") }
87
247
 
88
- # Minutely probes started and called
89
- wait_for("enough probe calls") { probe.calls >= 2 }
248
+ it "reports the APPSIGNAL_HOSTNAME as the hostname tag value" do
249
+ run(appsignal_plugin)
250
+
251
+ expect(logs).to_not include([:error, kind_of(String)])
252
+ expect_gauge(:connection_backlog, 1)
253
+ end
90
254
  end
91
255
 
92
- it "marks the PumaProbe thread as fork-safe" do
93
- out_stream = std_stream
94
- capture_stdout(out_stream) { Puma.run }
256
+ context "without Puma.stats_hash" do
257
+ before do
258
+ Puma.singleton_class.send(:remove_method, :stats_hash)
259
+ end
260
+
261
+ it "fetches metrics from Puma.stats instead" do
262
+ run(appsignal_plugin)
95
263
 
96
- expect(out_stream.read).not_to include("WARNING: Detected 1 Thread")
264
+ expect(logs).to_not include([:error, kind_of(String)])
265
+ expect(logs).to_not include([kind_of(Symbol), "AppSignal: No Puma stats to report."])
266
+ expect_gauge(:connection_backlog, 1)
267
+ end
97
268
  end
98
269
 
99
- context "without Puma.stats" do
100
- before { Puma.singleton_class.send(:remove_method, :stats) }
270
+ context "without Puma.stats and Puma.stats_hash" do
271
+ before do
272
+ Puma.singleton_class.send(:remove_method, :stats)
273
+ Puma.singleton_class.send(:remove_method, :stats_hash)
274
+ end
101
275
 
102
- it "does not register the PumaProbe" do
103
- expect(Appsignal::Minutely.probes[:my_probe]).to eql(probe)
104
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
105
- plugin = Puma::Plugin.plugin.new
106
- expect(launcher.events.on_booted).to be_nil
276
+ it "does not fetch metrics" do
277
+ run(appsignal_plugin)
107
278
 
108
- plugin.start(launcher)
109
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
110
- expect(launcher.events.on_booted).to_not be_nil
279
+ expect(logs).to_not include([:error, kind_of(String)])
280
+ expect(logs).to include([:log, "AppSignal: No Puma stats to report."])
281
+ expect(messages).to be_empty
282
+ end
283
+ end
111
284
 
112
- launcher.events.on_booted.call
113
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
285
+ context "without running StatsD server" do
286
+ it "does nothing" do
287
+ Appsignal.stop
288
+ stop_all
289
+ run(appsignal_plugin)
114
290
 
115
- # Minutely probes started and called
116
- wait_for("enough probe calls") { probe.calls >= 2 }
291
+ expect(logs).to_not include([:error, kind_of(String)])
292
+ expect(messages).to be_empty
117
293
  end
118
294
  end
119
295
  end