appsignal 2.8.4 → 2.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.rubocop_todo.yml +7 -16
- data/.travis.yml +4 -1
- data/CHANGELOG.md +16 -0
- data/README.md +23 -0
- data/Rakefile +10 -7
- data/appsignal.gemspec +3 -0
- data/build_matrix.yml +5 -1
- data/ext/Rakefile +23 -16
- data/ext/agent.yml +37 -37
- data/ext/base.rb +86 -24
- data/ext/extconf.rb +33 -26
- data/gemfiles/rails-6.0.gemfile +5 -0
- data/lib/appsignal.rb +14 -489
- data/lib/appsignal/cli/diagnose.rb +84 -4
- data/lib/appsignal/cli/diagnose/paths.rb +0 -5
- data/lib/appsignal/cli/diagnose/utils.rb +17 -0
- data/lib/appsignal/cli/helpers.rb +6 -0
- data/lib/appsignal/cli/install.rb +13 -7
- data/lib/appsignal/config.rb +1 -2
- data/lib/appsignal/event_formatter.rb +4 -5
- data/lib/appsignal/event_formatter/moped/query_formatter.rb +60 -59
- data/lib/appsignal/extension.rb +2 -2
- data/lib/appsignal/helpers/instrumentation.rb +485 -0
- data/lib/appsignal/helpers/metrics.rb +55 -0
- data/lib/appsignal/hooks.rb +9 -8
- data/lib/appsignal/hooks/puma.rb +65 -9
- data/lib/appsignal/hooks/sidekiq.rb +90 -0
- data/lib/appsignal/integrations/mongo_ruby_driver.rb +7 -0
- data/lib/appsignal/integrations/railtie.rb +2 -1
- data/lib/appsignal/marker.rb +2 -3
- data/lib/appsignal/minutely.rb +164 -14
- data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -1
- data/lib/appsignal/system.rb +16 -18
- data/lib/appsignal/utils/rails_helper.rb +16 -0
- data/lib/appsignal/version.rb +1 -1
- data/spec/lib/appsignal/cli/diagnose_spec.rb +129 -22
- data/spec/lib/appsignal/cli/install_spec.rb +6 -1
- data/spec/lib/appsignal/config_spec.rb +3 -3
- data/spec/lib/appsignal/event_formatter/moped/query_formatter_spec.rb +6 -0
- data/spec/lib/appsignal/event_formatter_spec.rb +168 -69
- data/spec/lib/appsignal/hooks/puma_spec.rb +129 -0
- data/spec/lib/appsignal/hooks/sidekiq_spec.rb +147 -0
- data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +24 -1
- data/spec/lib/appsignal/minutely_spec.rb +251 -21
- data/spec/lib/appsignal/system_spec.rb +0 -35
- data/spec/lib/appsignal/utils/hash_sanitizer_spec.rb +39 -31
- data/spec/lib/appsignal/utils/json_spec.rb +7 -3
- data/spec/lib/appsignal_spec.rb +27 -2
- data/spec/spec_helper.rb +13 -0
- data/spec/support/helpers/log_helpers.rb +6 -0
- data/spec/support/project_fixture/config/appsignal.yml +1 -0
- data/spec/support/stubs/sidekiq/api.rb +4 -0
- 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
|
3
|
-
Appsignal::Minutely.probes.clear
|
4
|
-
end
|
2
|
+
before { Appsignal::Minutely.probes.clear }
|
5
3
|
|
6
|
-
it "
|
7
|
-
expect(Appsignal::Minutely.probes)
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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 "
|
25
|
-
allow_any_instance_of(Time).to receive(:sec).and_return(
|
26
|
-
expect(Appsignal::Minutely.wait_time).to eq
|
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
|
31
|
-
|
32
|
-
expect(Appsignal::Minutely.probes).to be_empty
|
184
|
+
describe Appsignal::Minutely::ProbeCollection do
|
185
|
+
let(:collection) { described_class.new }
|
33
186
|
|
34
|
-
|
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
|
-
|
37
|
-
|
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 "
|
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
|