appsignal 3.6.5 → 3.7.1

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.
@@ -2,6 +2,274 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
+ class ProbeCollection
6
+ def initialize
7
+ @probes = {}
8
+ end
9
+
10
+ # @return [Integer] Number of probes that are registered.
11
+ def count
12
+ probes.count
13
+ end
14
+
15
+ # Clears all probes from the list.
16
+ # @return [void]
17
+ def clear
18
+ probes.clear
19
+ end
20
+
21
+ # Fetch a probe using its name.
22
+ # @param key [Symbol/String] The name of the probe to fetch.
23
+ # @return [Object] Returns the registered probe.
24
+ def [](key)
25
+ probes[key]
26
+ end
27
+
28
+ # @deprecated Use {Appsignal::Probes.register} instead.
29
+ def register(name, probe)
30
+ Appsignal::Utils::StdoutAndLoggerMessage.warning(
31
+ "The method 'Appsignal::Probes.probes.register' is deprecated. " \
32
+ "Use 'Appsignal::Probes.register' instead."
33
+ )
34
+ Appsignal::Probes.register(name, probe)
35
+ end
36
+
37
+ # @api private
38
+ def internal_register(name, probe)
39
+ if probes.key?(name)
40
+ logger.debug "A probe with the name `#{name}` is already " \
41
+ "registered. Overwriting the entry with the new probe."
42
+ end
43
+ probes[name] = probe
44
+ end
45
+
46
+ # @api private
47
+ def unregister(name)
48
+ probes.delete(name)
49
+ end
50
+
51
+ # @api private
52
+ def each(&block)
53
+ probes.each(&block)
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :probes
59
+
60
+ def logger
61
+ Appsignal.internal_logger
62
+ end
63
+ end
64
+
65
+ class << self
66
+ # @api private
67
+ def mutex
68
+ @mutex ||= Thread::Mutex.new
69
+ end
70
+
71
+ # @see ProbeCollection
72
+ # @return [ProbeCollection] Returns list of probes.
73
+ def probes
74
+ @probes ||= ProbeCollection.new
75
+ end
76
+
77
+ # Register a new minutely probe.
78
+ #
79
+ # Supported probe types are:
80
+ #
81
+ # - Lambda - A lambda is an object that listens to a `call` method call.
82
+ # This `call` method is called every minute.
83
+ # - Class - A class object is an object that listens to a `new` and
84
+ # `call` method call. The `new` method is called when the minutely
85
+ # probe thread is started to initialize all probes. This allows probes
86
+ # to load dependencies once beforehand. Their `call` method is called
87
+ # every minute.
88
+ # - Class instance - A class instance object is an object that listens to
89
+ # a `call` method call. The `call` method is called every minute.
90
+ #
91
+ # @example Register a new probe
92
+ # Appsignal::Probes.register :my_probe, lambda {}
93
+ #
94
+ # @example Overwrite an existing registered probe
95
+ # Appsignal::Probes.register :my_probe, lambda {}
96
+ # Appsignal::Probes.register :my_probe, lambda { puts "hello" }
97
+ #
98
+ # @example Add a lambda as a probe
99
+ # Appsignal::Probes.register :my_probe, lambda { puts "hello" }
100
+ # # "hello" # printed every minute
101
+ #
102
+ # @example Add a probe instance
103
+ # class MyProbe
104
+ # def initialize
105
+ # puts "started"
106
+ # end
107
+ #
108
+ # def call
109
+ # puts "called"
110
+ # end
111
+ # end
112
+ #
113
+ # Appsignal::Probes.register :my_probe, MyProbe.new
114
+ # # "started" # printed immediately
115
+ # # "called" # printed every minute
116
+ #
117
+ # @example Add a probe class
118
+ # class MyProbe
119
+ # def initialize
120
+ # # Add things that only need to be done on start up for this probe
121
+ # require "some/library/dependency"
122
+ # @cache = {} # initialize a local cache variable
123
+ # puts "started"
124
+ # end
125
+ #
126
+ # def call
127
+ # puts "called"
128
+ # end
129
+ # end
130
+ #
131
+ # Appsignal::Probes.register :my_probe, MyProbe
132
+ # Appsignal::Probes.start # This is called for you
133
+ # # "started" # Printed on Appsignal::Probes.start
134
+ # # "called" # Repeated every minute
135
+ #
136
+ # @param name [Symbol/String] Name of the probe. Can be used with
137
+ # {ProbeCollection#[]}. This name will be used in errors in the log and
138
+ # allows overwriting of probes by registering new ones with the same
139
+ # name.
140
+ # @param probe [Object] Any object that listens to the `call` method will
141
+ # be used as a probe.
142
+ # @return [void]
143
+ def register(name, probe)
144
+ probes.internal_register(name, probe)
145
+
146
+ initialize_probe(name, probe) if started?
147
+ end
148
+
149
+ # Unregister a probe that's registered with {register}.
150
+ # Can also be used to unregister automatically registered probes by the
151
+ # gem.
152
+ #
153
+ # @example Unregister probes
154
+ # # First register a probe
155
+ # Appsignal::Probes.register :my_probe, lambda {}
156
+ #
157
+ # # Then unregister a probe if needed
158
+ # Appsignal::Probes.unregister :my_probe
159
+ #
160
+ # @param name [Symbol/String] Name of the probe used to {register} the
161
+ # probe.
162
+ # @return [void]
163
+ def unregister(name)
164
+ probes.unregister(name)
165
+
166
+ uninitialize_probe(name)
167
+ end
168
+
169
+ # @api private
170
+ def start
171
+ stop
172
+ @started = true
173
+ @thread = Thread.new do
174
+ # Advise multi-threaded app servers to ignore this thread
175
+ # for the purposes of fork safety warnings
176
+ if Thread.current.respond_to?(:thread_variable_set)
177
+ Thread.current.thread_variable_set(:fork_safe, true)
178
+ end
179
+
180
+ sleep initial_wait_time
181
+ initialize_probes
182
+ loop do
183
+ logger = Appsignal.internal_logger
184
+ mutex.synchronize do
185
+ logger.debug("Gathering minutely metrics with #{probe_instances.count} probes")
186
+ probe_instances.each do |name, probe|
187
+ logger.debug("Gathering minutely metrics with '#{name}' probe")
188
+ probe.call
189
+ rescue => ex
190
+ logger.error "Error in minutely probe '#{name}': #{ex}"
191
+ logger.debug ex.backtrace.join("\n")
192
+ end
193
+ end
194
+ sleep wait_time
195
+ end
196
+ end
197
+ end
198
+
199
+ # Returns if the probes thread has been started. If the value is false or
200
+ # nil, it has not been started yet.
201
+ #
202
+ # @return [Boolean, nil]
203
+ def started?
204
+ @started
205
+ end
206
+
207
+ # Stop the minutely probes mechanism. Stop the thread and clear all probe
208
+ # instances.
209
+ def stop
210
+ defined?(@thread) && @thread.kill
211
+ @started = false
212
+ probe_instances.clear
213
+ end
214
+
215
+ # @api private
216
+ def wait_time
217
+ 60 - Time.now.sec
218
+ end
219
+
220
+ private
221
+
222
+ def initial_wait_time
223
+ remaining_seconds = 60 - Time.now.sec
224
+ return remaining_seconds if remaining_seconds > 30
225
+
226
+ remaining_seconds + 60
227
+ end
228
+
229
+ def initialize_probes
230
+ probes.each do |name, probe|
231
+ initialize_probe(name, probe)
232
+ end
233
+ end
234
+
235
+ def initialize_probe(name, probe)
236
+ if probe.respond_to? :new
237
+ instance = probe.new
238
+ klass = probe
239
+ else
240
+ instance = probe
241
+ klass = instance.class
242
+ end
243
+ unless dependencies_present?(klass)
244
+ Appsignal.internal_logger.debug "Skipping '#{name}' probe, " \
245
+ "#{klass}.dependency_present? returned falsy"
246
+ return
247
+ end
248
+ mutex.synchronize do
249
+ probe_instances[name] = instance
250
+ end
251
+ rescue => error
252
+ logger = Appsignal.internal_logger
253
+ logger.error "Error while initializing minutely probe '#{name}': #{error}"
254
+ logger.debug error.backtrace.join("\n")
255
+ end
256
+
257
+ def uninitialize_probe(name)
258
+ mutex.synchronize do
259
+ probe_instances.delete(name)
260
+ end
261
+ end
262
+
263
+ def dependencies_present?(probe)
264
+ return true unless probe.respond_to? :dependencies_present?
265
+
266
+ probe.dependencies_present?
267
+ end
268
+
269
+ def probe_instances
270
+ @probe_instances ||= {}
271
+ end
272
+ end
5
273
  end
6
274
  end
7
275
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Utils
5
+ # @api private
6
+ module StdoutAndLoggerMessage
7
+ def self.warning(message, logger = Appsignal.internal_logger)
8
+ Kernel.warn "appsignal WARNING: #{message}"
9
+ logger.warn message
10
+ end
11
+
12
+ def stdout_and_logger_warning(message, logger = Appsignal.internal_logger)
13
+ Appsignal::Utils::StdoutAndLoggerMessage.warning(message, logger)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "appsignal/utils/deprecation_message"
3
+ require "appsignal/utils/stdout_and_logger_message"
4
4
  require "appsignal/utils/data"
5
5
  require "appsignal/utils/hash_sanitizer"
6
6
  require "appsignal/utils/integration_logger"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "3.6.5"
4
+ VERSION = "3.7.1"
5
5
  end
data/lib/appsignal.rb CHANGED
@@ -5,7 +5,7 @@ require "securerandom"
5
5
  require "stringio"
6
6
 
7
7
  require "appsignal/logger"
8
- require "appsignal/utils/deprecation_message"
8
+ require "appsignal/utils/stdout_and_logger_message"
9
9
  require "appsignal/helpers/instrumentation"
10
10
  require "appsignal/helpers/metrics"
11
11
 
@@ -120,7 +120,7 @@ module Appsignal
120
120
  Appsignal::Environment.report_enabled("allocation_tracking")
121
121
  end
122
122
 
123
- Appsignal::Minutely.start if config[:enable_minutely_probes]
123
+ Appsignal::Probes.start if config[:enable_minutely_probes]
124
124
 
125
125
  collect_environment_metadata
126
126
  else
@@ -152,6 +152,7 @@ module Appsignal
152
152
  internal_logger.debug("Stopping appsignal")
153
153
  end
154
154
  Appsignal::Extension.stop
155
+ Appsignal::Probes.stop
155
156
  end
156
157
 
157
158
  def forked
@@ -218,7 +219,10 @@ module Appsignal
218
219
  else
219
220
  Appsignal::Config::DEFAULT_LOG_LEVEL
220
221
  end
221
- internal_logger << @in_memory_log.string if @in_memory_log
222
+ return unless @in_memory_log
223
+
224
+ internal_logger << @in_memory_log.string
225
+ @in_memory_log = nil
222
226
  end
223
227
 
224
228
  # Returns if the C-extension was loaded properly.
@@ -287,6 +291,22 @@ module Appsignal
287
291
  end
288
292
  Appsignal::Environment.report_supported_gems
289
293
  end
294
+
295
+ # Alias constants that have moved with a warning message that points to the
296
+ # place to update the reference.
297
+ def const_missing(name)
298
+ case name
299
+ when :Minutely
300
+ callers = caller
301
+ Appsignal::Utils::StdoutAndLoggerMessage.warning \
302
+ "The constant Appsignal::Minutely has been deprecated. " \
303
+ "Please update the constant name to Appsignal::Probes " \
304
+ "in the following file to remove this message.\n#{callers.first}"
305
+ Appsignal::Probes
306
+ else
307
+ super
308
+ end
309
+ end
290
310
  end
291
311
  end
292
312
 
@@ -300,10 +320,10 @@ require "appsignal/event_formatter"
300
320
  require "appsignal/hooks"
301
321
  require "appsignal/probes"
302
322
  require "appsignal/marker"
303
- require "appsignal/minutely"
304
323
  require "appsignal/garbage_collection"
305
324
  require "appsignal/integrations/railtie" if defined?(::Rails)
306
325
  require "appsignal/transaction"
307
326
  require "appsignal/version"
308
327
  require "appsignal/rack/generic_instrumentation"
309
328
  require "appsignal/transmitter"
329
+ require "appsignal/heartbeat"
@@ -172,6 +172,7 @@ describe Appsignal::Config do
172
172
  :filter_session_data => [],
173
173
  :ignore_actions => [],
174
174
  :ignore_errors => [],
175
+ :ignore_logs => [],
175
176
  :ignore_namespaces => [],
176
177
  :instrument_http_rb => true,
177
178
  :instrument_net_http => true,
@@ -421,6 +422,7 @@ describe Appsignal::Config do
421
422
  :dns_servers => ["8.8.8.8", "8.8.4.4"],
422
423
  :ignore_actions => %w[action1 action2],
423
424
  :ignore_errors => %w[ExampleStandardError AnotherError],
425
+ :ignore_logs => ["^start$", "^Completed 2.* in .*ms (.*)"],
424
426
  :ignore_namespaces => %w[admin private_namespace],
425
427
  :instrument_net_http => false,
426
428
  :instrument_redis => false,
@@ -443,6 +445,7 @@ describe Appsignal::Config do
443
445
  ENV["APPSIGNAL_DNS_SERVERS"] = "8.8.8.8,8.8.4.4"
444
446
  ENV["APPSIGNAL_IGNORE_ACTIONS"] = "action1,action2"
445
447
  ENV["APPSIGNAL_IGNORE_ERRORS"] = "ExampleStandardError,AnotherError"
448
+ ENV["APPSIGNAL_IGNORE_LOGS"] = "^start$,^Completed 2.* in .*ms (.*)"
446
449
  ENV["APPSIGNAL_IGNORE_NAMESPACES"] = "admin,private_namespace"
447
450
  ENV["APPSIGNAL_INSTRUMENT_NET_HTTP"] = "false"
448
451
  ENV["APPSIGNAL_INSTRUMENT_REDIS"] = "false"
@@ -639,6 +642,7 @@ describe Appsignal::Config do
639
642
  config[:http_proxy] = "http://localhost"
640
643
  config[:ignore_actions] = %w[action1 action2]
641
644
  config[:ignore_errors] = %w[ExampleStandardError AnotherError]
645
+ config[:ignore_logs] = ["^start$", "^Completed 2.* in .*ms (.*)"]
642
646
  config[:ignore_namespaces] = %w[admin private_namespace]
643
647
  config[:log] = "stdout"
644
648
  config[:log_path] = "/tmp"
@@ -672,6 +676,7 @@ describe Appsignal::Config do
672
676
  expect(ENV.fetch("_APPSIGNAL_HTTP_PROXY", nil)).to eq "http://localhost"
673
677
  expect(ENV.fetch("_APPSIGNAL_IGNORE_ACTIONS", nil)).to eq "action1,action2"
674
678
  expect(ENV.fetch("_APPSIGNAL_IGNORE_ERRORS", nil)).to eq "ExampleStandardError,AnotherError"
679
+ expect(ENV.fetch("_APPSIGNAL_IGNORE_LOGS", nil)).to eq "^start$,^Completed 2.* in .*ms (.*)"
675
680
  expect(ENV.fetch("_APPSIGNAL_IGNORE_NAMESPACES", nil)).to eq "admin,private_namespace"
676
681
  expect(ENV.fetch("_APPSIGNAL_RUNNING_IN_CONTAINER", nil)).to eq "false"
677
682
  expect(ENV.fetch("_APPSIGNAL_ENABLE_HOST_METRICS", nil)).to eq "true"
@@ -752,7 +757,10 @@ describe Appsignal::Config do
752
757
  let(:out_stream) { std_stream }
753
758
  let(:output) { out_stream.read }
754
759
  let(:config) { project_fixture_config("production", :log_path => log_path) }
755
- subject { capture_stdout(out_stream) { config.log_file_path } }
760
+
761
+ def log_file_path
762
+ capture_stdout(out_stream) { config.log_file_path }
763
+ end
756
764
 
757
765
  context "when path is writable" do
758
766
  let(:log_path) { File.join(tmp_dir, "writable-path") }
@@ -760,11 +768,11 @@ describe Appsignal::Config do
760
768
  after { FileUtils.rm_rf(log_path) }
761
769
 
762
770
  it "returns log file path" do
763
- expect(subject).to eq File.join(log_path, "appsignal.log")
771
+ expect(log_file_path).to eq File.join(log_path, "appsignal.log")
764
772
  end
765
773
 
766
774
  it "prints no warning" do
767
- subject
775
+ log_file_path
768
776
  expect(output).to be_empty
769
777
  end
770
778
  end
@@ -778,28 +786,47 @@ describe Appsignal::Config do
778
786
  before { FileUtils.chmod(0o777, system_tmp_dir) }
779
787
 
780
788
  it "returns returns the tmp location" do
781
- expect(subject).to eq(File.join(system_tmp_dir, "appsignal.log"))
789
+ expect(log_file_path).to eq(File.join(system_tmp_dir, "appsignal.log"))
782
790
  end
783
791
 
784
792
  it "prints a warning" do
785
- subject
793
+ log_file_path
786
794
  expect(output).to include "appsignal: Unable to log to '#{log_path}'. " \
787
795
  "Logging to '#{system_tmp_dir}' instead."
788
796
  end
797
+
798
+ it "prints a warning once" do
799
+ capture_stdout(out_stream) do
800
+ log_file_path
801
+ log_file_path
802
+ end
803
+ message = "appsignal: Unable to log to '#{log_path}'. " \
804
+ "Logging to '#{system_tmp_dir}' instead."
805
+ expect(output.scan(message).count).to eq(1)
806
+ end
789
807
  end
790
808
 
791
809
  context "when the /tmp fallback path is not writable" do
792
810
  before { FileUtils.chmod(0o555, system_tmp_dir) }
793
811
 
794
812
  it "returns nil" do
795
- expect(subject).to be_nil
813
+ expect(log_file_path).to be_nil
796
814
  end
797
815
 
798
816
  it "prints a warning" do
799
- subject
817
+ log_file_path
800
818
  expect(output).to include "appsignal: Unable to log to '#{log_path}' " \
801
819
  "or the '#{system_tmp_dir}' fallback."
802
820
  end
821
+
822
+ it "prints a warning once" do
823
+ capture_stdout(out_stream) do
824
+ log_file_path
825
+ log_file_path
826
+ end
827
+ message = "appsignal: Unable to log to '#{log_path}' or the '#{system_tmp_dir}' fallback."
828
+ expect(output.scan(message).count).to eq(1)
829
+ end
803
830
  end
804
831
  end
805
832
 
@@ -814,11 +841,11 @@ describe Appsignal::Config do
814
841
 
815
842
  context "when root_path is set" do
816
843
  it "returns returns the project log location" do
817
- expect(subject).to eq File.join(config.root_path, "log/appsignal.log")
844
+ expect(log_file_path).to eq File.join(config.root_path, "log/appsignal.log")
818
845
  end
819
846
 
820
847
  it "prints no warning" do
821
- subject
848
+ log_file_path
822
849
  expect(output).to be_empty
823
850
  end
824
851
  end
@@ -878,7 +905,7 @@ describe Appsignal::Config do
878
905
  end
879
906
 
880
907
  it "returns real path of log path" do
881
- expect(subject).to eq(File.join(real_path, "appsignal.log"))
908
+ expect(log_file_path).to eq(File.join(real_path, "appsignal.log"))
882
909
  end
883
910
  end
884
911
  end
@@ -0,0 +1,89 @@
1
+ describe Appsignal::Heartbeat do
2
+ let(:config) { project_fixture_config }
3
+ let(:heartbeat) { described_class.new(:name => "heartbeat-name") }
4
+ let(:transmitter) { Appsignal::Transmitter.new("http://heartbeats/", config) }
5
+
6
+ before(:each) do
7
+ allow(Appsignal).to receive(:active?).and_return(true)
8
+ config.logger = Logger.new(StringIO.new)
9
+ allow(Appsignal::Heartbeat).to receive(:transmitter).and_return(transmitter)
10
+ end
11
+
12
+ describe "when Appsignal is not active" do
13
+ it "should not transmit any events" do
14
+ allow(Appsignal).to receive(:active?).and_return(false)
15
+ expect(transmitter).not_to receive(:transmit)
16
+
17
+ heartbeat.start
18
+ heartbeat.finish
19
+ end
20
+ end
21
+
22
+ describe "#start" do
23
+ it "should send a heartbeat start" do
24
+ expect(transmitter).to receive(:transmit).with(hash_including(
25
+ :name => "heartbeat-name",
26
+ :kind => "start"
27
+ )).and_return(nil)
28
+
29
+ heartbeat.start
30
+ end
31
+ end
32
+
33
+ describe "#finish" do
34
+ it "should send a heartbeat finish" do
35
+ expect(transmitter).to receive(:transmit).with(hash_including(
36
+ :name => "heartbeat-name",
37
+ :kind => "finish"
38
+ )).and_return(nil)
39
+
40
+ heartbeat.finish
41
+ end
42
+ end
43
+
44
+ describe ".heartbeat" do
45
+ describe "when a block is given" do
46
+ it "should send a heartbeat start and finish and return the block output" do
47
+ expect(transmitter).to receive(:transmit).with(hash_including(
48
+ :kind => "start",
49
+ :name => "heartbeat-with-block"
50
+ )).and_return(nil)
51
+
52
+ expect(transmitter).to receive(:transmit).with(hash_including(
53
+ :kind => "finish",
54
+ :name => "heartbeat-with-block"
55
+ )).and_return(nil)
56
+
57
+ output = Appsignal.heartbeat("heartbeat-with-block") { "output" }
58
+ expect(output).to eq("output")
59
+ end
60
+
61
+ it "should not send a heartbeat finish event when an error is raised" do
62
+ expect(transmitter).to receive(:transmit).with(hash_including(
63
+ :kind => "start",
64
+ :name => "heartbeat-with-block"
65
+ )).and_return(nil)
66
+
67
+ expect(transmitter).not_to receive(:transmit).with(hash_including(
68
+ :kind => "finish",
69
+ :name => "heartbeat-with-block"
70
+ ))
71
+
72
+ expect do
73
+ Appsignal.heartbeat("heartbeat-with-block") { raise "error" }
74
+ end.to raise_error(RuntimeError, "error")
75
+ end
76
+ end
77
+
78
+ describe "when no block is given" do
79
+ it "should only send a heartbeat finish event" do
80
+ expect(transmitter).to receive(:transmit).with(hash_including(
81
+ :kind => "finish",
82
+ :name => "heartbeat-without-block"
83
+ )).and_return(nil)
84
+
85
+ Appsignal.heartbeat("heartbeat-without-block")
86
+ end
87
+ end
88
+ end
89
+ end
@@ -93,7 +93,7 @@ describe Appsignal::Hooks::GvlHook do
93
93
  it "is added to minutely probes" do
94
94
  Appsignal::Hooks.load_hooks
95
95
 
96
- expect(Appsignal::Minutely.probes[:gvl]).to be Appsignal::Probes::GvlProbe
96
+ expect(Appsignal::Probes.probes[:gvl]).to be Appsignal::Probes::GvlProbe
97
97
  end
98
98
  end
99
99
  end
@@ -16,7 +16,7 @@ describe Appsignal::Hooks::MriHook do
16
16
  end
17
17
 
18
18
  it "should be added to minutely probes" do
19
- expect(Appsignal::Minutely.probes[:mri]).to be Appsignal::Probes::MriProbe
19
+ expect(Appsignal::Probes.probes[:mri]).to be Appsignal::Probes::MriProbe
20
20
  end
21
21
  end
22
22
  end
@@ -27,7 +27,7 @@ describe Appsignal::Hooks::PumaHook do
27
27
  end
28
28
 
29
29
  describe "installation" do
30
- before { Appsignal::Minutely.probes.clear }
30
+ before { Appsignal::Probes.probes.clear }
31
31
 
32
32
  context "when not clustered mode" do
33
33
  it "does not add AppSignal stop behavior Puma::Cluster" do