appsignal 3.0.3 → 3.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.semaphore/semaphore.yml +64 -199
- data/CHANGELOG.md +47 -19
- data/README.md +9 -3
- data/appsignal.gemspec +16 -3
- data/build_matrix.yml +16 -24
- data/ext/agent.yml +17 -17
- data/ext/base.rb +12 -1
- data/gemfiles/capistrano2.gemfile +0 -1
- data/gemfiles/capistrano3.gemfile +0 -1
- data/gemfiles/grape.gemfile +0 -1
- data/gemfiles/no_dependencies.gemfile +4 -1
- data/gemfiles/rails-3.2.gemfile +2 -0
- data/gemfiles/rails-4.2.gemfile +6 -0
- data/gemfiles/resque-2.gemfile +0 -4
- data/gemfiles/sequel-435.gemfile +0 -1
- data/gemfiles/sequel.gemfile +0 -1
- data/gemfiles/sinatra.gemfile +0 -1
- data/lib/appsignal/config.rb +1 -0
- data/lib/appsignal/hooks.rb +2 -1
- data/lib/appsignal/hooks/excon.rb +19 -0
- data/lib/appsignal/hooks/puma.rb +1 -16
- data/lib/appsignal/integrations/excon.rb +20 -0
- data/lib/appsignal/integrations/padrino.rb +1 -1
- data/lib/appsignal/integrations/railtie.rb +1 -1
- data/lib/appsignal/integrations/redis.rb +8 -5
- data/lib/appsignal/integrations/sinatra.rb +1 -1
- data/lib/appsignal/probes.rb +0 -1
- data/lib/appsignal/version.rb +1 -1
- data/lib/puma/plugin/appsignal.rb +146 -17
- data/mono.yml +16 -0
- data/spec/lib/appsignal/cli/diagnose_spec.rb +1 -0
- data/spec/lib/appsignal/hooks/excon_spec.rb +74 -0
- data/spec/lib/appsignal/hooks/puma_spec.rb +0 -46
- data/spec/lib/appsignal/hooks/redis_spec.rb +34 -10
- data/spec/lib/appsignal/hooks_spec.rb +4 -1
- data/spec/lib/puma/appsignal_spec.rb +244 -68
- data/support/install_deps +9 -8
- metadata +8 -6
- data/lib/appsignal/probes/puma.rb +0 -61
- 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
|
-
|
42
|
+
"stub_id"
|
42
43
|
end
|
43
44
|
|
44
|
-
def
|
45
|
-
|
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
|
-
|
56
|
-
expect(
|
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
|
-
|
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).
|
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
|
-
|
14
|
-
|
15
|
-
|
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.
|
27
|
-
|
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 :
|
91
|
+
attr_reader :appsignal_plugin
|
50
92
|
|
51
93
|
def create(&block)
|
52
|
-
@
|
53
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
71
|
-
|
72
|
-
Object.send
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
86
|
-
|
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
|
-
|
89
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
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
|
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
|
103
|
-
|
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
|
-
|
109
|
-
expect(
|
110
|
-
expect(
|
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
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
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
|