appsignal 3.0.6-java → 3.0.7-java

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a1324bce086be01b0dfae65d0b46f4b985707ca35da77699cbe9f7335f84080
4
- data.tar.gz: 4e74a0eaac0b0e6330ecfcbd63762d80359ff880395e66644d39db4879c823a8
3
+ metadata.gz: b855b4dfbc358cac71b515acfa9060d0fe285bc166088a2ca8c350e13bb2e026
4
+ data.tar.gz: 4ea9db25ed39f99052d8e83a83513a8f75363a028895f15e09e23a18774d633c
5
5
  SHA512:
6
- metadata.gz: 6c9d4481e24413363bbfe8bfdb0486968d9b5bc45bd0e898e535762f9372e153dfa81890cc8a3f567a1a884f8ba431d5c7c1b543b61cb1eb6ced57a060372c00
7
- data.tar.gz: 1b728a26ade2bbcdedea7d454e9bb7ad548c1931a2503b79a5ed265e6d768e9f403b268f7e4984a62c5d9f6c679c08fbf6fa23d0365592b6dd2c34b68013a0dd
6
+ metadata.gz: 7c603ff374341866922386c85f3ed9ee8a842c632fa35c9be60e975afa33f8bb8262f4f7e70fce3e3b4503626825a4425481c5ee3fa259ff8aa6475aca813604
7
+ data.tar.gz: f91fcf688355c79d562fabda066473e3942aa1f174f3ce22485a43fe9aac6c530c2200b0f2be0af09a5b2568ca43e9a001d0a0293c5eaea6f989ff83027f709b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # AppSignal for Ruby gem Changelog
2
2
 
3
+ ## 3.0.7
4
+
5
+ - [27f9b178](https://github.com/appsignal/appsignal-ruby/commit/27f9b178c20006ee15e69bdf878f3a0c9975b1f4) patch - Bump agent to 6caf6d0. Replaces curl HTTP client and includes various other maintenance updates.
6
+ - [665d883a](https://github.com/appsignal/appsignal-ruby/commit/665d883a529e5c14b28e73eeb3ae6410deb3e182) patch - Improve Puma plugin stats collection. Instead of starting the AppSignal gem in the main process we send the stats to the AppSignal agent directly using StatsD. This should improve compatibility with phased restarts. If you use `prune_bundler`, you will need to add AppSignal to the extra `extra_runtime_dependencies` list.
7
+
8
+ ```
9
+ # config/puma.rb
10
+ plugin :appsignal
11
+ extra_runtime_dependencies ["appsignal"]
12
+ ```
13
+
3
14
  ## 3.0.6
4
15
 
5
16
  - [d354d79b](https://github.com/appsignal/appsignal-ruby/commit/d354d79b293fd549e66cae60d805d1b1e9e9d2d8) patch - Add Excon integration. Track requests and responses from the Excon gem.
data/README.md CHANGED
@@ -161,9 +161,13 @@ systems][supported-systems] page.
161
161
 
162
162
  Following the process below to release a new version of the Ruby gem.
163
163
 
164
- 1. Run: `rake publish`
165
- 1. Update the Ruby gem version number when prompted.
166
- 1. Update the `CHANGELOG` file with the changes since the last release.
164
+ 1. Make sure [mono](https://github.com/appsignal/mono/) is installed by
165
+ following the installation instructions.
166
+ 1. Run: `mono publish`
167
+ - Mono will automatically bump the version number based on the
168
+ `.changesets/`.
169
+ - Mono will automatically update the `CHANGELOG.md` file based on the
170
+ `.changesets/`.
167
171
  1. Confirm with your two-factor authentication token for Rubygems.org, _twice_.
168
172
 
169
173
  ## Development
@@ -176,6 +180,8 @@ install all possible dependencies.
176
180
  ```bash
177
181
  # Install Bundler
178
182
  gem install bundler
183
+ # Bootstrap the project
184
+ mono bootstrap
179
185
  # Install the AppSignal extension and _all_ gems we support.
180
186
  bundle exec rake install
181
187
  # Only install the AppSignal extension.
data/ext/agent.yml CHANGED
@@ -1,62 +1,62 @@
1
1
  ---
2
- version: 75e76ad
2
+ version: 3ecd06f
3
3
  mirrors:
4
4
  - https://appsignal-agent-releases.global.ssl.fastly.net
5
5
  - https://d135dj0rjqvssy.cloudfront.net
6
6
  triples:
7
7
  x86_64-darwin:
8
8
  static:
9
- checksum: 81edea50b934fe5b42c0081a1de5782bc477c321c1c76127d7fa525917f70a04
9
+ checksum: 39839e64832d7964fad51d9acc95374654446ade04d06cbba4d052e4483a8516
10
10
  filename: appsignal-x86_64-darwin-all-static.tar.gz
11
11
  dynamic:
12
- checksum: 60befd59ac704ee21d5881ca0aef6b38caa50b1d1972425bf8f40b4f9710ec1e
12
+ checksum: 841b82677aaf553219f689b92b6dc6988278420e2e72dcab28a6c5915a0e4158
13
13
  filename: appsignal-x86_64-darwin-all-dynamic.tar.gz
14
14
  universal-darwin:
15
15
  static:
16
- checksum: 81edea50b934fe5b42c0081a1de5782bc477c321c1c76127d7fa525917f70a04
16
+ checksum: 39839e64832d7964fad51d9acc95374654446ade04d06cbba4d052e4483a8516
17
17
  filename: appsignal-x86_64-darwin-all-static.tar.gz
18
18
  dynamic:
19
- checksum: 60befd59ac704ee21d5881ca0aef6b38caa50b1d1972425bf8f40b4f9710ec1e
19
+ checksum: 841b82677aaf553219f689b92b6dc6988278420e2e72dcab28a6c5915a0e4158
20
20
  filename: appsignal-x86_64-darwin-all-dynamic.tar.gz
21
21
  i686-linux:
22
22
  static:
23
- checksum: 60ad6a1f69c8f89b5642f49fbf794409e8ada7d63a9126b2c59832c2a92d5b22
23
+ checksum: fdf923153992816c813f938f41feadafbf80d8fa10a785123ee23184945047a0
24
24
  filename: appsignal-i686-linux-all-static.tar.gz
25
25
  dynamic:
26
- checksum: aedf259de392ea00fd17bc30924cde70d9a1d82a62c6eeefc881cd5ea5dcce63
26
+ checksum: d2b8635199a73a1769ee7495acac0cdc69c4f964fef9ec7749fde517d789e1dd
27
27
  filename: appsignal-i686-linux-all-dynamic.tar.gz
28
28
  x86-linux:
29
29
  static:
30
- checksum: 60ad6a1f69c8f89b5642f49fbf794409e8ada7d63a9126b2c59832c2a92d5b22
30
+ checksum: fdf923153992816c813f938f41feadafbf80d8fa10a785123ee23184945047a0
31
31
  filename: appsignal-i686-linux-all-static.tar.gz
32
32
  dynamic:
33
- checksum: aedf259de392ea00fd17bc30924cde70d9a1d82a62c6eeefc881cd5ea5dcce63
33
+ checksum: d2b8635199a73a1769ee7495acac0cdc69c4f964fef9ec7749fde517d789e1dd
34
34
  filename: appsignal-i686-linux-all-dynamic.tar.gz
35
35
  x86_64-linux:
36
36
  static:
37
- checksum: 7e3cf760f9bd364a6602f05e5014ce1c8e8ac9a97f7a533769711e0fb21e1f45
37
+ checksum: d81383dedb228f484e1d1e28a76ddc017d788fa62270b6e3f5a55765a4fb89d9
38
38
  filename: appsignal-x86_64-linux-all-static.tar.gz
39
39
  dynamic:
40
- checksum: 4a9a4ea22abc93c3afa7d5bfd6f442cd69bf4d672e8db2df1aa2157cfb3fcc4b
40
+ checksum: f98948600345ee4726712937b13059fe7145d9b83273fc6efc6e0fd6a255381a
41
41
  filename: appsignal-x86_64-linux-all-dynamic.tar.gz
42
42
  x86_64-linux-musl:
43
43
  static:
44
- checksum: 4d4dd00607cbe0fb4d14a15e74990f207eba90134c2d1a079b58f0d50f1ab76b
44
+ checksum: be2c414da7eb0837f23a8791b369e650aea26afbf9d3be9f530bc2de11fc888f
45
45
  filename: appsignal-x86_64-linux-musl-all-static.tar.gz
46
46
  dynamic:
47
- checksum: 067a6d821e220c9c88ceb8f936390ce458fa94f41c13d32603d773485a8cdfd2
47
+ checksum: 3d04b9dcfbfe7696e028f9205dcae08e41bd87d162f7f37d04cc3d4a7442a9ec
48
48
  filename: appsignal-x86_64-linux-musl-all-dynamic.tar.gz
49
49
  x86_64-freebsd:
50
50
  static:
51
- checksum: 176bc1ff1ad40a585ea10456a8ae3f6502c614d713dcbb957d550fa1ae44e7ca
51
+ checksum: fcbfabaa51f9004ed697e9000bfc1a7d71f48f40c04f9c9588a5c8f09cddf0ea
52
52
  filename: appsignal-x86_64-freebsd-all-static.tar.gz
53
53
  dynamic:
54
- checksum: 88c6f3e03f8f6c25a4705f7d6c4286eaa563069a9fb8fad3c0a19e5b9a09f80b
54
+ checksum: ffc261d6d2a799e9bbf1cee3f9134da60cd72cd6b47a72e8c63a5a11e4d0b472
55
55
  filename: appsignal-x86_64-freebsd-all-dynamic.tar.gz
56
56
  amd64-freebsd:
57
57
  static:
58
- checksum: 176bc1ff1ad40a585ea10456a8ae3f6502c614d713dcbb957d550fa1ae44e7ca
58
+ checksum: fcbfabaa51f9004ed697e9000bfc1a7d71f48f40c04f9c9588a5c8f09cddf0ea
59
59
  filename: appsignal-x86_64-freebsd-all-static.tar.gz
60
60
  dynamic:
61
- checksum: 88c6f3e03f8f6c25a4705f7d6c4286eaa563069a9fb8fad3c0a19e5b9a09f80b
61
+ checksum: ffc261d6d2a799e9bbf1cee3f9134da60cd72cd6b47a72e8c63a5a11e4d0b472
62
62
  filename: appsignal-x86_64-freebsd-all-dynamic.tar.gz
@@ -290,6 +290,7 @@ module Appsignal
290
290
  ENV["_APPSIGNAL_FILES_WORLD_ACCESSIBLE"] = config_hash[:files_world_accessible].to_s
291
291
  ENV["_APPSIGNAL_TRANSACTION_DEBUG_MODE"] = config_hash[:transaction_debug_mode].to_s
292
292
  ENV["_APPSIGNAL_SEND_ENVIRONMENT_METADATA"] = config_hash[:send_environment_metadata].to_s
293
+ ENV["_APPSIGNAL_ENABLE_STATSD"] = "true"
293
294
  ENV["_APP_REVISION"] = config_hash[:revision].to_s
294
295
  end
295
296
 
@@ -11,23 +11,8 @@ module Appsignal
11
11
  end
12
12
 
13
13
  def install
14
- if ::Puma.respond_to?(:stats) && !defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)
15
- # Only install the minutely probe if a user isn't using our Puma
16
- # plugin, which lives in `lib/puma/appsignal.rb`. This plugin defines
17
- # the {APPSIGNAL_PUMA_PLUGIN_LOADED} constant.
18
- #
19
- # We prefer people use the AppSignal Puma plugin. This fallback is
20
- # only there when users relied on our *magic* integration.
21
- #
22
- # Using the Puma plugin, the minutely probe thread will still run in
23
- # Puma workers, for other non-Puma probes, but the Puma probe only
24
- # runs in the Puma main process.
25
- # For more information:
26
- # https://docs.appsignal.com/ruby/integrations/puma.html
27
- Appsignal::Minutely.probes.register :puma, ::Appsignal::Probes::PumaProbe
28
- end
29
-
30
14
  return unless defined?(::Puma::Cluster)
15
+
31
16
  # For clustered mode with multiple workers
32
17
  ::Puma::Cluster.send(:prepend, Module.new do
33
18
  def stop_workers
@@ -3,5 +3,4 @@ module Appsignal
3
3
  end
4
4
  end
5
5
 
6
- require "appsignal/probes/puma"
7
6
  require "appsignal/probes/sidekiq"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "3.0.6".freeze
4
+ VERSION = "3.0.7".freeze
5
5
  end
@@ -1,27 +1,156 @@
1
- APPSIGNAL_PUMA_PLUGIN_LOADED = true
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
2
4
 
3
5
  # AppSignal Puma plugin
4
6
  #
5
- # This plugin ensures the minutely probe thread is started with the Puma
6
- # minutely probe in the Puma master process.
7
- #
8
- # The constant {APPSIGNAL_PUMA_PLUGIN_LOADED} is here to mark the Plugin as
9
- # loaded by the rest of the AppSignal gem. This ensures that the Puma minutely
10
- # probe is not also started in every Puma workers, which was the old behavior.
11
- # See {Appsignal::Hooks::PumaHook#install} for more information.
7
+ # This plugin ensures Puma metrics are sent to the AppSignal agent using StatsD.
12
8
  #
13
9
  # For even more information:
14
10
  # https://docs.appsignal.com/ruby/integrations/puma.html
15
- Puma::Plugin.create do
16
- def start(launcher = nil)
17
- launcher.events.on_booted do
18
- require "appsignal"
19
- if ::Puma.respond_to?(:stats)
20
- require "appsignal/probes/puma"
21
- Appsignal::Minutely.probes.register :puma, Appsignal::Probes::PumaProbe
11
+ Puma::Plugin.create do # rubocop:disable Metrics/BlockLength
12
+ def start(launcher)
13
+ @launcher = launcher
14
+ @launcher.events.debug "AppSignal: Puma plugin start."
15
+ in_background do
16
+ @launcher.events.debug "AppSignal: Start Puma stats collection loop."
17
+ plugin = AppsignalPumaPlugin.new
18
+
19
+ loop do
20
+ begin
21
+ # Implement similar behavior to minutely probes.
22
+ # Initial sleep to wait until the app is fully initalized.
23
+ # Then loop every 60 seconds and collect the Puma stats as AppSignal
24
+ # metrics.
25
+ sleep sleep_time
26
+
27
+ @launcher.events.debug "AppSignal: Collecting Puma stats."
28
+ stats = fetch_puma_stats
29
+ if stats
30
+ plugin.call(stats)
31
+ else
32
+ @launcher.events.log "AppSignal: No Puma stats to report."
33
+ end
34
+ rescue StandardError => error
35
+ log_error "Error while processing metrics.", error
36
+ end
22
37
  end
23
- Appsignal.start
24
- Appsignal.start_logger
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def sleep_time
44
+ 60 # seconds
45
+ end
46
+
47
+ def log_error(message, error)
48
+ @launcher.events.log "AppSignal: #{message}\n" \
49
+ "#{error.class}: #{error.message}\n#{error.backtrace.join("\n")}"
50
+ end
51
+
52
+ def fetch_puma_stats
53
+ if Puma.respond_to? :stats_hash # Puma >= 5.0.0
54
+ Puma.stats_hash
55
+ elsif Puma.respond_to? :stats # Puma < 5.0.0
56
+ # Puma.stats_hash returns symbolized keys as well
57
+ JSON.parse Puma.stats, :symbolize_names => true
58
+ end
59
+ rescue StandardError => error
60
+ log_error "Error while parsing Puma stats.", error
61
+ nil
62
+ end
63
+ end
64
+
65
+ # AppsignalPumaPlugin
66
+ #
67
+ # Class to handle the logic of translating the Puma stats to AppSignal metrics.
68
+ #
69
+ # @api private
70
+ class AppsignalPumaPlugin
71
+ def initialize
72
+ @hostname = fetch_hostname
73
+ @statsd = Statsd.new
74
+ end
75
+
76
+ def call(stats)
77
+ counts = {}
78
+ count_keys = [:backlog, :running, :pool_capacity, :max_threads]
79
+
80
+ if stats[:worker_status] # Clustered mode - Multiple workers
81
+ stats[:worker_status].each do |worker|
82
+ stat = worker[:last_status]
83
+ count_keys.each do |key|
84
+ count_if_present counts, key, stat
85
+ end
86
+ end
87
+
88
+ gauge(:workers, stats[:workers], :type => :count)
89
+ gauge(:workers, stats[:booted_workers], :type => :booted)
90
+ gauge(:workers, stats[:old_workers], :type => :old)
91
+ else # Single mode - Single worker
92
+ count_keys.each do |key|
93
+ count_if_present counts, key, stats
94
+ end
95
+ end
96
+
97
+ gauge(:connection_backlog, counts[:backlog]) if counts[:backlog]
98
+ gauge(:pool_capacity, counts[:pool_capacity]) if counts[:pool_capacity]
99
+ gauge(:threads, counts[:running], :type => :running) if counts[:running]
100
+ gauge(:threads, counts[:max_threads], :type => :max) if counts[:max_threads]
101
+ end
102
+
103
+ private
104
+
105
+ attr_reader :hostname
106
+
107
+ def fetch_hostname
108
+ # Configure hostname as reported for the Puma metrics with the
109
+ # APPSIGNAL_HOSTNAME environment variable.
110
+ env_hostname = ENV["APPSIGNAL_HOSTNAME"]
111
+ return env_hostname if env_hostname
112
+
113
+ # Auto detect hostname as fallback. May be inaccurate.
114
+ Socket.gethostname
115
+ end
116
+
117
+ def gauge(field, count, tags = {})
118
+ @statsd.gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
119
+ end
120
+
121
+ def count_if_present(counts, key, stats)
122
+ stat_value = stats[key]
123
+ return unless stat_value
124
+
125
+ counts[key] ||= 0
126
+ counts[key] += stat_value
127
+ end
128
+
129
+ class Statsd
130
+ def initialize
131
+ # StatsD server location as configured in AppSignal agent StatsD server.
132
+ @host = "127.0.0.1"
133
+ @port = 8125
134
+ end
135
+
136
+ def gauge(metric_name, value, tags)
137
+ send_metric "g", metric_name, value, tags
138
+ end
139
+
140
+ private
141
+
142
+ attr_reader :host, :port
143
+
144
+ def send_metric(type, metric_name, metric_value, tags_hash)
145
+ tags = tags_hash.map { |key, value| "#{key}:#{value}" }.join(",")
146
+ data = "#{metric_name}:#{metric_value}|#{type}|##{tags}"
147
+
148
+ # Open (and close) a new socket every time because we don't know when the
149
+ # plugin will exit and when to cleanly close the socket connection.
150
+ socket = UDPSocket.new
151
+ socket.send(data, 0, host, port)
152
+ ensure
153
+ socket && socket.close
25
154
  end
26
155
  end
27
156
  end
@@ -35,29 +35,6 @@ describe Appsignal::Hooks::PumaHook do
35
35
  # Does not error on call
36
36
  Appsignal::Hooks::PumaHook.new.install
37
37
  end
38
-
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 }
45
-
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
51
-
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
55
-
56
- Appsignal::Hooks::PumaHook.new.install
57
- probe = Appsignal::Minutely.probes[:puma]
58
- expect(probe).to eql(Appsignal::Probes::PumaProbe)
59
- end
60
- end
61
38
  end
62
39
 
63
40
  context "when in clustered mode" do
@@ -81,29 +58,6 @@ describe Appsignal::Hooks::PumaHook do
81
58
  cluster.stop_workers
82
59
  expect(cluster.instance_variable_get(:@called)).to be(true)
83
60
  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
97
-
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::Probes::PumaProbe)
105
- end
106
- end
107
61
  end
108
62
  end
109
63
  end
@@ -1,6 +1,4 @@
1
1
  RSpec.describe "Puma plugin" do
2
- include WaitForHelper
3
-
4
2
  class MockPumaLauncher
5
3
  def events
6
4
  return @events if defined?(@events)
@@ -10,110 +8,288 @@ RSpec.describe "Puma plugin" do
10
8
  end
11
9
 
12
10
  class MockPumaEvents
13
- def on_booted(&block)
14
- @on_booted = block if block_given?
15
- @on_booted if defined?(@on_booted)
11
+ attr_reader :logs
12
+
13
+ def initialize
14
+ @logs = []
15
+ end
16
+
17
+ def log(message)
18
+ @logs << [:log, message]
19
+ end
20
+
21
+ def debug(message)
22
+ @logs << [:debug, message]
23
+ end
24
+
25
+ def error(message)
26
+ @logs << [:error, message]
27
+ end
28
+ end
29
+
30
+ # StatsD server used for these tests.
31
+ # Open a UDPSocket and listen for messages sent by the AppSignal Puma plugin.
32
+ class StatsdServer
33
+ def start
34
+ stop
35
+ @socket = UDPSocket.new
36
+ @socket.bind("127.0.0.1", 8125)
37
+
38
+ loop do
39
+ begin
40
+ # Listen for messages and track them on the messages Array.
41
+ packet = @socket.recvfrom(1024)
42
+ track_message packet.first
43
+ rescue Errno::EBADF # rubocop:disable Lint/HandleExceptions
44
+ # Ignore error for JRuby 9.1.17.0 specifically, it doesn't appear to
45
+ # happen on 9.2.18.0. It doesn't break the tests themselves, ignoring
46
+ # this error. It's probably a timing issue where it tries to read
47
+ # from the socket after it's closed.
48
+ end
49
+ end
50
+ end
51
+
52
+ def stop
53
+ @socket && @socket.close
54
+ ensure
55
+ @socket = nil
56
+ end
57
+
58
+ def messages
59
+ @messages ||= []
60
+ end
61
+
62
+ private
63
+
64
+ def track_message(message)
65
+ @messages_mutex ||= Mutex.new
66
+ @messages_mutex.synchronize { messages << message }
16
67
  end
17
68
  end
18
69
 
19
70
  let(:probe) { MockProbe.new }
20
71
  let(:launcher) { MockPumaLauncher.new }
72
+ let(:hostname) { Socket.gethostname }
73
+ let(:expected_default_tags) { { "hostname" => hostname } }
74
+ let(:stats_data) { { :backlog => 1 } }
21
75
  before do
22
76
  module Puma
23
77
  def self.stats
78
+ JSON.dump(@_stats_data)
79
+ end
80
+
81
+ def self.stats_hash
82
+ @_stats_data
24
83
  end
25
84
 
26
- def self.run
27
- # Capture threads running before application is preloaded
28
- before = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
29
-
30
- # An abbreviated version of what happens in Puma::Cluster#run
31
- launcher = MockPumaLauncher.new
32
- plugin = Plugin.plugin.new
33
- plugin.start(launcher)
34
- launcher.events.on_booted.call
35
-
36
- # Wait for minutely probe thread to finish starting
37
- sleep 0.005
38
-
39
- # Capture any new threads running after application is preloaded.
40
- # Any threads created during the preloading phase will not be
41
- # carried over into the forked workers. Puma warns about these
42
- # but the minutely probe thread should only exist in the main process.
43
- after = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
44
- $stdout.puts "! WARNING: Detected #{after.size - before.size} Thread(s) started in app boot" if after.size > before.size
85
+ def self._set_stats=(data)
86
+ @_stats_data = data
45
87
  end
46
88
 
47
89
  class Plugin
48
90
  class << self
49
- attr_reader :plugin
91
+ attr_reader :appsignal_plugin
50
92
 
51
93
  def create(&block)
52
- @plugin = Class.new(::Puma::Plugin)
53
- @plugin.class_eval(&block)
94
+ @appsignal_plugin = Class.new(::Puma::Plugin)
95
+ @appsignal_plugin.class_eval(&block)
54
96
  end
55
97
  end
56
- end
57
- end
58
98
 
59
- Appsignal::Minutely.probes.clear
60
- ENV["APPSIGNAL_ENABLE_MINUTELY_PROBES"] = "true"
61
- Appsignal.config = project_fixture_config
62
- # Speed up test time
63
- allow(Appsignal::Minutely).to receive(:initial_wait_time).and_return(0.001)
64
- allow(Appsignal::Minutely).to receive(:wait_time).and_return(0.001)
99
+ attr_reader :in_background_block
65
100
 
66
- Appsignal::Minutely.probes.register :my_probe, probe
101
+ def in_background(&block)
102
+ @in_background_block = block
103
+ end
104
+ end
105
+ end
106
+ Puma._set_stats = stats_data
67
107
  load File.expand_path("../lib/puma/plugin/appsignal.rb", APPSIGNAL_SPEC_DIR)
108
+
109
+ @statsd = StatsdServer.new
110
+ @server_thread = Thread.new { @statsd.start }
111
+ @server_thread.abort_on_exception = true
68
112
  end
69
113
  after do
70
- Appsignal.config = nil
71
- Object.send :remove_const, :Puma
72
- Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED
114
+ @statsd = nil
115
+
116
+ Object.send(:remove_const, :Puma)
117
+ Object.send(:remove_const, :AppsignalPumaPlugin)
118
+ end
119
+
120
+ def run(plugin)
121
+ @client_thread = Thread.new { start_plugin(plugin) }
122
+ @client_thread.abort_on_exception = true
123
+ sleep 0.03
124
+ ensure
125
+ stop_all
73
126
  end
74
127
 
75
- it "registers the PumaProbe" do
76
- expect(Appsignal::Minutely.probes[:my_probe]).to eql(probe)
77
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
78
- plugin = Puma::Plugin.plugin.new
79
- expect(launcher.events.on_booted).to be_nil
128
+ def appsignal_plugin
129
+ Puma::Plugin.appsignal_plugin
130
+ end
80
131
 
132
+ def start_plugin(plugin_class)
133
+ plugin = plugin_class.new
134
+ # Speed up test by not waiting for 60 seconds initial wait time and loop
135
+ # interval.
136
+ allow(plugin).to receive(:sleep_time).and_return(0.01)
81
137
  plugin.start(launcher)
82
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
83
- expect(launcher.events.on_booted).to_not be_nil
138
+ plugin.in_background_block.call
139
+ end
140
+
141
+ # Stop all threads in test and stop listening on the UDPSocket
142
+ def stop_all
143
+ @client_thread.kill if defined?(@client_thread) && @client_thread
144
+ @server_thread.kill if defined?(@server_thread) && @server_thread
145
+ @statsd.stop if defined?(@statsd) && @statsd
146
+ @client_thread = nil
147
+ @server_thread = nil
148
+ end
149
+
150
+ def logs
151
+ launcher.events.logs
152
+ end
153
+
154
+ def messages
155
+ @statsd.messages.map do |message|
156
+ metric, type, tags_string = message.split("|")
157
+ metric_name, metric_value = metric.split(":")
158
+ tags = {}
159
+ tags_string[1..-1].split(",").each do |tag|
160
+ key, value = tag.split(":")
161
+ tags[key] = value
162
+ end
163
+ {
164
+ :name => metric_name,
165
+ :value => metric_value.to_i,
166
+ :type => type,
167
+ :tags => tags
168
+ }
169
+ end
170
+ end
171
+
172
+ def expect_gauge(metric_name, metric_value, tags_hash = {})
173
+ expect(messages).to include(
174
+ :name => "puma_#{metric_name}",
175
+ :value => metric_value,
176
+ :type => "g",
177
+ :tags => expected_default_tags.merge(tags_hash)
178
+ )
179
+ end
180
+
181
+ context "with multiple worker stats" do
182
+ let(:stats_data) do
183
+ {
184
+ :workers => 2,
185
+ :booted_workers => 2,
186
+ :old_workers => 0,
187
+ :worker_status => [
188
+ {
189
+ :last_status => {
190
+ :backlog => 0,
191
+ :running => 5,
192
+ :pool_capacity => 5,
193
+ :max_threads => 5
194
+ }
195
+ },
196
+ {
197
+ :last_status => {
198
+ :backlog => 0,
199
+ :running => 5,
200
+ :pool_capacity => 5,
201
+ :max_threads => 5
202
+ }
203
+ }
204
+ ]
205
+ }
206
+ end
207
+
208
+ it "collects puma stats as guage metrics with the (summed) worker metrics" do
209
+ run(appsignal_plugin)
210
+
211
+ expect(logs).to_not include([:error, kind_of(String)])
212
+ expect_gauge(:workers, 2, "type" => "count")
213
+ expect_gauge(:workers, 2, "type" => "booted")
214
+ expect_gauge(:workers, 0, "type" => "old")
215
+ expect_gauge(:connection_backlog, 0)
216
+ expect_gauge(:pool_capacity, 10)
217
+ expect_gauge(:threads, 10, "type" => "running")
218
+ expect_gauge(:threads, 10, "type" => "max")
219
+ end
220
+ end
221
+
222
+ context "with single worker stats" do
223
+ let(:stats_data) do
224
+ {
225
+ :backlog => 0,
226
+ :running => 5,
227
+ :pool_capacity => 5,
228
+ :max_threads => 5
229
+ }
230
+ end
231
+
232
+ it "calls `puma_gauge` with the (summed) worker metrics" do
233
+ run(appsignal_plugin)
234
+
235
+ expect(logs).to_not include([:error, kind_of(String)])
236
+ expect_gauge(:connection_backlog, 0)
237
+ expect_gauge(:pool_capacity, 5)
238
+ expect_gauge(:threads, 5, "type" => "running")
239
+ expect_gauge(:threads, 5, "type" => "max")
240
+ end
241
+ end
84
242
 
85
- launcher.events.on_booted.call
86
- expect(Appsignal::Minutely.probes[:puma]).to eql(Appsignal::Probes::PumaProbe)
243
+ context "when using APPSIGNAL_HOSTNAME" do
244
+ let(:hostname) { "my-host-name" }
245
+ before { ENV["APPSIGNAL_HOSTNAME"] = hostname }
246
+ after { ENV.delete("APPSIGNAL_HOSTNAME") }
87
247
 
88
- # Minutely probes started and called
89
- wait_for("enough probe calls") { probe.calls >= 2 }
248
+ it "reports the APPSIGNAL_HOSTNAME as the hostname tag value" do
249
+ run(appsignal_plugin)
250
+
251
+ expect(logs).to_not include([:error, kind_of(String)])
252
+ expect_gauge(:connection_backlog, 1)
253
+ end
90
254
  end
91
255
 
92
- it "marks the PumaProbe thread as fork-safe" do
93
- out_stream = std_stream
94
- capture_stdout(out_stream) { Puma.run }
256
+ context "without Puma.stats_hash" do
257
+ before do
258
+ Puma.singleton_class.send(:remove_method, :stats_hash)
259
+ end
260
+
261
+ it "fetches metrics from Puma.stats instead" do
262
+ run(appsignal_plugin)
95
263
 
96
- expect(out_stream.read).not_to include("WARNING: Detected 1 Thread")
264
+ expect(logs).to_not include([:error, kind_of(String)])
265
+ expect(logs).to_not include([kind_of(Symbol), "AppSignal: No Puma stats to report."])
266
+ expect_gauge(:connection_backlog, 1)
267
+ end
97
268
  end
98
269
 
99
- context "without Puma.stats" do
100
- before { Puma.singleton_class.send(:remove_method, :stats) }
270
+ context "without Puma.stats and Puma.stats_hash" do
271
+ before do
272
+ Puma.singleton_class.send(:remove_method, :stats)
273
+ Puma.singleton_class.send(:remove_method, :stats_hash)
274
+ end
101
275
 
102
- it "does not register the PumaProbe" do
103
- expect(Appsignal::Minutely.probes[:my_probe]).to eql(probe)
104
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
105
- plugin = Puma::Plugin.plugin.new
106
- expect(launcher.events.on_booted).to be_nil
276
+ it "does not fetch metrics" do
277
+ run(appsignal_plugin)
107
278
 
108
- plugin.start(launcher)
109
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
110
- expect(launcher.events.on_booted).to_not be_nil
279
+ expect(logs).to_not include([:error, kind_of(String)])
280
+ expect(logs).to include([:log, "AppSignal: No Puma stats to report."])
281
+ expect(messages).to be_empty
282
+ end
283
+ end
111
284
 
112
- launcher.events.on_booted.call
113
- expect(Appsignal::Minutely.probes[:puma]).to be_nil
285
+ context "without running StatsD server" do
286
+ it "does nothing" do
287
+ Appsignal.stop
288
+ stop_all
289
+ run(appsignal_plugin)
114
290
 
115
- # Minutely probes started and called
116
- wait_for("enough probe calls") { probe.calls >= 2 }
291
+ expect(logs).to_not include([:error, kind_of(String)])
292
+ expect(messages).to be_empty
117
293
  end
118
294
  end
119
295
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appsignal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.6
4
+ version: 3.0.7
5
5
  platform: java
6
6
  authors:
7
7
  - Robert Beekman
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-05-31 00:00:00.000000000 Z
13
+ date: 2021-06-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -263,7 +263,6 @@ files:
263
263
  - lib/appsignal/marker.rb
264
264
  - lib/appsignal/minutely.rb
265
265
  - lib/appsignal/probes.rb
266
- - lib/appsignal/probes/puma.rb
267
266
  - lib/appsignal/probes/sidekiq.rb
268
267
  - lib/appsignal/rack/generic_instrumentation.rb
269
268
  - lib/appsignal/rack/rails_instrumentation.rb
@@ -349,7 +348,6 @@ files:
349
348
  - spec/lib/appsignal/logger_spec.rb
350
349
  - spec/lib/appsignal/marker_spec.rb
351
350
  - spec/lib/appsignal/minutely_spec.rb
352
- - spec/lib/appsignal/probes/puma_spec.rb
353
351
  - spec/lib/appsignal/probes/sidekiq_spec.rb
354
352
  - spec/lib/appsignal/rack/generic_instrumentation_spec.rb
355
353
  - spec/lib/appsignal/rack/rails_instrumentation_spec.rb
@@ -498,7 +496,6 @@ test_files:
498
496
  - spec/lib/appsignal/logger_spec.rb
499
497
  - spec/lib/appsignal/marker_spec.rb
500
498
  - spec/lib/appsignal/minutely_spec.rb
501
- - spec/lib/appsignal/probes/puma_spec.rb
502
499
  - spec/lib/appsignal/probes/sidekiq_spec.rb
503
500
  - spec/lib/appsignal/rack/generic_instrumentation_spec.rb
504
501
  - spec/lib/appsignal/rack/rails_instrumentation_spec.rb
@@ -1,61 +0,0 @@
1
- module Appsignal
2
- module Probes
3
- class PumaProbe
4
- def initialize
5
- @hostname = Appsignal.config[:hostname] || Socket.gethostname
6
- end
7
-
8
- # @api private
9
- def call
10
- puma_stats = fetch_puma_stats
11
- return unless puma_stats
12
-
13
- stats = JSON.parse puma_stats, :symbolize_names => true
14
- counts = {}
15
- count_keys = [:backlog, :running, :pool_capacity, :max_threads]
16
-
17
- if stats[:worker_status] # Multiple workers
18
- stats[:worker_status].each do |worker|
19
- stat = worker[:last_status]
20
- count_keys.each do |key|
21
- count_if_present counts, key, stat
22
- end
23
- end
24
-
25
- gauge(:workers, stats[:workers], :type => :count)
26
- gauge(:workers, stats[:booted_workers], :type => :booted)
27
- gauge(:workers, stats[:old_workers], :type => :old)
28
- else # Single worker
29
- count_keys.each do |key|
30
- count_if_present counts, key, stats
31
- end
32
- end
33
-
34
- gauge(:connection_backlog, counts[:backlog]) if counts[:backlog]
35
- gauge(:pool_capacity, counts[:pool_capacity]) if counts[:pool_capacity]
36
- gauge(:threads, counts[:running], :type => :running) if counts[:running]
37
- gauge(:threads, counts[:max_threads], :type => :max) if counts[:max_threads]
38
- end
39
-
40
- private
41
-
42
- attr_reader :hostname
43
-
44
- def gauge(field, count, tags = {})
45
- Appsignal.set_gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
46
- end
47
-
48
- def count_if_present(counts, key, stats)
49
- stat_value = stats[key]
50
- return unless stat_value
51
- counts[key] ||= 0
52
- counts[key] += stat_value
53
- end
54
-
55
- def fetch_puma_stats
56
- ::Puma.stats
57
- rescue NoMethodError # rubocop:disable Lint/HandleExceptions
58
- end
59
- end
60
- end
61
- end
@@ -1,180 +0,0 @@
1
- require "appsignal/probes/puma"
2
-
3
- describe Appsignal::Probes::PumaProbe do
4
- before(:context) do
5
- Appsignal.config = project_fixture_config
6
- end
7
- after(:context) do
8
- Appsignal.config = nil
9
- end
10
-
11
- let(:probe) { described_class.new }
12
-
13
- describe "hostname" do
14
- it "returns the socket hostname" do
15
- expect(probe.send(:hostname)).to eql(Socket.gethostname)
16
- end
17
-
18
- context "with overridden hostname" do
19
- around do |sample|
20
- Appsignal.config[:hostname] = "frontend1"
21
- sample.run
22
- Appsignal.config[:hostname] = nil
23
- end
24
- it "returns the configured host" do
25
- expect(probe.send(:hostname)).to eql("frontend1")
26
- end
27
- end
28
- end
29
-
30
- describe "#call" do
31
- let(:expected_default_tags) { { :hostname => Socket.gethostname } }
32
-
33
- context "with multiple worker stats" do
34
- before(:context) do
35
- class Puma
36
- def self.stats
37
- {
38
- "workers" => 2,
39
- "booted_workers" => 2,
40
- "old_workers" => 0,
41
- "worker_status" => [
42
- {
43
- "last_status" => {
44
- "backlog" => 0,
45
- "running" => 5,
46
- "pool_capacity" => 5,
47
- "max_threads" => 5
48
- }
49
- },
50
- {
51
- "last_status" => {
52
- "backlog" => 0,
53
- "running" => 5,
54
- "pool_capacity" => 5,
55
- "max_threads" => 5
56
- }
57
- }
58
- ]
59
- }.to_json
60
- end
61
- end
62
- end
63
- after(:context) { Object.send(:remove_const, :Puma) }
64
-
65
- it "calls `puma_gauge` with the (summed) worker metrics" do
66
- expect_gauge(:workers, 2, :type => :count)
67
- expect_gauge(:workers, 2, :type => :booted)
68
- expect_gauge(:workers, 0, :type => :old)
69
-
70
- expect_gauge(:connection_backlog, 0)
71
- expect_gauge(:pool_capacity, 10)
72
- expect_gauge(:threads, 10, :type => :running)
73
- expect_gauge(:threads, 10, :type => :max)
74
-
75
- probe.call
76
- end
77
- end
78
-
79
- context "with single worker stats" do
80
- before(:context) do
81
- class Puma
82
- def self.stats
83
- {
84
- "backlog" => 0,
85
- "running" => 5,
86
- "pool_capacity" => 5,
87
- "max_threads" => 5
88
- }.to_json
89
- end
90
- end
91
- end
92
- after(:context) { Object.send(:remove_const, :Puma) }
93
-
94
- it "calls `puma_gauge` with the (summed) worker metrics" do
95
- expect_gauge(:connection_backlog, 0)
96
- expect_gauge(:pool_capacity, 5)
97
- expect_gauge(:threads, 5, :type => :running)
98
- expect_gauge(:threads, 5, :type => :max)
99
- probe.call
100
- end
101
- end
102
-
103
- context "without stats" do
104
- before(:context) do
105
- class Puma
106
- def self.stats
107
- end
108
- end
109
- end
110
- after(:context) { Object.send(:remove_const, :Puma) }
111
-
112
- context "when it returns nil" do
113
- it "does not track metrics" do
114
- expect(probe).to_not receive(:puma_gauge)
115
- probe.call
116
- end
117
- end
118
-
119
- # Puma.stats raises a NoMethodError on a nil object on the first call.
120
- context "when it returns a NoMethodError on the first call" do
121
- let(:log) { StringIO.new }
122
-
123
- it "ignores the first call and tracks the second call" do
124
- use_logger_with log do
125
- expect(Puma).to receive(:stats)
126
- .and_raise(NoMethodError.new("undefined method `stats' for nil:NilClass"))
127
- probe.call
128
-
129
- expect(Puma).to receive(:stats).and_return({
130
- "backlog" => 1,
131
- "running" => 5,
132
- "pool_capacity" => 4,
133
- "max_threads" => 6
134
- }.to_json)
135
-
136
- expect_gauge(:connection_backlog, 1)
137
- expect_gauge(:pool_capacity, 4)
138
- expect_gauge(:threads, 5, :type => :running)
139
- expect_gauge(:threads, 6, :type => :max)
140
- probe.call
141
- end
142
-
143
- expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
144
- end
145
- end
146
-
147
- context "when it does not have a complete stats payload" do
148
- let(:log) { StringIO.new }
149
-
150
- it "tracks whatever metrics we do have" do
151
- use_logger_with log do
152
- expect(Puma).to receive(:stats).and_return({
153
- "backlog" => 1,
154
- "running" => 5
155
- }.to_json)
156
-
157
- expect_gauge(:connection_backlog, 1)
158
- expect_no_gauge(:pool_capacity)
159
- expect_gauge(:threads, 5, :type => :running)
160
- expect_no_gauge(:threads, :type => :max)
161
- probe.call
162
- end
163
-
164
- expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
165
- end
166
- end
167
- end
168
-
169
- def expect_gauge(key, value, tags = {})
170
- expect(Appsignal).to receive(:set_gauge)
171
- .with("puma_#{key}", value, expected_default_tags.merge(tags))
172
- .and_call_original
173
- end
174
-
175
- def expect_no_gauge(key, tags = {})
176
- expect(Appsignal).to_not receive(:set_gauge)
177
- .with("puma_#{key}", anything, tags)
178
- end
179
- end
180
- end