appsignal 3.6.5 → 3.7.1

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