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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  3. data/.github/ISSUE_TEMPLATE/chore.md +14 -0
  4. data/.gitignore +2 -3
  5. data/.rubocop.yml +3 -0
  6. data/.rubocop_todo.yml +7 -16
  7. data/.travis.yml +28 -27
  8. data/CHANGELOG.md +657 -533
  9. data/README.md +31 -3
  10. data/Rakefile +128 -129
  11. data/SUPPORT.md +16 -0
  12. data/appsignal.gemspec +17 -4
  13. data/build_matrix.yml +21 -9
  14. data/ext/Rakefile +23 -17
  15. data/ext/agent.yml +40 -37
  16. data/ext/base.rb +116 -31
  17. data/ext/extconf.rb +34 -28
  18. data/gemfiles/capistrano2.gemfile +5 -0
  19. data/gemfiles/capistrano3.gemfile +5 -0
  20. data/gemfiles/grape.gemfile +5 -0
  21. data/gemfiles/no_dependencies.gemfile +5 -0
  22. data/gemfiles/padrino.gemfile +5 -0
  23. data/gemfiles/que.gemfile +5 -0
  24. data/gemfiles/que_beta.gemfile +10 -0
  25. data/gemfiles/rails-3.2.gemfile +5 -0
  26. data/gemfiles/rails-4.0.gemfile +5 -0
  27. data/gemfiles/rails-4.1.gemfile +5 -0
  28. data/gemfiles/rails-4.2.gemfile +5 -0
  29. data/gemfiles/rails-6.0.gemfile +5 -0
  30. data/gemfiles/resque.gemfile +5 -0
  31. data/lib/appsignal.rb +14 -492
  32. data/lib/appsignal/cli/demo.rb +5 -2
  33. data/lib/appsignal/cli/diagnose.rb +84 -4
  34. data/lib/appsignal/cli/diagnose/paths.rb +0 -5
  35. data/lib/appsignal/cli/diagnose/utils.rb +19 -0
  36. data/lib/appsignal/cli/helpers.rb +6 -0
  37. data/lib/appsignal/cli/install.rb +45 -15
  38. data/lib/appsignal/cli/notify_of_deploy.rb +10 -0
  39. data/lib/appsignal/config.rb +1 -2
  40. data/lib/appsignal/event_formatter.rb +4 -5
  41. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +10 -8
  42. data/lib/appsignal/event_formatter/moped/query_formatter.rb +60 -59
  43. data/lib/appsignal/extension.rb +2 -2
  44. data/lib/appsignal/helpers/instrumentation.rb +494 -0
  45. data/lib/appsignal/helpers/metrics.rb +54 -0
  46. data/lib/appsignal/hooks.rb +11 -8
  47. data/lib/appsignal/hooks/active_support_notifications.rb +2 -5
  48. data/lib/appsignal/hooks/puma.rb +74 -11
  49. data/lib/appsignal/hooks/sequel.rb +1 -1
  50. data/lib/appsignal/hooks/sidekiq.rb +115 -0
  51. data/lib/appsignal/integrations/mongo_ruby_driver.rb +7 -0
  52. data/lib/appsignal/integrations/que.rb +9 -8
  53. data/lib/appsignal/integrations/railtie.rb +2 -1
  54. data/lib/appsignal/marker.rb +2 -3
  55. data/lib/appsignal/minutely.rb +188 -19
  56. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -1
  57. data/lib/appsignal/system.rb +16 -18
  58. data/lib/appsignal/transaction.rb +8 -0
  59. data/lib/appsignal/utils/rails_helper.rb +20 -0
  60. data/lib/appsignal/version.rb +1 -1
  61. data/lib/puma/plugin/appsignal.rb +26 -0
  62. data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +40 -0
  63. data/spec/lib/appsignal/cli/diagnose_spec.rb +129 -22
  64. data/spec/lib/appsignal/cli/install_spec.rb +57 -8
  65. data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +10 -0
  66. data/spec/lib/appsignal/config_spec.rb +13 -11
  67. data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +38 -28
  68. data/spec/lib/appsignal/event_formatter/moped/query_formatter_spec.rb +6 -0
  69. data/spec/lib/appsignal/event_formatter_spec.rb +168 -69
  70. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +104 -25
  71. data/spec/lib/appsignal/hooks/puma_spec.rb +251 -34
  72. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +209 -0
  73. data/spec/lib/appsignal/hooks_spec.rb +4 -0
  74. data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +24 -1
  75. data/spec/lib/appsignal/minutely_spec.rb +318 -26
  76. data/spec/lib/appsignal/system_spec.rb +0 -35
  77. data/spec/lib/appsignal/transaction_spec.rb +68 -10
  78. data/spec/lib/appsignal/utils/hash_sanitizer_spec.rb +39 -31
  79. data/spec/lib/appsignal/utils/json_spec.rb +7 -3
  80. data/spec/lib/appsignal_spec.rb +98 -22
  81. data/spec/lib/puma/appsignal_spec.rb +91 -0
  82. data/spec/spec_helper.rb +13 -0
  83. data/spec/support/{project_fixture → fixtures/projects/valid}/config/application.rb +0 -0
  84. data/spec/support/{project_fixture → fixtures/projects/valid}/config/appsignal.yml +1 -0
  85. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/development.rb +0 -0
  86. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/production.rb +0 -0
  87. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/test.rb +0 -0
  88. data/spec/support/{project_fixture → fixtures/projects/valid}/log/.gitkeep +0 -0
  89. data/spec/support/helpers/config_helpers.rb +1 -1
  90. data/spec/support/helpers/log_helpers.rb +6 -0
  91. data/spec/support/helpers/wait_for_helper.rb +28 -0
  92. data/spec/support/mocks/mock_probe.rb +11 -0
  93. data/spec/support/stubs/sidekiq/api.rb +4 -0
  94. metadata +43 -31
  95. data/spec/support/fixtures/containers/cgroups/docker +0 -14
  96. data/spec/support/fixtures/containers/cgroups/docker_systemd +0 -8
  97. data/spec/support/fixtures/containers/cgroups/lxc +0 -10
  98. data/spec/support/fixtures/containers/cgroups/no_permission +0 -0
  99. 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
- before do
3
- Appsignal::Minutely.probes.clear
4
- end
2
+ include WaitForHelper
3
+
4
+ before { Appsignal::Minutely.probes.clear }
5
5
 
6
- it "should have a list of probes" do
7
- expect(Appsignal::Minutely.probes).to be_instance_of(Array)
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
- 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)
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
- sleep 0.5
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 "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
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 ".add_gc_probe" do
31
- it "should add the gc probe to the list" do
32
- expect(Appsignal::Minutely.probes).to be_empty
33
-
34
- Appsignal::Minutely.add_gc_probe
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
- expect(Appsignal::Minutely.probes.size).to eq(1)
37
- expect(Appsignal::Minutely.probes[0]).to be_instance_of(Appsignal::Minutely::GCProbe)
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::GCProbe do
42
- describe "#call" do
43
- it "should collect GC metrics" do
44
- expect(Appsignal).to receive(:set_process_gauge).at_least(8).times
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
- Appsignal::Minutely::GCProbe.new.call
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