appsignal 3.0.6 → 3.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -43,6 +43,14 @@ describe Appsignal::System do
43
43
  end
44
44
  end
45
45
 
46
+ context "when using the APPSIGNAL_BUILD_FOR_LINUX_ARM env var" do
47
+ it "returns the linux build" do
48
+ ENV["APPSIGNAL_BUILD_FOR_LINUX_ARM"] = "1"
49
+ is_expected.to eq("linux")
50
+ ENV.delete("APPSIGNAL_BUILD_FOR_LINUX_ARM")
51
+ end
52
+ end
53
+
46
54
  context "when on a musl system" do
47
55
  let(:ldd_output) { "musl libc (x86_64)\nVersion 1.1.16" }
48
56
 
@@ -93,4 +101,26 @@ describe Appsignal::System do
93
101
  end
94
102
  end
95
103
  end
104
+
105
+ describe ".agent_architecture" do
106
+ let(:architecture) { "x86_64" }
107
+ let(:ldd_output) { "" }
108
+ before do
109
+ allow(RbConfig::CONFIG).to receive(:[])
110
+ allow(RbConfig::CONFIG).to receive(:[]).with("host_cpu").and_return(architecture)
111
+ end
112
+ subject { described_class.agent_architecture }
113
+
114
+ it "returns the host CPU value" do
115
+ is_expected.to eq(architecture)
116
+ end
117
+
118
+ context "when using the APPSIGNAL_BUILD_FOR_LINUX_ARM env var" do
119
+ it "returns ARM 64 bit" do
120
+ ENV["APPSIGNAL_BUILD_FOR_LINUX_ARM"] = "1"
121
+ is_expected.to eq("aarch64")
122
+ ENV.delete("APPSIGNAL_BUILD_FOR_LINUX_ARM")
123
+ end
124
+ end
125
+ end
96
126
  end
@@ -1321,6 +1321,13 @@ describe Appsignal::Transaction do
1321
1321
  expect(subject).to eq ["line 1", "line 2"]
1322
1322
  end
1323
1323
 
1324
+ context "with Rails module but without backtrace_cleaner method" do
1325
+ it "returns the backtrace uncleaned" do
1326
+ stub_const("Rails", Module.new)
1327
+ expect(subject).to eq ["line 1", "line 2"]
1328
+ end
1329
+ end
1330
+
1324
1331
  if rails_present?
1325
1332
  context "with rails" do
1326
1333
  it "cleans the backtrace with the Rails backtrace cleaner" do
@@ -10,110 +10,290 @@ RSpec.describe "Puma plugin" do
10
10
  end
11
11
 
12
12
  class MockPumaEvents
13
- def on_booted(&block)
14
- @on_booted = block if block_given?
15
- @on_booted if defined?(@on_booted)
13
+ attr_reader :logs
14
+
15
+ def initialize
16
+ @logs = []
17
+ end
18
+
19
+ def log(message)
20
+ @logs << [:log, message]
21
+ end
22
+
23
+ def debug(message)
24
+ @logs << [:debug, message]
25
+ end
26
+
27
+ def error(message)
28
+ @logs << [:error, message]
29
+ end
30
+ end
31
+
32
+ # StatsD server used for these tests.
33
+ # Open a UDPSocket and listen for messages sent by the AppSignal Puma plugin.
34
+ class StatsdServer
35
+ def start
36
+ stop
37
+ @socket = UDPSocket.new
38
+ @socket.bind("127.0.0.1", 8125)
39
+
40
+ loop do
41
+ begin
42
+ # Listen for messages and track them on the messages Array.
43
+ packet = @socket.recvfrom(1024)
44
+ track_message packet.first
45
+ rescue Errno::EBADF # rubocop:disable Lint/HandleExceptions
46
+ # Ignore error for JRuby 9.1.17.0 specifically, it doesn't appear to
47
+ # happen on 9.2.18.0. It doesn't break the tests themselves, ignoring
48
+ # this error. It's probably a timing issue where it tries to read
49
+ # from the socket after it's closed.
50
+ end
51
+ end
52
+ end
53
+
54
+ def stop
55
+ defined?(@socket) && @socket && @socket.close
56
+ ensure
57
+ @socket = nil
58
+ end
59
+
60
+ def messages
61
+ @messages ||= []
62
+ end
63
+
64
+ private
65
+
66
+ def track_message(message)
67
+ @messages_mutex ||= Mutex.new
68
+ @messages_mutex.synchronize { messages << message }
16
69
  end
17
70
  end
18
71
 
19
72
  let(:probe) { MockProbe.new }
20
73
  let(:launcher) { MockPumaLauncher.new }
74
+ let(:hostname) { Socket.gethostname }
75
+ let(:expected_default_tags) { { "hostname" => hostname } }
76
+ let(:stats_data) { { :backlog => 1 } }
77
+ before :context do
78
+ Appsignal.stop
79
+ end
21
80
  before do
22
81
  module Puma
23
82
  def self.stats
83
+ JSON.dump(@_stats_data)
84
+ end
85
+
86
+ def self.stats_hash
87
+ @_stats_data
24
88
  end
25
89
 
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
90
+ def self._set_stats=(data)
91
+ @_stats_data = data
45
92
  end
46
93
 
47
94
  class Plugin
48
95
  class << self
49
- attr_reader :plugin
96
+ attr_reader :appsignal_plugin
50
97
 
51
98
  def create(&block)
52
- @plugin = Class.new(::Puma::Plugin)
53
- @plugin.class_eval(&block)
99
+ @appsignal_plugin = Class.new(::Puma::Plugin)
100
+ @appsignal_plugin.class_eval(&block)
54
101
  end
55
102
  end
56
- end
57
- end
58
103
 
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)
104
+ attr_reader :in_background_block
65
105
 
66
- Appsignal::Minutely.probes.register :my_probe, probe
106
+ def in_background(&block)
107
+ @in_background_block = block
108
+ end
109
+ end
110
+ end
111
+ Puma._set_stats = stats_data
67
112
  load File.expand_path("../lib/puma/plugin/appsignal.rb", APPSIGNAL_SPEC_DIR)
113
+
114
+ @statsd = StatsdServer.new
115
+ @server_thread = Thread.new { @statsd.start }
116
+ @server_thread.abort_on_exception = true
68
117
  end
69
118
  after do
70
- Appsignal.config = nil
71
- Object.send :remove_const, :Puma
72
- Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED
119
+ @statsd = nil
120
+
121
+ Object.send(:remove_const, :Puma)
122
+ Object.send(:remove_const, :AppsignalPumaPlugin)
123
+ end
124
+
125
+ def run_plugin(plugin, &block)
126
+ @client_thread = Thread.new { start_plugin(plugin) }
127
+ @client_thread.abort_on_exception = true
128
+ wait_for(:puma_client_wait, &block)
129
+ ensure
130
+ stop_all
73
131
  end
74
132
 
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
133
+ def appsignal_plugin
134
+ Puma::Plugin.appsignal_plugin
135
+ end
80
136
 
137
+ def start_plugin(plugin_class)
138
+ plugin = plugin_class.new
139
+ # Speed up test by not waiting for 60 seconds initial wait time and loop
140
+ # interval.
141
+ allow(plugin).to receive(:sleep_time).and_return(0.01)
81
142
  plugin.start(launcher)
82
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
83
- expect(launcher.events.on_booted).to_not be_nil
143
+ plugin.in_background_block.call
144
+ end
145
+
146
+ # Stop all threads in test and stop listening on the UDPSocket
147
+ def stop_all
148
+ @client_thread.kill if defined?(@client_thread) && @client_thread
149
+ @server_thread.kill if defined?(@server_thread) && @server_thread
150
+ @statsd.stop if defined?(@statsd) && @statsd
151
+ @client_thread = nil
152
+ @server_thread = nil
153
+ end
154
+
155
+ def logs
156
+ launcher.events.logs
157
+ end
158
+
159
+ def messages
160
+ @statsd.messages.map do |message|
161
+ metric, type, tags_string = message.split("|")
162
+ metric_name, metric_value = metric.split(":")
163
+ tags = {}
164
+ tags_string[1..-1].split(",").each do |tag|
165
+ key, value = tag.split(":")
166
+ tags[key] = value
167
+ end
168
+ {
169
+ :name => metric_name,
170
+ :value => metric_value.to_i,
171
+ :type => type,
172
+ :tags => tags
173
+ }
174
+ end
175
+ end
176
+
177
+ def expect_gauge(metric_name, metric_value, tags_hash = {})
178
+ expect(messages).to include(
179
+ :name => "puma_#{metric_name}",
180
+ :value => metric_value,
181
+ :type => "g",
182
+ :tags => expected_default_tags.merge(tags_hash)
183
+ )
184
+ end
185
+
186
+ context "with multiple worker stats" do
187
+ let(:stats_data) do
188
+ {
189
+ :workers => 2,
190
+ :booted_workers => 2,
191
+ :old_workers => 0,
192
+ :worker_status => [
193
+ {
194
+ :last_status => {
195
+ :backlog => 0,
196
+ :running => 5,
197
+ :pool_capacity => 5,
198
+ :max_threads => 5
199
+ }
200
+ },
201
+ {
202
+ :last_status => {
203
+ :backlog => 0,
204
+ :running => 5,
205
+ :pool_capacity => 5,
206
+ :max_threads => 5
207
+ }
208
+ }
209
+ ]
210
+ }
211
+ end
212
+
213
+ it "collects puma stats as guage metrics with the (summed) worker metrics" do
214
+ run_plugin(appsignal_plugin) do
215
+ expect(logs).to_not include([:error, kind_of(String)])
216
+ expect_gauge(:workers, 2, "type" => "count")
217
+ expect_gauge(:workers, 2, "type" => "booted")
218
+ expect_gauge(:workers, 0, "type" => "old")
219
+ expect_gauge(:connection_backlog, 0)
220
+ expect_gauge(:pool_capacity, 10)
221
+ expect_gauge(:threads, 10, "type" => "running")
222
+ expect_gauge(:threads, 10, "type" => "max")
223
+ end
224
+ end
225
+ end
84
226
 
85
- launcher.events.on_booted.call
86
- expect(Appsignal::Minutely.probes[:puma]).to eql(Appsignal::Probes::PumaProbe)
227
+ context "with single worker stats" do
228
+ let(:stats_data) do
229
+ {
230
+ :backlog => 0,
231
+ :running => 5,
232
+ :pool_capacity => 5,
233
+ :max_threads => 5
234
+ }
235
+ end
87
236
 
88
- # Minutely probes started and called
89
- wait_for("enough probe calls") { probe.calls >= 2 }
237
+ it "calls `puma_gauge` with the (summed) worker metrics" do
238
+ run_plugin(appsignal_plugin) do
239
+ expect(logs).to_not include([:error, kind_of(String)])
240
+ expect_gauge(:connection_backlog, 0)
241
+ expect_gauge(:pool_capacity, 5)
242
+ expect_gauge(:threads, 5, "type" => "running")
243
+ expect_gauge(:threads, 5, "type" => "max")
244
+ end
245
+ end
90
246
  end
91
247
 
92
- it "marks the PumaProbe thread as fork-safe" do
93
- out_stream = std_stream
94
- capture_stdout(out_stream) { Puma.run }
248
+ context "when using APPSIGNAL_HOSTNAME" do
249
+ let(:hostname) { "my-host-name" }
250
+ before { ENV["APPSIGNAL_HOSTNAME"] = hostname }
251
+ after { ENV.delete("APPSIGNAL_HOSTNAME") }
95
252
 
96
- expect(out_stream.read).not_to include("WARNING: Detected 1 Thread")
253
+ it "reports the APPSIGNAL_HOSTNAME as the hostname tag value" do
254
+ run_plugin(appsignal_plugin) do
255
+ expect(logs).to_not include([:error, kind_of(String)])
256
+ expect_gauge(:connection_backlog, 1)
257
+ end
258
+ end
97
259
  end
98
260
 
99
- context "without Puma.stats" do
100
- before { Puma.singleton_class.send(:remove_method, :stats) }
261
+ context "without Puma.stats_hash" do
262
+ before do
263
+ Puma.singleton_class.send(:remove_method, :stats_hash)
264
+ end
101
265
 
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
266
+ it "fetches metrics from Puma.stats instead" do
267
+ run_plugin(appsignal_plugin) do
268
+ expect(logs).to_not include([:error, kind_of(String)])
269
+ expect(logs).to_not include([kind_of(Symbol), "AppSignal: No Puma stats to report."])
270
+ expect_gauge(:connection_backlog, 1)
271
+ end
272
+ end
273
+ end
107
274
 
108
- plugin.start(launcher)
109
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
110
- expect(launcher.events.on_booted).to_not be_nil
275
+ context "without Puma.stats and Puma.stats_hash" do
276
+ before do
277
+ Puma.singleton_class.send(:remove_method, :stats)
278
+ Puma.singleton_class.send(:remove_method, :stats_hash)
279
+ end
111
280
 
112
- launcher.events.on_booted.call
113
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
281
+ it "does not fetch metrics" do
282
+ run_plugin(appsignal_plugin) do
283
+ expect(logs).to_not include([:error, kind_of(String)])
284
+ expect(logs).to include([:log, "AppSignal: No Puma stats to report."])
285
+ expect(messages).to be_empty
286
+ end
287
+ end
288
+ end
114
289
 
115
- # Minutely probes started and called
116
- wait_for("enough probe calls") { probe.calls >= 2 }
290
+ context "without running StatsD server" do
291
+ it "does nothing" do
292
+ stop_all
293
+ run_plugin(appsignal_plugin) do
294
+ expect(logs).to_not include([:error, kind_of(String)])
295
+ expect(messages).to be_empty
296
+ end
117
297
  end
118
298
  end
119
299
  end
@@ -16,13 +16,25 @@ module WaitForHelper
16
16
  def wait_for(name)
17
17
  max_wait = 5_000
18
18
  i = 0
19
+ error = nil
19
20
  while i < max_wait
20
- break if yield
21
- i += 1
22
- sleep 0.001
21
+ begin
22
+ result = yield
23
+ break if result
24
+ rescue Exception => e # rubocop:disable Lint/RescueException
25
+ # Capture error so we know if it exited with an error
26
+ error = e
27
+ ensure
28
+ i += 1
29
+ sleep 0.001
30
+ end
23
31
  end
24
32
 
25
33
  return unless i >= max_wait
26
- raise "Waited 5 seconds for #{name} condition, but was not met."
34
+ error_message =
35
+ if error
36
+ "\nError: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}"
37
+ end
38
+ raise "Waited 5 seconds for #{name} condition, but was not met.#{error_message}"
27
39
  end
28
40
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appsignal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.6
4
+ version: 3.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Beekman
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-05-31 00:00:00.000000000 Z
13
+ date: 2021-07-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -136,6 +136,7 @@ files:
136
136
  - ".github/ISSUE_TEMPLATE/bug_report.md"
137
137
  - ".github/ISSUE_TEMPLATE/chore.md"
138
138
  - ".gitignore"
139
+ - ".gitmodules"
139
140
  - ".rspec"
140
141
  - ".rubocop.yml"
141
142
  - ".rubocop_todo.yml"
@@ -249,7 +250,6 @@ files:
249
250
  - lib/appsignal/marker.rb
250
251
  - lib/appsignal/minutely.rb
251
252
  - lib/appsignal/probes.rb
252
- - lib/appsignal/probes/puma.rb
253
253
  - lib/appsignal/probes/sidekiq.rb
254
254
  - lib/appsignal/rack/generic_instrumentation.rb
255
255
  - lib/appsignal/rack/rails_instrumentation.rb
@@ -271,6 +271,7 @@ files:
271
271
  - mono.yml
272
272
  - resources/appsignal.yml.erb
273
273
  - resources/cacert.pem
274
+ - script/install_lintje
274
275
  - spec/.rubocop.yml
275
276
  - spec/lib/appsignal/auth_check_spec.rb
276
277
  - spec/lib/appsignal/capistrano2_spec.rb
@@ -335,7 +336,6 @@ files:
335
336
  - spec/lib/appsignal/logger_spec.rb
336
337
  - spec/lib/appsignal/marker_spec.rb
337
338
  - spec/lib/appsignal/minutely_spec.rb
338
- - spec/lib/appsignal/probes/puma_spec.rb
339
339
  - spec/lib/appsignal/probes/sidekiq_spec.rb
340
340
  - spec/lib/appsignal/rack/generic_instrumentation_spec.rb
341
341
  - spec/lib/appsignal/rack/rails_instrumentation_spec.rb
@@ -484,7 +484,6 @@ test_files:
484
484
  - spec/lib/appsignal/logger_spec.rb
485
485
  - spec/lib/appsignal/marker_spec.rb
486
486
  - spec/lib/appsignal/minutely_spec.rb
487
- - spec/lib/appsignal/probes/puma_spec.rb
488
487
  - spec/lib/appsignal/probes/sidekiq_spec.rb
489
488
  - spec/lib/appsignal/rack/generic_instrumentation_spec.rb
490
489
  - spec/lib/appsignal/rack/rails_instrumentation_spec.rb