appsignal 2.8.4-java → 2.9.0-java

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