appsignal 3.0.3-java → 3.0.7-java

Sign up to get free protection for your applications and to get access to all the features.
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