appsignal 2.9.2.alpha.1 → 2.9.18.beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  3. data/.github/ISSUE_TEMPLATE/chore.md +14 -0
  4. data/.gitignore +1 -2
  5. data/.rubocop.yml +3 -0
  6. data/.travis.yml +25 -27
  7. data/CHANGELOG.md +632 -535
  8. data/README.md +8 -3
  9. data/Rakefile +118 -122
  10. data/SUPPORT.md +16 -0
  11. data/appsignal.gemspec +14 -4
  12. data/build_matrix.yml +16 -8
  13. data/ext/Rakefile +2 -3
  14. data/ext/agent.yml +40 -37
  15. data/ext/base.rb +37 -14
  16. data/ext/extconf.rb +3 -4
  17. data/gemfiles/capistrano2.gemfile +5 -0
  18. data/gemfiles/capistrano3.gemfile +5 -0
  19. data/gemfiles/grape.gemfile +5 -0
  20. data/gemfiles/no_dependencies.gemfile +5 -0
  21. data/gemfiles/padrino.gemfile +5 -0
  22. data/gemfiles/que.gemfile +5 -0
  23. data/gemfiles/que_beta.gemfile +10 -0
  24. data/gemfiles/rails-3.2.gemfile +5 -0
  25. data/gemfiles/rails-4.0.gemfile +5 -0
  26. data/gemfiles/rails-4.1.gemfile +5 -0
  27. data/gemfiles/rails-4.2.gemfile +5 -0
  28. data/gemfiles/rails-6.0.gemfile +1 -1
  29. data/gemfiles/resque.gemfile +5 -0
  30. data/lib/appsignal.rb +1 -4
  31. data/lib/appsignal/cli/demo.rb +5 -2
  32. data/lib/appsignal/cli/diagnose/utils.rb +2 -0
  33. data/lib/appsignal/cli/install.rb +34 -10
  34. data/lib/appsignal/cli/notify_of_deploy.rb +10 -0
  35. data/lib/appsignal/config.rb +5 -2
  36. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +10 -8
  37. data/lib/appsignal/helpers/instrumentation.rb +18 -9
  38. data/lib/appsignal/helpers/metrics.rb +0 -1
  39. data/lib/appsignal/hooks.rb +3 -1
  40. data/lib/appsignal/hooks/active_support_notifications.rb +2 -5
  41. data/lib/appsignal/hooks/puma.rb +15 -13
  42. data/lib/appsignal/hooks/sequel.rb +1 -1
  43. data/lib/appsignal/hooks/sidekiq.rb +33 -8
  44. data/lib/appsignal/integrations/que.rb +9 -8
  45. data/lib/appsignal/minutely.rb +38 -19
  46. data/lib/appsignal/transaction.rb +5 -0
  47. data/lib/appsignal/utils/rails_helper.rb +4 -0
  48. data/lib/appsignal/version.rb +1 -1
  49. data/lib/puma/plugin/appsignal.rb +26 -0
  50. data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +40 -0
  51. data/spec/lib/appsignal/cli/install_spec.rb +51 -7
  52. data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +10 -0
  53. data/spec/lib/appsignal/config_spec.rb +13 -8
  54. data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +38 -28
  55. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +104 -25
  56. data/spec/lib/appsignal/hooks/puma_spec.rb +69 -39
  57. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +65 -3
  58. data/spec/lib/appsignal/hooks_spec.rb +4 -0
  59. data/spec/lib/appsignal/minutely_spec.rb +150 -88
  60. data/spec/lib/appsignal/transaction_spec.rb +27 -4
  61. data/spec/lib/appsignal_spec.rb +62 -11
  62. data/spec/lib/puma/appsignal_spec.rb +91 -0
  63. data/spec/support/{project_fixture → fixtures/projects/valid}/config/application.rb +0 -0
  64. data/spec/support/{project_fixture → fixtures/projects/valid}/config/appsignal.yml +0 -0
  65. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/development.rb +0 -0
  66. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/production.rb +0 -0
  67. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/test.rb +0 -0
  68. data/spec/support/{project_fixture → fixtures/projects/valid}/log/.gitkeep +0 -0
  69. data/spec/support/helpers/config_helpers.rb +1 -1
  70. data/spec/support/helpers/wait_for_helper.rb +28 -0
  71. data/spec/support/mocks/mock_probe.rb +11 -0
  72. metadata +38 -30
  73. data/spec/support/fixtures/containers/cgroups/docker +0 -14
  74. data/spec/support/fixtures/containers/cgroups/docker_systemd +0 -8
  75. data/spec/support/fixtures/containers/cgroups/lxc +0 -10
  76. data/spec/support/fixtures/containers/cgroups/no_permission +0 -0
  77. data/spec/support/fixtures/containers/cgroups/none +0 -1
@@ -8,11 +8,6 @@ describe Appsignal::Hooks::PumaHook do
8
8
  def self.cli_config
9
9
  @cli_config ||= CliConfig.new
10
10
  end
11
-
12
- class Cluster
13
- def stop_workers
14
- end
15
- end
16
11
  end
17
12
 
18
13
  class CliConfig
@@ -31,49 +26,84 @@ describe Appsignal::Hooks::PumaHook do
31
26
  it { is_expected.to be_truthy }
32
27
  end
33
28
 
34
- context "when installed" do
35
- before do
36
- Appsignal::Hooks::PumaHook.new.install
37
- end
29
+ describe "installation" do
30
+ before { Appsignal::Minutely.probes.clear }
38
31
 
39
- it "adds behavior to Unicorn::Worker#close" do
40
- cluster = Puma::Cluster.new
32
+ context "when not clustered mode" do
33
+ it "does not add AppSignal stop behavior Puma::Cluster" do
34
+ expect(defined?(::Puma::Cluster)).to be_falsy
35
+ # Does not error on call
36
+ Appsignal::Hooks::PumaHook.new.install
37
+ end
41
38
 
42
- expect(Appsignal).to receive(:stop)
43
- expect(cluster).to receive(:stop_workers_without_appsignal)
39
+ context "with APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
40
+ before do
41
+ # Set in lib/puma/appsignal.rb
42
+ APPSIGNAL_PUMA_PLUGIN_LOADED = true
43
+ end
44
+ after { Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED }
44
45
 
45
- cluster.stop_workers
46
- end
46
+ it "does not add the Puma minutely probe" do
47
+ Appsignal::Hooks::PumaHook.new.install
48
+ expect(Appsignal::Minutely.probes[:puma]).to be_nil
49
+ end
50
+ end
47
51
 
48
- it "adds the Puma minutely probe" do
49
- probe = Appsignal::Minutely.probes[:puma]
50
- expect(probe).to eql(Appsignal::Hooks::PumaProbe)
51
- end
52
- end
52
+ context "without APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
53
+ it "adds the Puma minutely probe" do
54
+ expect(defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)).to be_nil
53
55
 
54
- context "with nil hooks" do
55
- before do
56
- Puma.cli_config.options.delete(:before_worker_boot)
57
- Puma.cli_config.options.delete(:before_worker_shutdown)
58
- Appsignal::Hooks::PumaHook.new.install
56
+ Appsignal::Hooks::PumaHook.new.install
57
+ probe = Appsignal::Minutely.probes[:puma]
58
+ expect(probe).to eql(Appsignal::Hooks::PumaProbe)
59
+ end
60
+ end
59
61
  end
60
62
 
61
- it "should add a before shutdown worker callback" do
62
- expect(Puma.cli_config.options[:before_worker_boot].first).to be_a(Proc)
63
- expect(Puma.cli_config.options[:before_worker_shutdown].first).to be_a(Proc)
64
- end
65
- end
63
+ context "when in clustered mode" do
64
+ before do
65
+ class Puma
66
+ class Cluster
67
+ def stop_workers
68
+ @called = true
69
+ end
70
+ end
71
+ end
72
+ end
73
+ after { Puma.send(:remove_const, :Cluster) }
66
74
 
67
- context "with existing hooks" do
68
- before do
69
- Puma.cli_config.options[:before_worker_boot] = []
70
- Puma.cli_config.options[:before_worker_shutdown] = []
71
- Appsignal::Hooks::PumaHook.new.install
72
- end
75
+ it "adds behavior to Puma::Cluster.stop_workers" do
76
+ Appsignal::Hooks::PumaHook.new.install
77
+ cluster = Puma::Cluster.new
78
+
79
+ expect(cluster.instance_variable_defined?(:@called)).to be_falsy
80
+ expect(Appsignal).to receive(:stop).and_call_original
81
+ cluster.stop_workers
82
+ expect(cluster.instance_variable_get(:@called)).to be(true)
83
+ end
84
+
85
+ context "with APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
86
+ before do
87
+ # Set in lib/puma/appsignal.rb
88
+ APPSIGNAL_PUMA_PLUGIN_LOADED = true
89
+ end
90
+ after { Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED }
91
+
92
+ it "does not add the Puma minutely probe" do
93
+ Appsignal::Hooks::PumaHook.new.install
94
+ expect(Appsignal::Minutely.probes[:puma]).to be_nil
95
+ end
96
+ end
73
97
 
74
- it "should add a before shutdown worker callback" do
75
- expect(Puma.cli_config.options[:before_worker_boot].first).to be_a(Proc)
76
- expect(Puma.cli_config.options[:before_worker_shutdown].first).to be_a(Proc)
98
+ context "without APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
99
+ it "adds the Puma minutely probe" do
100
+ expect(defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)).to be_nil
101
+
102
+ Appsignal::Hooks::PumaHook.new.install
103
+ probe = Appsignal::Minutely.probes[:puma]
104
+ expect(probe).to eql(Appsignal::Hooks::PumaProbe)
105
+ end
106
+ end
77
107
  end
78
108
  end
79
109
  end
@@ -665,12 +665,52 @@ describe Appsignal::Hooks::SidekiqProbe do
665
665
  end
666
666
  after { Object.send(:remove_const, "Sidekiq") }
667
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
+
668
692
  it "loads Sidekiq::API" do
669
693
  expect(defined?(Sidekiq::API)).to be_falsy
670
694
  probe
671
695
  expect(defined?(Sidekiq::API)).to be_truthy
672
696
  end
673
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
+
674
714
  it "collects custom metrics" do
675
715
  expect_gauge("worker_count", 24).twice
676
716
  expect_gauge("process_count", 25).twice
@@ -684,21 +724,43 @@ describe Appsignal::Hooks::SidekiqProbe do
684
724
  expect_gauge("job_count", 14, :status => :scheduled).twice
685
725
  expect_gauge("job_count", 15, :status => :enqueued).twice
686
726
  expect_gauge("queue_length", 10, :queue => "default").twice
687
- expect_gauge("queue_latency", 12, :queue => "default").twice
727
+ expect_gauge("queue_latency", 12_000, :queue => "default").twice
688
728
  expect_gauge("queue_length", 1, :queue => "critical").twice
689
- expect_gauge("queue_latency", 2, :queue => "critical").twice
729
+ expect_gauge("queue_latency", 2_000, :queue => "critical").twice
690
730
  # Call probe twice so we can calculate the delta for some gauge values
691
731
  probe.call
692
732
  probe.call
693
733
  end
694
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
+
695
748
  context "when hostname is configured for probe" do
696
749
  let(:redis_hostname) { "my_redis_server" }
697
750
  let(:probe) { described_class.new(:hostname => redis_hostname) }
698
751
 
699
752
  it "uses the redis hostname for the hostname tag" do
700
753
  allow(Appsignal).to receive(:set_gauge).and_call_original
701
- probe.call
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
+ )
702
764
  expect(Appsignal).to have_received(:set_gauge)
703
765
  .with(anything, anything, :hostname => redis_hostname).at_least(:once)
704
766
  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
 
@@ -1,4 +1,6 @@
1
1
  describe Appsignal::Minutely do
2
+ include WaitForHelper
3
+
2
4
  before { Appsignal::Minutely.probes.clear }
3
5
 
4
6
  it "returns a ProbeCollection" do
@@ -7,78 +9,151 @@ describe Appsignal::Minutely do
7
9
  end
8
10
 
9
11
  describe ".start" do
10
- class Probe
11
- attr_reader :calls
12
-
13
- def initialize
14
- @calls = 0
12
+ class ProbeWithoutDependency < MockProbe
13
+ def self.dependencies_present?
14
+ true
15
15
  end
16
+ end
16
17
 
17
- def call
18
- @calls += 1
18
+ class ProbeWithMissingDependency < MockProbe
19
+ def self.dependencies_present?
20
+ false
19
21
  end
20
22
  end
21
23
 
22
- class BrokenProbe < Probe
24
+ class BrokenProbe < MockProbe
23
25
  def call
24
26
  super
25
27
  raise "oh no!"
26
28
  end
27
29
  end
28
30
 
29
- let(:log_stream) { StringIO.new }
30
- let(:log) do
31
- log_stream.rewind
32
- log_stream.read
31
+ class BrokenProbeOnInitialize < MockProbe
32
+ def initialize
33
+ super
34
+ raise "oh no initialize!"
35
+ end
36
+
37
+ def call
38
+ true
39
+ end
33
40
  end
41
+
42
+ let(:log_stream) { StringIO.new }
43
+ let(:log) { log_contents(log_stream) }
34
44
  before do
35
- Appsignal.logger = Logger.new(log_stream)
45
+ Appsignal.logger = test_logger(log_stream)
36
46
  # Speed up test time
47
+ allow(Appsignal::Minutely).to receive(:initial_wait_time).and_return(0.001)
37
48
  allow(Appsignal::Minutely).to receive(:wait_time).and_return(0.001)
38
49
  end
39
50
 
40
51
  context "with an instance of a class" do
41
52
  it "calls the probe every <wait_time>" do
42
- probe = Probe.new
53
+ probe = MockProbe.new
43
54
  Appsignal::Minutely.probes.register :my_probe, probe
44
55
  Appsignal::Minutely.start
45
56
 
46
57
  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")
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
49
78
  end
50
79
  end
51
80
 
52
81
  context "with probe class" do
53
82
  it "creates an instance of the class and call that every <wait time>" do
54
- probe = Probe
55
- probe_instance = Probe.new
83
+ probe = MockProbe
84
+ probe_instance = MockProbe.new
56
85
  expect(probe).to receive(:new).and_return(probe_instance)
57
86
  Appsignal::Minutely.probes.register :my_probe, probe
58
87
  Appsignal::Minutely.start
59
88
 
60
89
  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")
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
63
138
  end
64
139
  end
65
140
 
66
141
  context "with a lambda" do
67
142
  it "calls the lambda every <wait time>" do
68
143
  calls = 0
69
- probe = -> { calls += 1 }
144
+ probe = lambda { calls += 1 }
70
145
  Appsignal::Minutely.probes.register :my_probe, probe
71
146
  Appsignal::Minutely.start
72
147
 
73
148
  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")
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")
76
151
  end
77
152
  end
78
153
 
79
154
  context "with a broken probe" do
80
155
  it "logs the error and continues calling the probes every <wait_time>" do
81
- probe = Probe.new
156
+ probe = MockProbe.new
82
157
  broken_probe = BrokenProbe.new
83
158
  Appsignal::Minutely.probes.register :my_probe, probe
84
159
  Appsignal::Minutely.probes.register :broken_probe, broken_probe
@@ -87,24 +162,31 @@ describe Appsignal::Minutely do
87
162
  wait_for("enough probe calls") { probe.calls >= 2 }
88
163
  wait_for("enough broken_probe calls") { broken_probe.calls >= 2 }
89
164
 
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!")
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)
94
171
  end
95
172
  end
96
173
 
97
174
  it "ensures only one minutely probes thread is active at a time" do
98
175
  alive_thread_counter = proc { Thread.list.reject { |t| t.status == "dead" }.length }
99
- probe = Probe.new
176
+ probe = MockProbe.new
100
177
  Appsignal::Minutely.probes.register :my_probe, probe
101
178
  expect do
102
179
  Appsignal::Minutely.start
103
180
  end.to change { alive_thread_counter.call }.by(1)
104
181
 
105
182
  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")
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
108
190
  expect do
109
191
  # Fetch old thread
110
192
  thread = Appsignal::Minutely.class_variable_get(:@@thread)
@@ -112,36 +194,13 @@ describe Appsignal::Minutely do
112
194
  thread && thread.join # Wait for old thread to exit
113
195
  end.to_not(change { alive_thread_counter.call })
114
196
  end
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
197
  end
143
198
 
144
199
  describe ".stop" do
200
+ before do
201
+ allow(Appsignal::Minutely).to receive(:initial_wait_time).and_return(0.001)
202
+ end
203
+
145
204
  it "stops the minutely thread" do
146
205
  Appsignal::Minutely.start
147
206
  thread = Appsignal::Minutely.class_variable_get(:@@thread)
@@ -152,9 +211,12 @@ describe Appsignal::Minutely do
152
211
  end
153
212
 
154
213
  it "clears the probe instances array" do
155
- Appsignal::Minutely.probes.register :my_probe, -> {}
214
+ Appsignal::Minutely.probes.register :my_probe, lambda {}
156
215
  Appsignal::Minutely.start
157
216
  thread = Appsignal::Minutely.class_variable_get(:@@thread)
217
+ wait_for("probes initialized") do
218
+ !Appsignal::Minutely.send(:probe_instances).empty?
219
+ end
158
220
  expect(Appsignal::Minutely.send(:probe_instances)).to_not be_empty
159
221
  Appsignal::Minutely.stop
160
222
  thread.join
@@ -164,20 +226,30 @@ describe Appsignal::Minutely do
164
226
 
165
227
  describe ".wait_time" do
166
228
  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
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
169
233
  end
170
234
  end
171
235
 
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
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
177
245
 
178
- expect(Appsignal::Minutely.probes.count).to eql(1)
179
- expect(Appsignal::Minutely.probes[:garbage_collection])
180
- .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
181
253
  end
182
254
  end
183
255
 
@@ -187,17 +259,17 @@ describe Appsignal::Minutely do
187
259
  describe "#count" do
188
260
  it "returns how many probes are registered" do
189
261
  expect(collection.count).to eql(0)
190
- collection.register :my_probe_1, -> {}
262
+ collection.register :my_probe_1, lambda {}
191
263
  expect(collection.count).to eql(1)
192
- collection.register :my_probe_2, -> {}
264
+ collection.register :my_probe_2, lambda {}
193
265
  expect(collection.count).to eql(2)
194
266
  end
195
267
  end
196
268
 
197
269
  describe "#clear" do
198
270
  it "clears the list of probes" do
199
- collection.register :my_probe_1, -> {}
200
- collection.register :my_probe_2, -> {}
271
+ collection.register :my_probe_1, lambda {}
272
+ collection.register :my_probe_2, lambda {}
201
273
  expect(collection.count).to eql(2)
202
274
  collection.clear
203
275
  expect(collection.count).to eql(0)
@@ -206,7 +278,7 @@ describe Appsignal::Minutely do
206
278
 
207
279
  describe "#[]" do
208
280
  it "returns the probe for that name" do
209
- probe = -> {}
281
+ probe = lambda {}
210
282
  collection.register :my_probe, probe
211
283
  expect(collection[:my_probe]).to eql(probe)
212
284
  end
@@ -220,7 +292,7 @@ describe Appsignal::Minutely do
220
292
  before { Appsignal.logger = test_logger(log_stream) }
221
293
 
222
294
  it "adds the probe, but prints a deprecation warning" do
223
- probe = -> {}
295
+ probe = lambda {}
224
296
  capture_stdout(out_stream) { collection << probe }
225
297
  deprecation_message = "Deprecated " \
226
298
  "`Appsignal::Minute.probes <<` call. Please use " \
@@ -237,14 +309,14 @@ describe Appsignal::Minutely do
237
309
  before { Appsignal.logger = test_logger(log_stream) }
238
310
 
239
311
  it "adds the by key probe" do
240
- probe = -> {}
312
+ probe = lambda {}
241
313
  collection.register :my_probe, probe
242
314
  expect(collection[:my_probe]).to eql(probe)
243
315
  end
244
316
 
245
317
  context "when a probe is already registered with the same key" do
246
318
  it "logs a debug message" do
247
- probe = -> {}
319
+ probe = lambda {}
248
320
  collection.register :my_probe, probe
249
321
  collection.register :my_probe, probe
250
322
  expect(log).to contains_log :debug, "A probe with the name " \
@@ -257,7 +329,7 @@ describe Appsignal::Minutely do
257
329
 
258
330
  describe "#each" do
259
331
  it "loops over the registered probes" do
260
- probe = -> {}
332
+ probe = lambda {}
261
333
  collection.register :my_probe, probe
262
334
  list = []
263
335
  collection.each do |name, p|
@@ -267,14 +339,4 @@ describe Appsignal::Minutely do
267
339
  end
268
340
  end
269
341
  end
270
-
271
- describe Appsignal::Minutely::GCProbe do
272
- describe "#call" do
273
- it "collects GC metrics" do
274
- expect(Appsignal).to receive(:set_process_gauge).at_least(8).times
275
-
276
- Appsignal::Minutely::GCProbe.new.call
277
- end
278
- end
279
- end
280
342
  end