appsignal 2.9.2.alpha.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 (76) 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 +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/event_formatter/action_view/render_formatter.rb +10 -8
  36. data/lib/appsignal/helpers/instrumentation.rb +18 -9
  37. data/lib/appsignal/helpers/metrics.rb +0 -1
  38. data/lib/appsignal/hooks.rb +3 -1
  39. data/lib/appsignal/hooks/active_support_notifications.rb +2 -5
  40. data/lib/appsignal/hooks/puma.rb +15 -13
  41. data/lib/appsignal/hooks/sequel.rb +1 -1
  42. data/lib/appsignal/hooks/sidekiq.rb +33 -8
  43. data/lib/appsignal/integrations/que.rb +9 -8
  44. data/lib/appsignal/minutely.rb +38 -19
  45. data/lib/appsignal/transaction.rb +5 -0
  46. data/lib/appsignal/utils/rails_helper.rb +4 -0
  47. data/lib/appsignal/version.rb +1 -1
  48. data/lib/puma/plugin/appsignal.rb +26 -0
  49. data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +40 -0
  50. data/spec/lib/appsignal/cli/install_spec.rb +51 -7
  51. data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +10 -0
  52. data/spec/lib/appsignal/config_spec.rb +10 -8
  53. data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +38 -28
  54. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +104 -25
  55. data/spec/lib/appsignal/hooks/puma_spec.rb +69 -39
  56. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +65 -3
  57. data/spec/lib/appsignal/hooks_spec.rb +4 -0
  58. data/spec/lib/appsignal/minutely_spec.rb +150 -88
  59. data/spec/lib/appsignal/transaction_spec.rb +27 -4
  60. data/spec/lib/appsignal_spec.rb +62 -11
  61. data/spec/lib/puma/appsignal_spec.rb +91 -0
  62. data/spec/support/{project_fixture → fixtures/projects/valid}/config/application.rb +0 -0
  63. data/spec/support/{project_fixture → fixtures/projects/valid}/config/appsignal.yml +0 -0
  64. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/development.rb +0 -0
  65. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/production.rb +0 -0
  66. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/test.rb +0 -0
  67. data/spec/support/{project_fixture → fixtures/projects/valid}/log/.gitkeep +0 -0
  68. data/spec/support/helpers/config_helpers.rb +1 -1
  69. data/spec/support/helpers/wait_for_helper.rb +28 -0
  70. data/spec/support/mocks/mock_probe.rb +11 -0
  71. metadata +37 -30
  72. data/spec/support/fixtures/containers/cgroups/docker +0 -14
  73. data/spec/support/fixtures/containers/cgroups/docker_systemd +0 -8
  74. data/spec/support/fixtures/containers/cgroups/lxc +0 -10
  75. data/spec/support/fixtures/containers/cgroups/no_permission +0 -0
  76. 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