appsignal 2.8.4-java → 2.9.0-java

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop_todo.yml +7 -16
  4. data/.travis.yml +4 -1
  5. data/CHANGELOG.md +16 -0
  6. data/README.md +23 -0
  7. data/Rakefile +10 -7
  8. data/appsignal.gemspec +3 -0
  9. data/build_matrix.yml +5 -1
  10. data/ext/Rakefile +23 -16
  11. data/ext/agent.yml +37 -37
  12. data/ext/base.rb +86 -24
  13. data/ext/extconf.rb +33 -26
  14. data/gemfiles/rails-6.0.gemfile +5 -0
  15. data/lib/appsignal.rb +14 -489
  16. data/lib/appsignal/cli/diagnose.rb +84 -4
  17. data/lib/appsignal/cli/diagnose/paths.rb +0 -5
  18. data/lib/appsignal/cli/diagnose/utils.rb +17 -0
  19. data/lib/appsignal/cli/helpers.rb +6 -0
  20. data/lib/appsignal/cli/install.rb +13 -7
  21. data/lib/appsignal/config.rb +1 -2
  22. data/lib/appsignal/event_formatter.rb +4 -5
  23. data/lib/appsignal/event_formatter/moped/query_formatter.rb +60 -59
  24. data/lib/appsignal/extension.rb +2 -2
  25. data/lib/appsignal/helpers/instrumentation.rb +485 -0
  26. data/lib/appsignal/helpers/metrics.rb +55 -0
  27. data/lib/appsignal/hooks.rb +9 -8
  28. data/lib/appsignal/hooks/puma.rb +65 -9
  29. data/lib/appsignal/hooks/sidekiq.rb +90 -0
  30. data/lib/appsignal/integrations/mongo_ruby_driver.rb +7 -0
  31. data/lib/appsignal/integrations/railtie.rb +2 -1
  32. data/lib/appsignal/marker.rb +2 -3
  33. data/lib/appsignal/minutely.rb +164 -14
  34. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -1
  35. data/lib/appsignal/system.rb +16 -18
  36. data/lib/appsignal/utils/rails_helper.rb +16 -0
  37. data/lib/appsignal/version.rb +1 -1
  38. data/spec/lib/appsignal/cli/diagnose_spec.rb +129 -22
  39. data/spec/lib/appsignal/cli/install_spec.rb +6 -1
  40. data/spec/lib/appsignal/config_spec.rb +3 -3
  41. data/spec/lib/appsignal/event_formatter/moped/query_formatter_spec.rb +6 -0
  42. data/spec/lib/appsignal/event_formatter_spec.rb +168 -69
  43. data/spec/lib/appsignal/hooks/puma_spec.rb +129 -0
  44. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +147 -0
  45. data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +24 -1
  46. data/spec/lib/appsignal/minutely_spec.rb +251 -21
  47. data/spec/lib/appsignal/system_spec.rb +0 -35
  48. data/spec/lib/appsignal/utils/hash_sanitizer_spec.rb +39 -31
  49. data/spec/lib/appsignal/utils/json_spec.rb +7 -3
  50. data/spec/lib/appsignal_spec.rb +27 -2
  51. data/spec/spec_helper.rb +13 -0
  52. data/spec/support/helpers/log_helpers.rb +6 -0
  53. data/spec/support/project_fixture/config/appsignal.yml +1 -0
  54. data/spec/support/stubs/sidekiq/api.rb +4 -0
  55. metadata +8 -2
@@ -2,6 +2,9 @@ describe Appsignal::Hooks::PumaHook do
2
2
  context "with puma" do
3
3
  before(:context) do
4
4
  class Puma
5
+ def self.stats
6
+ end
7
+
5
8
  def self.cli_config
6
9
  @cli_config ||= CliConfig.new
7
10
  end
@@ -41,6 +44,11 @@ describe Appsignal::Hooks::PumaHook do
41
44
 
42
45
  cluster.stop_workers
43
46
  end
47
+
48
+ it "adds the Puma minutely probe" do
49
+ probe = Appsignal::Minutely.probes[:puma]
50
+ expect(probe).to eql(Appsignal::Hooks::PumaProbe)
51
+ end
44
52
  end
45
53
 
46
54
  context "with nil hooks" do
@@ -78,3 +86,124 @@ describe Appsignal::Hooks::PumaHook do
78
86
  end
79
87
  end
80
88
  end
89
+
90
+ describe Appsignal::Hooks::PumaProbe do
91
+ before(:context) do
92
+ Appsignal.config = project_fixture_config
93
+ end
94
+ after(:context) do
95
+ Appsignal.config = nil
96
+ end
97
+
98
+ let(:probe) { Appsignal::Hooks::PumaProbe.new }
99
+
100
+ describe "hostname" do
101
+ it "returns the socket hostname" do
102
+ expect(probe.send(:hostname)).to eql(Socket.gethostname)
103
+ end
104
+
105
+ context "with overridden hostname" do
106
+ around do |sample|
107
+ Appsignal.config[:hostname] = "frontend1"
108
+ sample.run
109
+ Appsignal.config[:hostname] = nil
110
+ end
111
+ it "returns the configured host" do
112
+ expect(probe.send(:hostname)).to eql("frontend1")
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "#call" do
118
+ let(:expected_default_tags) { { :hostname => Socket.gethostname } }
119
+
120
+ context "with multiple worker stats" do
121
+ before(:context) do
122
+ class Puma
123
+ def self.stats
124
+ {
125
+ "workers" => 2,
126
+ "booted_workers" => 2,
127
+ "old_workers" => 0,
128
+ "worker_status" => [
129
+ {
130
+ "last_status" => {
131
+ "backlog" => 0,
132
+ "running" => 5,
133
+ "pool_capacity" => 5,
134
+ "max_threads" => 5
135
+ }
136
+ },
137
+ {
138
+ "last_status" => {
139
+ "backlog" => 0,
140
+ "running" => 5,
141
+ "pool_capacity" => 5,
142
+ "max_threads" => 5
143
+ }
144
+ }
145
+ ]
146
+ }.to_json
147
+ end
148
+ end
149
+ end
150
+ after(:context) { Object.send(:remove_const, :Puma) }
151
+
152
+ it "calls `puma_gauge` with the (summed) worker metrics" do
153
+ expect_gauge(:workers, 2, :type => :count)
154
+ expect_gauge(:workers, 2, :type => :booted)
155
+ expect_gauge(:workers, 0, :type => :old)
156
+
157
+ expect_gauge(:connection_backlog, 0)
158
+ expect_gauge(:pool_capacity, 10)
159
+ expect_gauge(:threads, 10, :type => :running)
160
+ expect_gauge(:threads, 10, :type => :max)
161
+ end
162
+ end
163
+
164
+ context "with single worker stats" do
165
+ before(:context) do
166
+ class Puma
167
+ def self.stats
168
+ {
169
+ "backlog" => 0,
170
+ "running" => 5,
171
+ "pool_capacity" => 5,
172
+ "max_threads" => 5
173
+ }.to_json
174
+ end
175
+ end
176
+ end
177
+ after(:context) { Object.send(:remove_const, :Puma) }
178
+
179
+ it "calls `puma_gauge` with the (summed) worker metrics" do
180
+ expect_gauge(:connection_backlog, 0)
181
+ expect_gauge(:pool_capacity, 5)
182
+ expect_gauge(:threads, 5, :type => :running)
183
+ expect_gauge(:threads, 5, :type => :max)
184
+ end
185
+ end
186
+
187
+ context "without stats" do
188
+ before(:context) do
189
+ class Puma
190
+ def self.stats
191
+ end
192
+ end
193
+ end
194
+ after(:context) { Object.send(:remove_const, :Puma) }
195
+
196
+ it "does not track metrics" do
197
+ expect(probe).to_not receive(:puma_gauge)
198
+ end
199
+ end
200
+
201
+ after { probe.call }
202
+
203
+ def expect_gauge(key, value, tags = {})
204
+ expect(Appsignal).to receive(:set_gauge)
205
+ .with("puma_#{key}", value, expected_default_tags.merge(tags))
206
+ .and_call_original
207
+ end
208
+ end
209
+ end
@@ -431,6 +431,11 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
431
431
  let(:error) { ExampleException }
432
432
 
433
433
  it "creates a transaction and adds the error" do
434
+ expect(Appsignal).to receive(:increment_counter)
435
+ .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :failed)
436
+ expect(Appsignal).to receive(:increment_counter)
437
+ .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :processed)
438
+
434
439
  expect do
435
440
  perform_job { raise error, "uh oh" }
436
441
  end.to raise_error(error)
@@ -466,6 +471,9 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
466
471
 
467
472
  context "without an error" do
468
473
  it "creates a transaction with events" do
474
+ expect(Appsignal).to receive(:increment_counter)
475
+ .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :processed)
476
+
469
477
  perform_job
470
478
 
471
479
  transaction_hash = transaction.to_h
@@ -541,6 +549,7 @@ describe Appsignal::Hooks::SidekiqHook do
541
549
 
542
550
  describe "#install" do
543
551
  before do
552
+ Appsignal.config = project_fixture_config
544
553
  class Sidekiq
545
554
  def self.middlewares
546
555
  @middlewares ||= Set.new
@@ -564,3 +573,141 @@ describe Appsignal::Hooks::SidekiqHook do
564
573
  end
565
574
  end
566
575
  end
576
+
577
+ describe Appsignal::Hooks::SidekiqProbe do
578
+ describe "#call" do
579
+ let(:probe) { described_class.new }
580
+ let(:redis_hostname) { "localhost" }
581
+ let(:expected_default_tags) { { :hostname => "localhost" } }
582
+ before do
583
+ Appsignal.config = project_fixture_config
584
+ class Sidekiq
585
+ def self.redis_info
586
+ {
587
+ "connected_clients" => 2,
588
+ "used_memory" => 1024,
589
+ "used_memory_rss" => 512
590
+ }
591
+ end
592
+
593
+ def self.redis
594
+ yield Client.new
595
+ end
596
+
597
+ class Client
598
+ def connection
599
+ { :host => "localhost" }
600
+ end
601
+ end
602
+
603
+ class Stats
604
+ class << self
605
+ attr_reader :calls
606
+
607
+ def count_call
608
+ @calls ||= -1
609
+ @calls += 1
610
+ end
611
+ end
612
+
613
+ def workers_size
614
+ # First method called, so count it towards a call
615
+ self.class.count_call
616
+ 24
617
+ end
618
+
619
+ def processes_size
620
+ 25
621
+ end
622
+
623
+ # Return two different values for two separate calls.
624
+ # This allows us to test the delta of the value send as a gauge.
625
+ def processed
626
+ [10, 15][self.class.calls]
627
+ end
628
+
629
+ # Return two different values for two separate calls.
630
+ # This allows us to test the delta of the value send as a gauge.
631
+ def failed
632
+ [10, 13][self.class.calls]
633
+ end
634
+
635
+ def retry_size
636
+ 12
637
+ end
638
+
639
+ # Return two different values for two separate calls.
640
+ # This allows us to test the delta of the value send as a gauge.
641
+ def dead_size
642
+ [10, 12][self.class.calls]
643
+ end
644
+
645
+ def scheduled_size
646
+ 14
647
+ end
648
+
649
+ def enqueued
650
+ 15
651
+ end
652
+ end
653
+
654
+ class Queue
655
+ Queue = Struct.new(:name, :size, :latency)
656
+
657
+ def self.all
658
+ [
659
+ Queue.new("default", 10, 12),
660
+ Queue.new("critical", 1, 2)
661
+ ]
662
+ end
663
+ end
664
+ end
665
+ end
666
+ after { Object.send(:remove_const, "Sidekiq") }
667
+
668
+ it "loads Sidekiq::API" do
669
+ expect(defined?(Sidekiq::API)).to be_falsy
670
+ probe
671
+ expect(defined?(Sidekiq::API)).to be_truthy
672
+ end
673
+
674
+ it "collects custom metrics" do
675
+ expect_gauge("worker_count", 24).twice
676
+ expect_gauge("process_count", 25).twice
677
+ expect_gauge("connection_count", 2).twice
678
+ expect_gauge("memory_usage", 1024).twice
679
+ expect_gauge("memory_usage_rss", 512).twice
680
+ expect_gauge("job_count", 5, :status => :processed) # Gauge delta
681
+ expect_gauge("job_count", 3, :status => :failed) # Gauge delta
682
+ expect_gauge("job_count", 12, :status => :retry_queue).twice
683
+ expect_gauge("job_count", 2, :status => :died) # Gauge delta
684
+ expect_gauge("job_count", 14, :status => :scheduled).twice
685
+ expect_gauge("job_count", 15, :status => :enqueued).twice
686
+ expect_gauge("queue_length", 10, :queue => "default").twice
687
+ expect_gauge("queue_latency", 12, :queue => "default").twice
688
+ expect_gauge("queue_length", 1, :queue => "critical").twice
689
+ expect_gauge("queue_latency", 2, :queue => "critical").twice
690
+ # Call probe twice so we can calculate the delta for some gauge values
691
+ probe.call
692
+ probe.call
693
+ end
694
+
695
+ context "when hostname is configured for probe" do
696
+ let(:redis_hostname) { "my_redis_server" }
697
+ let(:probe) { described_class.new(:hostname => redis_hostname) }
698
+
699
+ it "uses the redis hostname for the hostname tag" do
700
+ allow(Appsignal).to receive(:set_gauge).and_call_original
701
+ probe.call
702
+ expect(Appsignal).to have_received(:set_gauge)
703
+ .with(anything, anything, :hostname => redis_hostname).at_least(:once)
704
+ end
705
+ end
706
+
707
+ def expect_gauge(key, value, tags = {})
708
+ expect(Appsignal).to receive(:set_gauge)
709
+ .with("sidekiq_#{key}", value, expected_default_tags.merge(tags))
710
+ .and_call_original
711
+ end
712
+ end
713
+ end
@@ -62,7 +62,8 @@ describe Appsignal::Hooks::MongoMonitorSubscriber do
62
62
  double(
63
63
  :request_id => 2,
64
64
  :command_name => :find,
65
- :database_name => "test"
65
+ :database_name => "test",
66
+ :duration => 0.9919
66
67
  )
67
68
  end
68
69
 
@@ -71,6 +72,16 @@ describe Appsignal::Hooks::MongoMonitorSubscriber do
71
72
  store[2] = command
72
73
  end
73
74
 
75
+ it "should emit a measurement" do
76
+ expect(Appsignal).to receive(:add_distribution_value).with(
77
+ "mongodb_query_duration",
78
+ 0.9919,
79
+ :database => "test"
80
+ ).and_call_original
81
+
82
+ subscriber.finish("SUCCEEDED", event)
83
+ end
84
+
74
85
  it "should get the query from the store" do
75
86
  expect(transaction).to receive(:store).with("mongo_driver").and_return(command)
76
87
 
@@ -106,6 +117,12 @@ describe Appsignal::Hooks::MongoMonitorSubscriber do
106
117
 
107
118
  subscriber.finish("SUCCEEDED", double)
108
119
  end
120
+
121
+ it "should not attempt to send duration metrics" do
122
+ expect(Appsignal).to_not receive(:add_distribution_value)
123
+
124
+ subscriber.finish("SUCCEEDED", double)
125
+ end
109
126
  end
110
127
 
111
128
  context "when appsignal is paused" do
@@ -123,5 +140,11 @@ describe Appsignal::Hooks::MongoMonitorSubscriber do
123
140
 
124
141
  subscriber.finish("SUCCEEDED", double)
125
142
  end
143
+
144
+ it "should not attempt to send duration metrics" do
145
+ expect(Appsignal).to_not receive(:add_distribution_value)
146
+
147
+ subscriber.finish("SUCCEEDED", double)
148
+ end
126
149
  end
127
150
  end
@@ -1,46 +1,276 @@
1
1
  describe Appsignal::Minutely do
2
- before do
3
- Appsignal::Minutely.probes.clear
4
- end
2
+ before { Appsignal::Minutely.probes.clear }
5
3
 
6
- it "should have a list of probes" do
7
- expect(Appsignal::Minutely.probes).to be_instance_of(Array)
4
+ it "returns a ProbeCollection" do
5
+ expect(Appsignal::Minutely.probes)
6
+ .to be_instance_of(Appsignal::Minutely::ProbeCollection)
8
7
  end
9
8
 
10
9
  describe ".start" do
11
- it "should call the probes periodically" do
12
- probe = double
13
- expect(probe).to receive(:call).at_least(:twice)
14
- Appsignal::Minutely.probes << probe
15
- allow(Appsignal::Minutely).to receive(:wait_time).and_return(0.1)
10
+ class Probe
11
+ attr_reader :calls
12
+
13
+ def initialize
14
+ @calls = 0
15
+ end
16
+
17
+ def call
18
+ @calls += 1
19
+ end
20
+ end
21
+
22
+ class BrokenProbe < Probe
23
+ def call
24
+ super
25
+ raise "oh no!"
26
+ end
27
+ end
28
+
29
+ let(:log_stream) { StringIO.new }
30
+ let(:log) do
31
+ log_stream.rewind
32
+ log_stream.read
33
+ end
34
+ before do
35
+ Appsignal.logger = Logger.new(log_stream)
36
+ # Speed up test time
37
+ allow(Appsignal::Minutely).to receive(:wait_time).and_return(0.001)
38
+ end
39
+
40
+ context "with an instance of a class" do
41
+ it "calls the probe every <wait_time>" do
42
+ probe = Probe.new
43
+ Appsignal::Minutely.probes.register :my_probe, probe
44
+ Appsignal::Minutely.start
45
+
46
+ wait_for("enough probe calls") { probe.calls >= 2 }
47
+ expect(log).to include("Gathering minutely metrics with 1 probe")
48
+ expect(log).to include("Gathering minutely metrics with 'my_probe' probe")
49
+ end
50
+ end
51
+
52
+ context "with probe class" do
53
+ it "creates an instance of the class and call that every <wait time>" do
54
+ probe = Probe
55
+ probe_instance = Probe.new
56
+ expect(probe).to receive(:new).and_return(probe_instance)
57
+ Appsignal::Minutely.probes.register :my_probe, probe
58
+ Appsignal::Minutely.start
59
+
60
+ wait_for("enough probe calls") { probe_instance.calls >= 2 }
61
+ expect(log).to include("Gathering minutely metrics with 1 probe")
62
+ expect(log).to include("Gathering minutely metrics with 'my_probe' probe")
63
+ end
64
+ end
65
+
66
+ context "with a lambda" do
67
+ it "calls the lambda every <wait time>" do
68
+ calls = 0
69
+ probe = -> { calls += 1 }
70
+ Appsignal::Minutely.probes.register :my_probe, probe
71
+ Appsignal::Minutely.start
72
+
73
+ wait_for("enough probe calls") { calls >= 2 }
74
+ expect(log).to include("Gathering minutely metrics with 1 probe")
75
+ expect(log).to include("Gathering minutely metrics with 'my_probe' probe")
76
+ end
77
+ end
78
+
79
+ context "with a broken probe" do
80
+ it "logs the error and continues calling the probes every <wait_time>" do
81
+ probe = Probe.new
82
+ broken_probe = BrokenProbe.new
83
+ Appsignal::Minutely.probes.register :my_probe, probe
84
+ Appsignal::Minutely.probes.register :broken_probe, broken_probe
85
+ Appsignal::Minutely.start
86
+
87
+ wait_for("enough probe calls") { probe.calls >= 2 }
88
+ wait_for("enough broken_probe calls") { broken_probe.calls >= 2 }
89
+
90
+ expect(log).to include("Gathering minutely metrics with 2 probes")
91
+ expect(log).to include("Gathering minutely metrics with 'my_probe' probe")
92
+ expect(log).to include("Gathering minutely metrics with 'broken_probe' probe")
93
+ expect(log).to include("Error in minutely probe 'broken_probe': oh no!")
94
+ end
95
+ end
96
+
97
+ it "ensures only one minutely probes thread is active at a time" do
98
+ alive_thread_counter = proc { Thread.list.reject { |t| t.status == "dead" }.length }
99
+ probe = Probe.new
100
+ Appsignal::Minutely.probes.register :my_probe, probe
101
+ expect do
102
+ Appsignal::Minutely.start
103
+ end.to change { alive_thread_counter.call }.by(1)
104
+
105
+ wait_for("enough probe calls") { probe.calls >= 2 }
106
+ expect(log).to include("Gathering minutely metrics with 1 probe")
107
+ expect(log).to include("Gathering minutely metrics with 'my_probe' probe")
108
+ expect do
109
+ # Fetch old thread
110
+ thread = Appsignal::Minutely.class_variable_get(:@@thread)
111
+ Appsignal::Minutely.start
112
+ thread && thread.join # Wait for old thread to exit
113
+ end.to_not(change { alive_thread_counter.call })
114
+ end
16
115
 
116
+ # Wait for a condition to be met
117
+ #
118
+ # @example
119
+ # # Perform threaded operation
120
+ # wait_for("enough probe calls") { probe.calls >= 2 }
121
+ # # Assert on result
122
+ #
123
+ # @param name [String] The name of the condition to check. Used in the
124
+ # error when it fails.
125
+ # @yield Assertion to check.
126
+ # @yieldreturn [Boolean] True/False value that indicates if the condition
127
+ # is met.
128
+ # @raise [StandardError] Raises error if the condition is not met after 5
129
+ # seconds, 5_000 tries.
130
+ def wait_for(name)
131
+ max_wait = 5_000
132
+ i = 0
133
+ while i <= max_wait
134
+ break if yield
135
+ i += 1
136
+ sleep 0.001
137
+ end
138
+
139
+ return unless i == max_wait
140
+ raise "Waited 5 seconds for #{name} condition, but was not met."
141
+ end
142
+ end
143
+
144
+ describe ".stop" do
145
+ it "stops the minutely thread" do
17
146
  Appsignal::Minutely.start
147
+ thread = Appsignal::Minutely.class_variable_get(:@@thread)
148
+ expect(%w[sleep run]).to include(thread.status)
149
+ Appsignal::Minutely.stop
150
+ thread.join
151
+ expect(thread.status).to eql(false)
152
+ end
18
153
 
19
- sleep 0.5
154
+ it "clears the probe instances array" do
155
+ Appsignal::Minutely.probes.register :my_probe, -> {}
156
+ Appsignal::Minutely.start
157
+ thread = Appsignal::Minutely.class_variable_get(:@@thread)
158
+ expect(Appsignal::Minutely.send(:probe_instances)).to_not be_empty
159
+ Appsignal::Minutely.stop
160
+ thread.join
161
+ expect(Appsignal::Minutely.send(:probe_instances)).to be_empty
20
162
  end
21
163
  end
22
164
 
23
165
  describe ".wait_time" do
24
- it "should get the time to the next minute" do
25
- allow_any_instance_of(Time).to receive(:sec).and_return(30)
26
- expect(Appsignal::Minutely.wait_time).to eq 30
166
+ it "gets the time to the next minute" do
167
+ allow_any_instance_of(Time).to receive(:sec).and_return(20)
168
+ expect(Appsignal::Minutely.wait_time).to eq 40
169
+ end
170
+ end
171
+
172
+ describe ".register_garbage_collection_probe" do
173
+ it "adds the GC probe to the probes list" do
174
+ expect(Appsignal::Minutely.probes.count).to eql(0)
175
+
176
+ Appsignal::Minutely.register_garbage_collection_probe
177
+
178
+ expect(Appsignal::Minutely.probes.count).to eql(1)
179
+ expect(Appsignal::Minutely.probes[:garbage_collection])
180
+ .to be_instance_of(Appsignal::Minutely::GCProbe)
27
181
  end
28
182
  end
29
183
 
30
- describe ".add_gc_probe" do
31
- it "should add the gc probe to the list" do
32
- expect(Appsignal::Minutely.probes).to be_empty
184
+ describe Appsignal::Minutely::ProbeCollection do
185
+ let(:collection) { described_class.new }
33
186
 
34
- Appsignal::Minutely.add_gc_probe
187
+ describe "#count" do
188
+ it "returns how many probes are registered" do
189
+ expect(collection.count).to eql(0)
190
+ collection.register :my_probe_1, -> {}
191
+ expect(collection.count).to eql(1)
192
+ collection.register :my_probe_2, -> {}
193
+ expect(collection.count).to eql(2)
194
+ end
195
+ end
35
196
 
36
- expect(Appsignal::Minutely.probes.size).to eq(1)
37
- expect(Appsignal::Minutely.probes[0]).to be_instance_of(Appsignal::Minutely::GCProbe)
197
+ describe "#clear" do
198
+ it "clears the list of probes" do
199
+ collection.register :my_probe_1, -> {}
200
+ collection.register :my_probe_2, -> {}
201
+ expect(collection.count).to eql(2)
202
+ collection.clear
203
+ expect(collection.count).to eql(0)
204
+ end
205
+ end
206
+
207
+ describe "#[]" do
208
+ it "returns the probe for that name" do
209
+ probe = -> {}
210
+ collection.register :my_probe, probe
211
+ expect(collection[:my_probe]).to eql(probe)
212
+ end
213
+ end
214
+
215
+ describe "#<<" do
216
+ let(:out_stream) { std_stream }
217
+ let(:output) { out_stream.read }
218
+ let(:log_stream) { std_stream }
219
+ let(:log) { log_contents(log_stream) }
220
+ before { Appsignal.logger = test_logger(log_stream) }
221
+
222
+ it "adds the probe, but prints a deprecation warning" do
223
+ probe = -> {}
224
+ capture_stdout(out_stream) { collection << probe }
225
+ deprecation_message = "Deprecated " \
226
+ "`Appsignal::Minute.probes <<` call. Please use " \
227
+ "`Appsignal::Minutely.probes.register` instead."
228
+ expect(output).to include "appsignal WARNING: #{deprecation_message}"
229
+ expect(log).to contains_log :warn, deprecation_message
230
+ expect(collection[probe.object_id]).to eql(probe)
231
+ end
232
+ end
233
+
234
+ describe "#register" do
235
+ let(:log_stream) { std_stream }
236
+ let(:log) { log_contents(log_stream) }
237
+ before { Appsignal.logger = test_logger(log_stream) }
238
+
239
+ it "adds the by key probe" do
240
+ probe = -> {}
241
+ collection.register :my_probe, probe
242
+ expect(collection[:my_probe]).to eql(probe)
243
+ end
244
+
245
+ context "when a probe is already registered with the same key" do
246
+ it "logs a debug message" do
247
+ probe = -> {}
248
+ collection.register :my_probe, probe
249
+ collection.register :my_probe, probe
250
+ expect(log).to contains_log :debug, "A probe with the name " \
251
+ "`my_probe` is already registered. Overwriting the entry " \
252
+ "with the new probe."
253
+ expect(collection[:my_probe]).to eql(probe)
254
+ end
255
+ end
256
+ end
257
+
258
+ describe "#each" do
259
+ it "loops over the registered probes" do
260
+ probe = -> {}
261
+ collection.register :my_probe, probe
262
+ list = []
263
+ collection.each do |name, p|
264
+ list << [name, p]
265
+ end
266
+ expect(list).to eql([[:my_probe, probe]])
267
+ end
38
268
  end
39
269
  end
40
270
 
41
271
  describe Appsignal::Minutely::GCProbe do
42
272
  describe "#call" do
43
- it "should collect GC metrics" do
273
+ it "collects GC metrics" do
44
274
  expect(Appsignal).to receive(:set_process_gauge).at_least(8).times
45
275
 
46
276
  Appsignal::Minutely::GCProbe.new.call