appsignal 2.8.4.beta.1 → 2.9.18.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- data/.github/ISSUE_TEMPLATE/chore.md +14 -0
- data/.gitignore +2 -3
- data/.rubocop.yml +3 -0
- data/.rubocop_todo.yml +7 -16
- data/.travis.yml +28 -27
- data/CHANGELOG.md +657 -533
- data/README.md +31 -3
- data/Rakefile +128 -129
- data/SUPPORT.md +16 -0
- data/appsignal.gemspec +17 -4
- data/build_matrix.yml +21 -9
- data/ext/Rakefile +23 -17
- data/ext/agent.yml +40 -37
- data/ext/base.rb +116 -31
- data/ext/extconf.rb +34 -28
- data/gemfiles/capistrano2.gemfile +5 -0
- data/gemfiles/capistrano3.gemfile +5 -0
- data/gemfiles/grape.gemfile +5 -0
- data/gemfiles/no_dependencies.gemfile +5 -0
- data/gemfiles/padrino.gemfile +5 -0
- data/gemfiles/que.gemfile +5 -0
- data/gemfiles/que_beta.gemfile +10 -0
- data/gemfiles/rails-3.2.gemfile +5 -0
- data/gemfiles/rails-4.0.gemfile +5 -0
- data/gemfiles/rails-4.1.gemfile +5 -0
- data/gemfiles/rails-4.2.gemfile +5 -0
- data/gemfiles/rails-6.0.gemfile +5 -0
- data/gemfiles/resque.gemfile +5 -0
- data/lib/appsignal.rb +14 -492
- data/lib/appsignal/cli/demo.rb +5 -2
- data/lib/appsignal/cli/diagnose.rb +84 -4
- data/lib/appsignal/cli/diagnose/paths.rb +0 -5
- data/lib/appsignal/cli/diagnose/utils.rb +19 -0
- data/lib/appsignal/cli/helpers.rb +6 -0
- data/lib/appsignal/cli/install.rb +45 -15
- data/lib/appsignal/cli/notify_of_deploy.rb +10 -0
- data/lib/appsignal/config.rb +1 -2
- data/lib/appsignal/event_formatter.rb +4 -5
- data/lib/appsignal/event_formatter/action_view/render_formatter.rb +10 -8
- data/lib/appsignal/event_formatter/moped/query_formatter.rb +60 -59
- data/lib/appsignal/extension.rb +2 -2
- data/lib/appsignal/helpers/instrumentation.rb +494 -0
- data/lib/appsignal/helpers/metrics.rb +54 -0
- data/lib/appsignal/hooks.rb +11 -8
- data/lib/appsignal/hooks/active_support_notifications.rb +2 -5
- data/lib/appsignal/hooks/puma.rb +74 -11
- data/lib/appsignal/hooks/sequel.rb +1 -1
- data/lib/appsignal/hooks/sidekiq.rb +115 -0
- data/lib/appsignal/integrations/mongo_ruby_driver.rb +7 -0
- data/lib/appsignal/integrations/que.rb +9 -8
- data/lib/appsignal/integrations/railtie.rb +2 -1
- data/lib/appsignal/marker.rb +2 -3
- data/lib/appsignal/minutely.rb +188 -19
- data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -1
- data/lib/appsignal/system.rb +16 -18
- data/lib/appsignal/transaction.rb +8 -0
- data/lib/appsignal/utils/rails_helper.rb +20 -0
- data/lib/appsignal/version.rb +1 -1
- data/lib/puma/plugin/appsignal.rb +26 -0
- data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +40 -0
- data/spec/lib/appsignal/cli/diagnose_spec.rb +129 -22
- data/spec/lib/appsignal/cli/install_spec.rb +57 -8
- data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +10 -0
- data/spec/lib/appsignal/config_spec.rb +13 -11
- data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +38 -28
- 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/active_support_notifications_spec.rb +104 -25
- data/spec/lib/appsignal/hooks/puma_spec.rb +251 -34
- data/spec/lib/appsignal/hooks/sidekiq_spec.rb +209 -0
- data/spec/lib/appsignal/hooks_spec.rb +4 -0
- data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +24 -1
- data/spec/lib/appsignal/minutely_spec.rb +318 -26
- data/spec/lib/appsignal/system_spec.rb +0 -35
- data/spec/lib/appsignal/transaction_spec.rb +68 -10
- 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 +98 -22
- data/spec/lib/puma/appsignal_spec.rb +91 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/{project_fixture → fixtures/projects/valid}/config/application.rb +0 -0
- data/spec/support/{project_fixture → fixtures/projects/valid}/config/appsignal.yml +1 -0
- data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/development.rb +0 -0
- data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/production.rb +0 -0
- data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/test.rb +0 -0
- data/spec/support/{project_fixture → fixtures/projects/valid}/log/.gitkeep +0 -0
- data/spec/support/helpers/config_helpers.rb +1 -1
- data/spec/support/helpers/log_helpers.rb +6 -0
- data/spec/support/helpers/wait_for_helper.rb +28 -0
- data/spec/support/mocks/mock_probe.rb +11 -0
- data/spec/support/stubs/sidekiq/api.rb +4 -0
- metadata +43 -31
- data/spec/support/fixtures/containers/cgroups/docker +0 -14
- data/spec/support/fixtures/containers/cgroups/docker_systemd +0 -8
- data/spec/support/fixtures/containers/cgroups/lxc +0 -10
- data/spec/support/fixtures/containers/cgroups/no_permission +0 -0
- data/spec/support/fixtures/containers/cgroups/none +0 -1
@@ -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,203 @@ 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
|
+
describe ".dependencies_present?" do
|
669
|
+
before do
|
670
|
+
class Redis; end
|
671
|
+
Redis.const_set(:VERSION, version)
|
672
|
+
end
|
673
|
+
after { Object.send(:remove_const, "Redis") }
|
674
|
+
|
675
|
+
context "when Redis version is < 3.3.5" do
|
676
|
+
let(:version) { "3.3.4" }
|
677
|
+
|
678
|
+
it "does not start probe" do
|
679
|
+
expect(described_class.dependencies_present?).to be_falsy
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
context "when Redis version is >= 3.3.5" do
|
684
|
+
let(:version) { "3.3.5" }
|
685
|
+
|
686
|
+
it "does not start probe" do
|
687
|
+
expect(described_class.dependencies_present?).to be_truthy
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
692
|
+
it "loads Sidekiq::API" do
|
693
|
+
expect(defined?(Sidekiq::API)).to be_falsy
|
694
|
+
probe
|
695
|
+
expect(defined?(Sidekiq::API)).to be_truthy
|
696
|
+
end
|
697
|
+
|
698
|
+
it "logs config on initialize" do
|
699
|
+
log = capture_logs { probe }
|
700
|
+
expect(log).to contains_log(:debug, "Initializing Sidekiq probe\n")
|
701
|
+
end
|
702
|
+
|
703
|
+
it "logs used hostname on call once" do
|
704
|
+
log = capture_logs { probe.call }
|
705
|
+
expect(log).to contains_log(
|
706
|
+
:debug,
|
707
|
+
%(Sidekiq probe: Using Redis server hostname "localhost" as hostname)
|
708
|
+
)
|
709
|
+
log = capture_logs { probe.call }
|
710
|
+
# Match more logs with incompelete message
|
711
|
+
expect(log).to_not contains_log(:debug, %(Sidekiq probe: ))
|
712
|
+
end
|
713
|
+
|
714
|
+
it "collects custom metrics" do
|
715
|
+
expect_gauge("worker_count", 24).twice
|
716
|
+
expect_gauge("process_count", 25).twice
|
717
|
+
expect_gauge("connection_count", 2).twice
|
718
|
+
expect_gauge("memory_usage", 1024).twice
|
719
|
+
expect_gauge("memory_usage_rss", 512).twice
|
720
|
+
expect_gauge("job_count", 5, :status => :processed) # Gauge delta
|
721
|
+
expect_gauge("job_count", 3, :status => :failed) # Gauge delta
|
722
|
+
expect_gauge("job_count", 12, :status => :retry_queue).twice
|
723
|
+
expect_gauge("job_count", 2, :status => :died) # Gauge delta
|
724
|
+
expect_gauge("job_count", 14, :status => :scheduled).twice
|
725
|
+
expect_gauge("job_count", 15, :status => :enqueued).twice
|
726
|
+
expect_gauge("queue_length", 10, :queue => "default").twice
|
727
|
+
expect_gauge("queue_latency", 12_000, :queue => "default").twice
|
728
|
+
expect_gauge("queue_length", 1, :queue => "critical").twice
|
729
|
+
expect_gauge("queue_latency", 2_000, :queue => "critical").twice
|
730
|
+
# Call probe twice so we can calculate the delta for some gauge values
|
731
|
+
probe.call
|
732
|
+
probe.call
|
733
|
+
end
|
734
|
+
|
735
|
+
context "when `redis_info` is not defined" do
|
736
|
+
before do
|
737
|
+
allow(Sidekiq).to receive(:respond_to?).with(:redis_info).and_return(false)
|
738
|
+
end
|
739
|
+
|
740
|
+
it "does not collect redis metrics" do
|
741
|
+
expect_gauge("connection_count", 2).never
|
742
|
+
expect_gauge("memory_usage", 1024).never
|
743
|
+
expect_gauge("memory_usage_rss", 512).never
|
744
|
+
probe.call
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
context "when hostname is configured for probe" do
|
749
|
+
let(:redis_hostname) { "my_redis_server" }
|
750
|
+
let(:probe) { described_class.new(:hostname => redis_hostname) }
|
751
|
+
|
752
|
+
it "uses the redis hostname for the hostname tag" do
|
753
|
+
allow(Appsignal).to receive(:set_gauge).and_call_original
|
754
|
+
log = capture_logs { probe }
|
755
|
+
expect(log).to contains_log(
|
756
|
+
:debug,
|
757
|
+
%(Initializing Sidekiq probe with config: {:hostname=>"#{redis_hostname}"})
|
758
|
+
)
|
759
|
+
log = capture_logs { probe.call }
|
760
|
+
expect(log).to contains_log(
|
761
|
+
:debug,
|
762
|
+
"Sidekiq probe: Using hostname config option #{redis_hostname.inspect} as hostname"
|
763
|
+
)
|
764
|
+
expect(Appsignal).to have_received(:set_gauge)
|
765
|
+
.with(anything, anything, :hostname => redis_hostname).at_least(:once)
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
def expect_gauge(key, value, tags = {})
|
770
|
+
expect(Appsignal).to receive(:set_gauge)
|
771
|
+
.with("sidekiq_#{key}", value, expected_default_tags.merge(tags))
|
772
|
+
.and_call_original
|
773
|
+
end
|
774
|
+
end
|
775
|
+
end
|
@@ -68,6 +68,10 @@ describe Appsignal::Hooks do
|
|
68
68
|
expect(Appsignal::Hooks.hooks[:mock_error_hook].installed?).to be_falsy
|
69
69
|
|
70
70
|
expect(Appsignal.logger).to receive(:error).with("Error while installing mock_error_hook hook: error").once
|
71
|
+
expect(Appsignal.logger).to receive(:debug).once do |message|
|
72
|
+
# Start of the error backtrace as debug log
|
73
|
+
expect(message).to start_with(File.expand_path("../../../../", __FILE__))
|
74
|
+
end
|
71
75
|
|
72
76
|
Appsignal::Hooks.load_hooks
|
73
77
|
|
@@ -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,49 +1,341 @@
|
|
1
1
|
describe Appsignal::Minutely do
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
include WaitForHelper
|
3
|
+
|
4
|
+
before { Appsignal::Minutely.probes.clear }
|
5
5
|
|
6
|
-
it "
|
7
|
-
expect(Appsignal::Minutely.probes)
|
6
|
+
it "returns a ProbeCollection" do
|
7
|
+
expect(Appsignal::Minutely.probes)
|
8
|
+
.to be_instance_of(Appsignal::Minutely::ProbeCollection)
|
8
9
|
end
|
9
10
|
|
10
11
|
describe ".start" do
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
class ProbeWithoutDependency < MockProbe
|
13
|
+
def self.dependencies_present?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ProbeWithMissingDependency < MockProbe
|
19
|
+
def self.dependencies_present?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class BrokenProbe < MockProbe
|
25
|
+
def call
|
26
|
+
super
|
27
|
+
raise "oh no!"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class BrokenProbeOnInitialize < MockProbe
|
32
|
+
def initialize
|
33
|
+
super
|
34
|
+
raise "oh no initialize!"
|
35
|
+
end
|
36
|
+
|
37
|
+
def call
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:log_stream) { StringIO.new }
|
43
|
+
let(:log) { log_contents(log_stream) }
|
44
|
+
before do
|
45
|
+
Appsignal.logger = test_logger(log_stream)
|
46
|
+
# Speed up test time
|
47
|
+
allow(Appsignal::Minutely).to receive(:initial_wait_time).and_return(0.001)
|
48
|
+
allow(Appsignal::Minutely).to receive(:wait_time).and_return(0.001)
|
49
|
+
end
|
50
|
+
|
51
|
+
context "with an instance of a class" do
|
52
|
+
it "calls the probe every <wait_time>" do
|
53
|
+
probe = MockProbe.new
|
54
|
+
Appsignal::Minutely.probes.register :my_probe, probe
|
55
|
+
Appsignal::Minutely.start
|
56
|
+
|
57
|
+
wait_for("enough probe calls") { probe.calls >= 2 }
|
58
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 1 probe")
|
59
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 'my_probe' probe")
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when dependency requirement is not met" do
|
63
|
+
it "does not initialize the probe" do
|
64
|
+
# Working probe which we can use to wait for X ticks
|
65
|
+
working_probe = ProbeWithoutDependency.new
|
66
|
+
Appsignal::Minutely.probes.register :probe_without_dep, working_probe
|
67
|
+
|
68
|
+
probe = ProbeWithMissingDependency.new
|
69
|
+
Appsignal::Minutely.probes.register :probe_with_missing_dep, probe
|
70
|
+
Appsignal::Minutely.start
|
71
|
+
|
72
|
+
wait_for("enough probe calls") { working_probe.calls >= 2 }
|
73
|
+
# Only counts initialized probes
|
74
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 1 probe")
|
75
|
+
expect(log).to contains_log :debug, "Skipping 'probe_with_missing_dep' probe, " \
|
76
|
+
"ProbeWithMissingDependency.dependency_present? returned falsy"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "with probe class" do
|
82
|
+
it "creates an instance of the class and call that every <wait time>" do
|
83
|
+
probe = MockProbe
|
84
|
+
probe_instance = MockProbe.new
|
85
|
+
expect(probe).to receive(:new).and_return(probe_instance)
|
86
|
+
Appsignal::Minutely.probes.register :my_probe, probe
|
87
|
+
Appsignal::Minutely.start
|
88
|
+
|
89
|
+
wait_for("enough probe calls") { probe_instance.calls >= 2 }
|
90
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 1 probe")
|
91
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 'my_probe' probe")
|
92
|
+
end
|
93
|
+
|
94
|
+
context "when dependency requirement is not met" do
|
95
|
+
it "does not initialize the probe" do
|
96
|
+
# Working probe which we can use to wait for X ticks
|
97
|
+
working_probe = ProbeWithoutDependency
|
98
|
+
working_probe_instance = working_probe.new
|
99
|
+
expect(working_probe).to receive(:new).and_return(working_probe_instance)
|
100
|
+
Appsignal::Minutely.probes.register :probe_without_dep, working_probe
|
101
|
+
|
102
|
+
probe = ProbeWithMissingDependency
|
103
|
+
Appsignal::Minutely.probes.register :probe_with_missing_dep, probe
|
104
|
+
Appsignal::Minutely.start
|
105
|
+
|
106
|
+
wait_for("enough probe calls") { working_probe_instance.calls >= 2 }
|
107
|
+
# Only counts initialized probes
|
108
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 1 probe")
|
109
|
+
expect(log).to contains_log :debug, "Skipping 'probe_with_missing_dep' probe, " \
|
110
|
+
"ProbeWithMissingDependency.dependency_present? returned falsy"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context "when there is a problem initializing the probe" do
|
115
|
+
it "logs an error" do
|
116
|
+
# Working probe which we can use to wait for X ticks
|
117
|
+
working_probe = ProbeWithoutDependency
|
118
|
+
working_probe_instance = working_probe.new
|
119
|
+
expect(working_probe).to receive(:new).and_return(working_probe_instance)
|
120
|
+
Appsignal::Minutely.probes.register :probe_without_dep, working_probe
|
121
|
+
|
122
|
+
probe = BrokenProbeOnInitialize
|
123
|
+
Appsignal::Minutely.probes.register :broken_probe_on_initialize, probe
|
124
|
+
Appsignal::Minutely.start
|
125
|
+
|
126
|
+
wait_for("enough probe calls") { working_probe_instance.calls >= 2 }
|
127
|
+
# Only counts initialized probes
|
128
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 1 probe")
|
129
|
+
# Logs error
|
130
|
+
expect(log).to contains_log(
|
131
|
+
:error,
|
132
|
+
"Error while initializing minutely probe 'broken_probe_on_initialize': " \
|
133
|
+
"oh no initialize!"
|
134
|
+
)
|
135
|
+
# Start of the error backtrace as debug log
|
136
|
+
expect(log).to contains_log :debug, File.expand_path("../../../../", __FILE__)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "with a lambda" do
|
142
|
+
it "calls the lambda every <wait time>" do
|
143
|
+
calls = 0
|
144
|
+
probe = lambda { calls += 1 }
|
145
|
+
Appsignal::Minutely.probes.register :my_probe, probe
|
146
|
+
Appsignal::Minutely.start
|
147
|
+
|
148
|
+
wait_for("enough probe calls") { calls >= 2 }
|
149
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 1 probe")
|
150
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 'my_probe' probe")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "with a broken probe" do
|
155
|
+
it "logs the error and continues calling the probes every <wait_time>" do
|
156
|
+
probe = MockProbe.new
|
157
|
+
broken_probe = BrokenProbe.new
|
158
|
+
Appsignal::Minutely.probes.register :my_probe, probe
|
159
|
+
Appsignal::Minutely.probes.register :broken_probe, broken_probe
|
160
|
+
Appsignal::Minutely.start
|
161
|
+
|
162
|
+
wait_for("enough probe calls") { probe.calls >= 2 }
|
163
|
+
wait_for("enough broken_probe calls") { broken_probe.calls >= 2 }
|
164
|
+
|
165
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 2 probes")
|
166
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 'my_probe' probe")
|
167
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 'broken_probe' probe")
|
168
|
+
expect(log).to contains_log(:error, "Error in minutely probe 'broken_probe': oh no!")
|
169
|
+
gem_path = File.expand_path("../../../../", __FILE__) # Start of backtrace
|
170
|
+
expect(log).to contains_log(:debug, gem_path)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
it "ensures only one minutely probes thread is active at a time" do
|
175
|
+
alive_thread_counter = proc { Thread.list.reject { |t| t.status == "dead" }.length }
|
176
|
+
probe = MockProbe.new
|
177
|
+
Appsignal::Minutely.probes.register :my_probe, probe
|
178
|
+
expect do
|
179
|
+
Appsignal::Minutely.start
|
180
|
+
end.to change { alive_thread_counter.call }.by(1)
|
181
|
+
|
182
|
+
wait_for("enough probe calls") { probe.calls >= 2 }
|
183
|
+
expect(Appsignal::Minutely).to have_received(:initial_wait_time).once
|
184
|
+
expect(Appsignal::Minutely).to have_received(:wait_time).at_least(:once)
|
185
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 1 probe")
|
186
|
+
expect(log).to contains_log(:debug, "Gathering minutely metrics with 'my_probe' probe")
|
187
|
+
|
188
|
+
# Starting twice in this spec, so expecting it more than once
|
189
|
+
expect(Appsignal::Minutely).to have_received(:initial_wait_time).once
|
190
|
+
expect do
|
191
|
+
# Fetch old thread
|
192
|
+
thread = Appsignal::Minutely.class_variable_get(:@@thread)
|
193
|
+
Appsignal::Minutely.start
|
194
|
+
thread && thread.join # Wait for old thread to exit
|
195
|
+
end.to_not(change { alive_thread_counter.call })
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
describe ".stop" do
|
200
|
+
before do
|
201
|
+
allow(Appsignal::Minutely).to receive(:initial_wait_time).and_return(0.001)
|
202
|
+
end
|
16
203
|
|
204
|
+
it "stops the minutely thread" do
|
17
205
|
Appsignal::Minutely.start
|
206
|
+
thread = Appsignal::Minutely.class_variable_get(:@@thread)
|
207
|
+
expect(%w[sleep run]).to include(thread.status)
|
208
|
+
Appsignal::Minutely.stop
|
209
|
+
thread.join
|
210
|
+
expect(thread.status).to eql(false)
|
211
|
+
end
|
18
212
|
|
19
|
-
|
213
|
+
it "clears the probe instances array" do
|
214
|
+
Appsignal::Minutely.probes.register :my_probe, lambda {}
|
215
|
+
Appsignal::Minutely.start
|
216
|
+
thread = Appsignal::Minutely.class_variable_get(:@@thread)
|
217
|
+
wait_for("probes initialized") do
|
218
|
+
!Appsignal::Minutely.send(:probe_instances).empty?
|
219
|
+
end
|
220
|
+
expect(Appsignal::Minutely.send(:probe_instances)).to_not be_empty
|
221
|
+
Appsignal::Minutely.stop
|
222
|
+
thread.join
|
223
|
+
expect(Appsignal::Minutely.send(:probe_instances)).to be_empty
|
20
224
|
end
|
21
225
|
end
|
22
226
|
|
23
227
|
describe ".wait_time" do
|
24
|
-
it "
|
25
|
-
|
26
|
-
|
228
|
+
it "gets the time to the next minute" do
|
229
|
+
time = Time.new(2019, 4, 9, 12, 0, 20)
|
230
|
+
Timecop.freeze time do
|
231
|
+
expect(Appsignal::Minutely.wait_time).to eq 40
|
232
|
+
end
|
27
233
|
end
|
28
234
|
end
|
29
235
|
|
30
|
-
describe ".
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
236
|
+
describe ".initial_wait_time" do
|
237
|
+
context "when started in the last 30 seconds of a minute" do
|
238
|
+
it "waits for the number of seconds + 60" do
|
239
|
+
time = Time.new(2019, 4, 9, 12, 0, 31)
|
240
|
+
Timecop.freeze time do
|
241
|
+
expect(Appsignal::Minutely.send(:initial_wait_time)).to eql(29 + 60)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
35
245
|
|
36
|
-
|
37
|
-
|
246
|
+
context "when started in the first 30 seconds of a minute" do
|
247
|
+
it "waits the remaining seconds in the minute" do
|
248
|
+
time = Time.new(2019, 4, 9, 12, 0, 29)
|
249
|
+
Timecop.freeze time do
|
250
|
+
expect(Appsignal::Minutely.send(:initial_wait_time)).to eql(31)
|
251
|
+
end
|
252
|
+
end
|
38
253
|
end
|
39
254
|
end
|
40
255
|
|
41
|
-
describe Appsignal::Minutely::
|
42
|
-
|
43
|
-
|
44
|
-
|
256
|
+
describe Appsignal::Minutely::ProbeCollection do
|
257
|
+
let(:collection) { described_class.new }
|
258
|
+
|
259
|
+
describe "#count" do
|
260
|
+
it "returns how many probes are registered" do
|
261
|
+
expect(collection.count).to eql(0)
|
262
|
+
collection.register :my_probe_1, lambda {}
|
263
|
+
expect(collection.count).to eql(1)
|
264
|
+
collection.register :my_probe_2, lambda {}
|
265
|
+
expect(collection.count).to eql(2)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
describe "#clear" do
|
270
|
+
it "clears the list of probes" do
|
271
|
+
collection.register :my_probe_1, lambda {}
|
272
|
+
collection.register :my_probe_2, lambda {}
|
273
|
+
expect(collection.count).to eql(2)
|
274
|
+
collection.clear
|
275
|
+
expect(collection.count).to eql(0)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
describe "#[]" do
|
280
|
+
it "returns the probe for that name" do
|
281
|
+
probe = lambda {}
|
282
|
+
collection.register :my_probe, probe
|
283
|
+
expect(collection[:my_probe]).to eql(probe)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
describe "#<<" do
|
288
|
+
let(:out_stream) { std_stream }
|
289
|
+
let(:output) { out_stream.read }
|
290
|
+
let(:log_stream) { std_stream }
|
291
|
+
let(:log) { log_contents(log_stream) }
|
292
|
+
before { Appsignal.logger = test_logger(log_stream) }
|
293
|
+
|
294
|
+
it "adds the probe, but prints a deprecation warning" do
|
295
|
+
probe = lambda {}
|
296
|
+
capture_stdout(out_stream) { collection << probe }
|
297
|
+
deprecation_message = "Deprecated " \
|
298
|
+
"`Appsignal::Minute.probes <<` call. Please use " \
|
299
|
+
"`Appsignal::Minutely.probes.register` instead."
|
300
|
+
expect(output).to include "appsignal WARNING: #{deprecation_message}"
|
301
|
+
expect(log).to contains_log :warn, deprecation_message
|
302
|
+
expect(collection[probe.object_id]).to eql(probe)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
describe "#register" do
|
307
|
+
let(:log_stream) { std_stream }
|
308
|
+
let(:log) { log_contents(log_stream) }
|
309
|
+
before { Appsignal.logger = test_logger(log_stream) }
|
310
|
+
|
311
|
+
it "adds the by key probe" do
|
312
|
+
probe = lambda {}
|
313
|
+
collection.register :my_probe, probe
|
314
|
+
expect(collection[:my_probe]).to eql(probe)
|
315
|
+
end
|
316
|
+
|
317
|
+
context "when a probe is already registered with the same key" do
|
318
|
+
it "logs a debug message" do
|
319
|
+
probe = lambda {}
|
320
|
+
collection.register :my_probe, probe
|
321
|
+
collection.register :my_probe, probe
|
322
|
+
expect(log).to contains_log :debug, "A probe with the name " \
|
323
|
+
"`my_probe` is already registered. Overwriting the entry " \
|
324
|
+
"with the new probe."
|
325
|
+
expect(collection[:my_probe]).to eql(probe)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
45
329
|
|
46
|
-
|
330
|
+
describe "#each" do
|
331
|
+
it "loops over the registered probes" do
|
332
|
+
probe = lambda {}
|
333
|
+
collection.register :my_probe, probe
|
334
|
+
list = []
|
335
|
+
collection.each do |name, p|
|
336
|
+
list << [name, p]
|
337
|
+
end
|
338
|
+
expect(list).to eql([[:my_probe, probe]])
|
47
339
|
end
|
48
340
|
end
|
49
341
|
end
|