appsignal 2.11.3 → 2.11.8

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: 140c8dc48aa610503d589cbadbd8080234498628f73570eb9dec1fe40d43e7c6
4
- data.tar.gz: 9d3bda2c23f46391752218d6a08290e19ffe59a9cdabd3d89841f208a5a4499c
3
+ metadata.gz: 8cb57893fb10d1d54593fcf13b4d2b13ac728e9d0c5c16f046580b1befb6048d
4
+ data.tar.gz: ef465afb88df2f8f53015e84192159d464c4827e66a1ee19bed38b73712e810c
5
5
  SHA512:
6
- metadata.gz: 1496d2e2ab87742b5ede043d513a290aa95679445c7dd5a1f041c90bb2e05fc983e558733b974a60ed880c1db3ac9faabfc431ed4c3e1cf68fad150dd5cd4e61
7
- data.tar.gz: 62052287406db0bb8e95c6e9130886f9db8f7e9822b9d835a9faec3090facd4990addf13b99f107a26e777a37354bbe8ea795c392917cfe2c0884772000b0b3c
6
+ metadata.gz: 663ba5e66fd1bad997a27c164d7e9845effb7d419b6b23a3b33a1f0f71f50d18071194f75216e347956f11e3ad2730f82e7c24fb4029b2c9daf4bd597c289450
7
+ data.tar.gz: e7a8ba54779115a77703090837a9814e017884607d3b059b620a875cba9c52bc449fa56006ad443ecc076d1bf8bfc7b8c109bd7d61c811c8f5748e683b94f3d4
data/.rubocop.yml CHANGED
@@ -13,6 +13,8 @@ AllCops:
13
13
  - "gemfiles/vendor/**/*"
14
14
  - "vendor/**/*"
15
15
  - "benchmark.rake"
16
+ - "lib/appsignal/integrations/object_ruby_modern.rb"
17
+ - "spec/lib/appsignal/integrations/object_spec.rb"
16
18
  DisplayCopNames: true
17
19
  UseCache: true
18
20
  CacheRootDirectory: ./tmp
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ # 2.11.8
4
+ - Mark minutely probe thread as fork-safe by @pixeltrix. PR #704
5
+
6
+ # 2.11.7
7
+ - Fix ActionCable integration in test environment using `stub_connection`.
8
+ PR #705
9
+
10
+ # 2.11.6
11
+ - Prepend Sidekiq middleware to wrap all Sidekiq middleware. Catches more
12
+ errors and provide more complete performance measurements. PR #698
13
+
14
+ # 2.11.5
15
+ - Add more detailed logging to finish_event calls when the event is unknown, so
16
+ we know what event is being tried to finish. Commit
17
+ c888a04d1b9ac947652b29c111c650fb5a5cf71c
18
+
19
+ # 2.11.4
20
+ - Support Ruby 3.0 for Object method instrumentation with keyword arguments
21
+ (https://docs.appsignal.com/ruby/instrumentation/method-instrumentation.html)
22
+ PR #693
23
+
3
24
  # 2.11.3
4
25
  - Support Shoryuken batch workers. PR #687
5
26
 
data/Rakefile CHANGED
@@ -275,7 +275,8 @@ namespace :publish do
275
275
  Dir.chdir("#{File.dirname(__FILE__)}/pkg") do
276
276
  Dir["*.gem"].each do |gem_package|
277
277
  puts "## Publishing gem package: #{gem_package}"
278
- system "gem push #{gem_package}"
278
+ result = system "gem push #{gem_package}"
279
+ raise "Failed to Push gem" unless result
279
280
  end
280
281
  end
281
282
  end
@@ -374,19 +375,21 @@ end
374
375
  begin
375
376
  require "rspec/core/rake_task"
376
377
  is_jruby = defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
377
- unless is_jruby
378
- jruby_opts = "--exclude-pattern=spec/lib/appsignal/extension/jruby_spec.rb"
379
- end
378
+ excludes = []
379
+ excludes << "spec/lib/appsignal/extension/jruby_spec.rb" unless is_jruby
380
+ is_ruby19 = RUBY_VERSION < "2.0"
381
+ excludes << "spec/lib/appsignal/integrations/object_spec.rb" if is_ruby19
382
+ exclude_pattern = "--exclude-pattern=#{excludes.join(",")}" if excludes.any?
380
383
 
381
384
  desc "Run the AppSignal gem test suite."
382
385
  RSpec::Core::RakeTask.new :test do |t|
383
- t.rspec_opts = jruby_opts
386
+ t.rspec_opts = exclude_pattern
384
387
  end
385
388
 
386
389
  namespace :test do
387
390
  desc "Run the Appsignal gem test in an extension failure scenario"
388
391
  RSpec::Core::RakeTask.new :failure do |t|
389
- t.rspec_opts = "#{jruby_opts} --tag extension_installation_failure"
392
+ t.rspec_opts = "#{exclude_pattern} --tag extension_installation_failure"
390
393
  end
391
394
  end
392
395
  rescue LoadError # rubocop:disable Lint/HandleExceptions
data/ext/agent.yml CHANGED
@@ -1,62 +1,62 @@
1
1
  ---
2
- version: 361340a
2
+ version: d98461b
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: 4e08cb0cef0ea7e30f8d507380b923f6cfa14adaea12c81804e118acd6395b57
9
+ checksum: 178ab2329c7b29cf45140e4707e75c20379fa0c7dfd7f39266a7a95aea510780
10
10
  filename: appsignal-x86_64-darwin-all-static.tar.gz
11
11
  dynamic:
12
- checksum: 1a9c3e26bd453fe60a2f511d536e64bdeddb1f939664bda90d6a41eaeedf5250
12
+ checksum: '0923985cc78c5cf278f45d530679954b94b1a91c87453369116fc05525e29864'
13
13
  filename: appsignal-x86_64-darwin-all-dynamic.tar.gz
14
14
  universal-darwin:
15
15
  static:
16
- checksum: 4e08cb0cef0ea7e30f8d507380b923f6cfa14adaea12c81804e118acd6395b57
16
+ checksum: 178ab2329c7b29cf45140e4707e75c20379fa0c7dfd7f39266a7a95aea510780
17
17
  filename: appsignal-x86_64-darwin-all-static.tar.gz
18
18
  dynamic:
19
- checksum: 1a9c3e26bd453fe60a2f511d536e64bdeddb1f939664bda90d6a41eaeedf5250
19
+ checksum: '0923985cc78c5cf278f45d530679954b94b1a91c87453369116fc05525e29864'
20
20
  filename: appsignal-x86_64-darwin-all-dynamic.tar.gz
21
21
  i686-linux:
22
22
  static:
23
- checksum: 01c027b3e472cb39d844284fcc8ba532628c00731b912e0e9718646ed124ae6e
23
+ checksum: cb772a8a178edb25d666b650efda80149c98e80e4264435d6176f8a6104f959a
24
24
  filename: appsignal-i686-linux-all-static.tar.gz
25
25
  dynamic:
26
- checksum: 30696eac3ae5646bcb21ff86a1824dd4511a41dd323514e4a97a41b5ff09e2f8
26
+ checksum: c9ac51f4d1b3cc13773d8fa8ea5cfad6af7909144d85186cef9324ec0531bdac
27
27
  filename: appsignal-i686-linux-all-dynamic.tar.gz
28
28
  x86-linux:
29
29
  static:
30
- checksum: 01c027b3e472cb39d844284fcc8ba532628c00731b912e0e9718646ed124ae6e
30
+ checksum: cb772a8a178edb25d666b650efda80149c98e80e4264435d6176f8a6104f959a
31
31
  filename: appsignal-i686-linux-all-static.tar.gz
32
32
  dynamic:
33
- checksum: 30696eac3ae5646bcb21ff86a1824dd4511a41dd323514e4a97a41b5ff09e2f8
33
+ checksum: c9ac51f4d1b3cc13773d8fa8ea5cfad6af7909144d85186cef9324ec0531bdac
34
34
  filename: appsignal-i686-linux-all-dynamic.tar.gz
35
35
  x86_64-linux:
36
36
  static:
37
- checksum: fe2038d6fa468fc23900fea6d8179d2b37d41d54f4ff33c573116183ac1cb491
37
+ checksum: d6c280e992d74f97d59da9827ec5707ca4f6776b0568cde1c083c1113e4b7104
38
38
  filename: appsignal-x86_64-linux-all-static.tar.gz
39
39
  dynamic:
40
- checksum: bcbf9546b078fb8bbaae5e7df4ed84831346c814bc865a47d32a3a095a84acbb
40
+ checksum: ef1a3f5d4b2ed61ea2ae4d5cb1a261a0e685fb9d3e7ea9efe455498aad386e59
41
41
  filename: appsignal-x86_64-linux-all-dynamic.tar.gz
42
42
  x86_64-linux-musl:
43
43
  static:
44
- checksum: b2ee5579a62b76a1d2f41f4e06749c4c549f9aaa40764862eff49c5a6a8841f1
44
+ checksum: 9cf8ad34392662746a45cfce18113ad19cc29954789e2058f30e527163f2e98a
45
45
  filename: appsignal-x86_64-linux-musl-all-static.tar.gz
46
46
  dynamic:
47
- checksum: 53cd2464853e61c2d8f911ec32ff98cb48f824293ffd575da6743bc34625a7eb
47
+ checksum: 01bd76983227648d9bc41d035e1552fcf18d62f0d6818bf7bb7fc2e7c166513d
48
48
  filename: appsignal-x86_64-linux-musl-all-dynamic.tar.gz
49
49
  x86_64-freebsd:
50
50
  static:
51
- checksum: f228dd2f2cf951c9eb9f04487be50fdfdc3d29a956e639787a4022bd73f02e53
51
+ checksum: 403a597cbdbdba08460c5c9e93347ed9ad1d24333204e6d72db342deb266a296
52
52
  filename: appsignal-x86_64-freebsd-all-static.tar.gz
53
53
  dynamic:
54
- checksum: 030b322b2cb15607260cbb0424c03f6e41646dca7aacc43a30279ad63a336541
54
+ checksum: a1faae80ae09a6588c4cd35cb91dfa4b2176fc3cb17fbf79db80194c21e75bf9
55
55
  filename: appsignal-x86_64-freebsd-all-dynamic.tar.gz
56
56
  amd64-freebsd:
57
57
  static:
58
- checksum: f228dd2f2cf951c9eb9f04487be50fdfdc3d29a956e639787a4022bd73f02e53
58
+ checksum: 403a597cbdbdba08460c5c9e93347ed9ad1d24333204e6d72db342deb266a296
59
59
  filename: appsignal-x86_64-freebsd-all-static.tar.gz
60
60
  dynamic:
61
- checksum: 030b322b2cb15607260cbb0424c03f6e41646dca7aacc43a30279ad63a336541
61
+ checksum: a1faae80ae09a6588c4cd35cb91dfa4b2176fc3cb17fbf79db80194c21e75bf9
62
62
  filename: appsignal-x86_64-freebsd-all-dynamic.tar.gz
@@ -56,7 +56,11 @@ module Appsignal
56
56
  def install_callbacks
57
57
  ActionCable::Channel::Base.set_callback :subscribe, :around, :prepend => true do |channel, inner|
58
58
  # The request is only the original websocket request
59
- env = channel.connection.env
59
+ connection = channel.connection
60
+ # #env is not available on the Rails ConnectionStub class used in the
61
+ # Rails app test suite. If we call `#env` it causes an error to occur
62
+ # in apps' test suites.
63
+ env = connection.respond_to?(:env) ? connection.env : {}
60
64
  request = ActionDispatch::Request.new(env)
61
65
  env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||=
62
66
  request.request_id || SecureRandom.uuid
@@ -84,7 +88,11 @@ module Appsignal
84
88
 
85
89
  ActionCable::Channel::Base.set_callback :unsubscribe, :around, :prepend => true do |channel, inner|
86
90
  # The request is only the original websocket request
87
- env = channel.connection.env
91
+ connection = channel.connection
92
+ # #env is not available on the Rails ConnectionStub class used in the
93
+ # Rails app test suite. If we call `#env` it causes an error to occur
94
+ # in apps' test suites.
95
+ env = connection.respond_to?(:env) ? connection.env : {}
88
96
  request = ActionDispatch::Request.new(env)
89
97
  env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||=
90
98
  request.request_id || SecureRandom.uuid
@@ -16,7 +16,11 @@ module Appsignal
16
16
 
17
17
  ::Sidekiq.configure_server do |config|
18
18
  config.server_middleware do |chain|
19
- chain.add Appsignal::Hooks::SidekiqPlugin
19
+ if chain.respond_to? :prepend
20
+ chain.prepend Appsignal::Hooks::SidekiqPlugin
21
+ else
22
+ chain.add Appsignal::Hooks::SidekiqPlugin
23
+ end
20
24
  end
21
25
  end
22
26
  end
@@ -4,38 +4,8 @@ if defined?(Appsignal)
4
4
  Appsignal::Environment.report_enabled("object_instrumentation")
5
5
  end
6
6
 
7
- class Object
8
- def self.appsignal_instrument_class_method(method_name, options = {})
9
- singleton_class.send \
10
- :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
11
- singleton_class.send(:define_method, method_name) do |*args, &block|
12
- name = options.fetch(:name) do
13
- "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
14
- end
15
- Appsignal.instrument name do
16
- send "appsignal_uninstrumented_#{method_name}", *args, &block
17
- end
18
- end
19
- end
20
-
21
- def self.appsignal_instrument_method(method_name, options = {})
22
- alias_method "appsignal_uninstrumented_#{method_name}", method_name
23
- define_method method_name do |*args, &block|
24
- name = options.fetch(:name) do
25
- "#{method_name}.#{appsignal_reverse_class_name}.other"
26
- end
27
- Appsignal.instrument name do
28
- send "appsignal_uninstrumented_#{method_name}", *args, &block
29
- end
30
- end
31
- end
32
-
33
- def self.appsignal_reverse_class_name
34
- return "AnonymousClass" unless name
35
- name.split("::").reverse.join(".")
36
- end
37
-
38
- def appsignal_reverse_class_name
39
- self.class.appsignal_reverse_class_name
40
- end
7
+ if RUBY_VERSION < "2.0"
8
+ require "appsignal/integrations/object_ruby_19"
9
+ else
10
+ require "appsignal/integrations/object_ruby_modern"
41
11
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Object
4
+ def self.appsignal_instrument_class_method(method_name, options = {})
5
+ singleton_class.send \
6
+ :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
7
+ singleton_class.send(:define_method, method_name) do |*args, &block|
8
+ name = options.fetch(:name) do
9
+ "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
10
+ end
11
+ Appsignal.instrument name do
12
+ send "appsignal_uninstrumented_#{method_name}", *args, &block
13
+ end
14
+ end
15
+ end
16
+
17
+ def self.appsignal_instrument_method(method_name, options = {})
18
+ alias_method "appsignal_uninstrumented_#{method_name}", method_name
19
+ define_method method_name do |*args, &block|
20
+ name = options.fetch(:name) do
21
+ "#{method_name}.#{appsignal_reverse_class_name}.other"
22
+ end
23
+ Appsignal.instrument name do
24
+ send "appsignal_uninstrumented_#{method_name}", *args, &block
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.appsignal_reverse_class_name
30
+ return "AnonymousClass" unless name
31
+ name.split("::").reverse.join(".")
32
+ end
33
+
34
+ def appsignal_reverse_class_name
35
+ self.class.appsignal_reverse_class_name
36
+ end
37
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Object
4
+ if Appsignal::System.ruby_2_7_or_newer?
5
+ def self.appsignal_instrument_class_method(method_name, options = {})
6
+ singleton_class.send \
7
+ :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
8
+ singleton_class.send(:define_method, method_name) do |*args, **kwargs, &block|
9
+ name = options.fetch(:name) do
10
+ "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
11
+ end
12
+ Appsignal.instrument name do
13
+ send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
14
+ end
15
+ end
16
+ end
17
+
18
+ def self.appsignal_instrument_method(method_name, options = {})
19
+ alias_method "appsignal_uninstrumented_#{method_name}", method_name
20
+ define_method method_name do |*args, **kwargs, &block|
21
+ name = options.fetch(:name) do
22
+ "#{method_name}.#{appsignal_reverse_class_name}.other"
23
+ end
24
+ Appsignal.instrument name do
25
+ send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
26
+ end
27
+ end
28
+ end
29
+ else
30
+ def self.appsignal_instrument_class_method(method_name, options = {})
31
+ singleton_class.send \
32
+ :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
33
+ singleton_class.send(:define_method, method_name) do |*args, &block|
34
+ name = options.fetch(:name) do
35
+ "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
36
+ end
37
+ Appsignal.instrument name do
38
+ send "appsignal_uninstrumented_#{method_name}", *args, &block
39
+ end
40
+ end
41
+ end
42
+
43
+ def self.appsignal_instrument_method(method_name, options = {})
44
+ alias_method "appsignal_uninstrumented_#{method_name}", method_name
45
+ define_method method_name do |*args, &block|
46
+ name = options.fetch(:name) do
47
+ "#{method_name}.#{appsignal_reverse_class_name}.other"
48
+ end
49
+ Appsignal.instrument name do
50
+ send "appsignal_uninstrumented_#{method_name}", *args, &block
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def self.appsignal_reverse_class_name
57
+ return "AnonymousClass" unless name
58
+ name.split("::").reverse.join(".")
59
+ end
60
+
61
+ def appsignal_reverse_class_name
62
+ self.class.appsignal_reverse_class_name
63
+ end
64
+ end
@@ -135,6 +135,12 @@ module Appsignal
135
135
  def start
136
136
  stop
137
137
  @thread = Thread.new do
138
+ # Advise multi-threaded app servers to ignore this thread
139
+ # for the purposes of fork safety warnings
140
+ if Thread.current.respond_to?(:thread_variable_set)
141
+ Thread.current.thread_variable_set(:fork_safe, true)
142
+ end
143
+
138
144
  sleep initial_wait_time
139
145
  initialize_probes
140
146
  loop do
@@ -79,5 +79,9 @@ module Appsignal
79
79
  def self.jruby?
80
80
  RUBY_PLATFORM == "java"
81
81
  end
82
+
83
+ def self.ruby_2_7_or_newer?
84
+ RUBY_VERSION > "2.7"
85
+ end
82
86
  end
83
87
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.11.3".freeze
4
+ VERSION = "2.11.8".freeze
5
5
  end
@@ -2,6 +2,8 @@ describe Appsignal::Hooks::ActionCableHook do
2
2
  if DependencyHelper.action_cable_present?
3
3
  context "with ActionCable" do
4
4
  require "action_cable/engine"
5
+ # Require test helper to test with ConnectionStub
6
+ require "action_cable/channel/test_case" if DependencyHelper.rails6_present?
5
7
 
6
8
  describe ".dependencies_present?" do
7
9
  subject { described_class.new.dependencies_present? }
@@ -262,6 +264,49 @@ describe Appsignal::Hooks::ActionCableHook do
262
264
  )
263
265
  end
264
266
  end
267
+
268
+ if DependencyHelper.rails6_present?
269
+ context "with ConnectionStub" do
270
+ let(:connection) { ActionCable::Channel::ConnectionStub.new }
271
+ let(:transaction_id) { "Stubbed transaction id" }
272
+ before do
273
+ # Stub future (private AppSignal) transaction id generated by the hook.
274
+ expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
275
+ end
276
+
277
+ it "does not fail on missing `#env` method on `ConnectionStub`" do
278
+ instance.subscribe_to_channel
279
+
280
+ expect(subject).to include(
281
+ "action" => "MyChannel#subscribed",
282
+ "error" => nil,
283
+ "id" => transaction_id,
284
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
285
+ "metadata" => {
286
+ "method" => "websocket",
287
+ "path" => "" # No path as the ConnectionStub doesn't have the real request env
288
+ }
289
+ )
290
+ expect(subject["events"].first).to include(
291
+ "allocation_count" => kind_of(Integer),
292
+ "body" => "",
293
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
294
+ "child_allocation_count" => kind_of(Integer),
295
+ "child_duration" => kind_of(Float),
296
+ "child_gc_duration" => kind_of(Float),
297
+ "count" => 1,
298
+ "gc_duration" => kind_of(Float),
299
+ "start" => kind_of(Float),
300
+ "duration" => kind_of(Float),
301
+ "name" => "subscribed.action_cable",
302
+ "title" => ""
303
+ )
304
+ expect(subject["sample_data"]).to include(
305
+ "params" => { "internal" => "true" }
306
+ )
307
+ end
308
+ end
309
+ end
265
310
  end
266
311
 
267
312
  describe "unsubscribe callback" do
@@ -349,6 +394,49 @@ describe Appsignal::Hooks::ActionCableHook do
349
394
  )
350
395
  end
351
396
  end
397
+
398
+ if DependencyHelper.rails6_present?
399
+ context "with ConnectionStub" do
400
+ let(:connection) { ActionCable::Channel::ConnectionStub.new }
401
+ let(:transaction_id) { "Stubbed transaction id" }
402
+ before do
403
+ # Stub future (private AppSignal) transaction id generated by the hook.
404
+ expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
405
+ end
406
+
407
+ it "does not fail on missing `#env` method on `ConnectionStub`" do
408
+ instance.unsubscribe_from_channel
409
+
410
+ expect(subject).to include(
411
+ "action" => "MyChannel#unsubscribed",
412
+ "error" => nil,
413
+ "id" => transaction_id,
414
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
415
+ "metadata" => {
416
+ "method" => "websocket",
417
+ "path" => "" # No path as the ConnectionStub doesn't have the real request env
418
+ }
419
+ )
420
+ expect(subject["events"].first).to include(
421
+ "allocation_count" => kind_of(Integer),
422
+ "body" => "",
423
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
424
+ "child_allocation_count" => kind_of(Integer),
425
+ "child_duration" => kind_of(Float),
426
+ "child_gc_duration" => kind_of(Float),
427
+ "count" => 1,
428
+ "gc_duration" => kind_of(Float),
429
+ "start" => kind_of(Float),
430
+ "duration" => kind_of(Float),
431
+ "name" => "unsubscribed.action_cable",
432
+ "title" => ""
433
+ )
434
+ expect(subject["sample_data"]).to include(
435
+ "params" => { "internal" => "true" }
436
+ )
437
+ end
438
+ end
439
+ end
352
440
  end
353
441
  end
354
442
  end
@@ -16,14 +16,31 @@ describe Appsignal::Hooks::SidekiqHook do
16
16
  end
17
17
 
18
18
  describe "#install" do
19
- class SidekiqMiddlewareMock < Set
20
- def exists?(middleware)
21
- include?(middleware)
19
+ class SidekiqMiddlewareMockWithPrepend < Array
20
+ alias add <<
21
+ alias exists? include?
22
+
23
+ unless method_defined? :prepend
24
+ def prepend(middleware) # For Ruby < 2.5
25
+ insert(0, middleware)
26
+ end
22
27
  end
23
28
  end
29
+
30
+ class SidekiqMiddlewareMockWithoutPrepend < Array
31
+ alias add <<
32
+ alias exists? include?
33
+
34
+ undef_method :prepend if method_defined? :prepend # For Ruby >= 2.5
35
+ end
36
+
24
37
  module SidekiqMock
38
+ def self.middleware_mock=(mock)
39
+ @middlewares = mock.new
40
+ end
41
+
25
42
  def self.middlewares
26
- @middlewares ||= SidekiqMiddlewareMock.new
43
+ @middlewares
27
44
  end
28
45
 
29
46
  def self.configure_server
@@ -36,15 +53,52 @@ describe Appsignal::Hooks::SidekiqHook do
36
53
  end
37
54
  end
38
55
 
56
+ def add_middleware(middleware)
57
+ Sidekiq.configure_server do |sidekiq_config|
58
+ sidekiq_config.middlewares.add(middleware)
59
+ end
60
+ end
61
+
39
62
  before do
40
63
  Appsignal.config = project_fixture_config
41
64
  stub_const "Sidekiq", SidekiqMock
42
65
  end
43
66
 
44
- it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain" do
45
- described_class.new.install
67
+ context "when Sidekiq middleware responds to prepend method" do # Sidekiq 3.3.0 and newer
68
+ before { Sidekiq.middleware_mock = SidekiqMiddlewareMockWithPrepend }
69
+
70
+ it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain in the first position" do
71
+ user_middleware1 = proc {}
72
+ add_middleware(user_middleware1)
73
+ described_class.new.install
74
+ user_middleware2 = proc {}
75
+ add_middleware(user_middleware2)
46
76
 
47
- expect(Sidekiq.server_middleware.exists?(Appsignal::Hooks::SidekiqPlugin)).to be(true)
77
+ expect(Sidekiq.server_middleware).to eql([
78
+ Appsignal::Hooks::SidekiqPlugin, # Prepend makes it the first entry
79
+ user_middleware1,
80
+ user_middleware2
81
+ ])
82
+ end
83
+ end
84
+
85
+ context "when Sidekiq middleware does not respond to prepend method" do
86
+ before { Sidekiq.middleware_mock = SidekiqMiddlewareMockWithoutPrepend }
87
+
88
+ it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain" do
89
+ user_middleware1 = proc {}
90
+ add_middleware(user_middleware1)
91
+ described_class.new.install
92
+ user_middleware2 = proc {}
93
+ add_middleware(user_middleware2)
94
+
95
+ # Add middlewares in whatever order they were added
96
+ expect(Sidekiq.server_middleware).to eql([
97
+ user_middleware1,
98
+ Appsignal::Hooks::SidekiqPlugin,
99
+ user_middleware2
100
+ ])
101
+ end
48
102
  end
49
103
  end
50
104
  end
@@ -0,0 +1,266 @@
1
+ require "appsignal/integrations/object"
2
+
3
+ def is_ruby_19
4
+ RUBY_VERSION < "2.0"
5
+ end
6
+
7
+ describe Object do
8
+ describe "#instrument_method" do
9
+ context "with instance method" do
10
+ let(:klass) do
11
+ Class.new do
12
+ def foo(param1, options = {})
13
+ [param1, options]
14
+ end
15
+ appsignal_instrument_method :foo
16
+ end
17
+ end
18
+ let(:instance) { klass.new }
19
+
20
+ def call_with_arguments
21
+ instance.foo(
22
+ "abc",
23
+ :foo => "bar"
24
+ )
25
+ end
26
+
27
+ context "when active" do
28
+ let(:transaction) { http_request_transaction }
29
+ before do
30
+ Appsignal.config = project_fixture_config
31
+ expect(Appsignal::Transaction).to receive(:current).at_least(:once).and_return(transaction)
32
+ end
33
+ after { Appsignal.config = nil }
34
+
35
+ context "with anonymous class" do
36
+ it "instruments the method and calls it" do
37
+ expect(Appsignal.active?).to be_truthy
38
+ expect(transaction).to receive(:start_event)
39
+ expect(transaction).to receive(:finish_event).with \
40
+ "foo.AnonymousClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
41
+ expect(call_with_arguments).to eq(["abc", { :foo => "bar" }])
42
+ end
43
+ end
44
+
45
+ context "with named class" do
46
+ before do
47
+ class NamedClass
48
+ def foo
49
+ 1
50
+ end
51
+ appsignal_instrument_method :foo
52
+ end
53
+ end
54
+ after { Object.send(:remove_const, :NamedClass) }
55
+ let(:klass) { NamedClass }
56
+
57
+ it "instruments the method and calls it" do
58
+ expect(Appsignal.active?).to be_truthy
59
+ expect(transaction).to receive(:start_event)
60
+ expect(transaction).to receive(:finish_event).with \
61
+ "foo.NamedClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
62
+ expect(instance.foo).to eq(1)
63
+ end
64
+ end
65
+
66
+ context "with nested named class" do
67
+ before do
68
+ module MyModule
69
+ module NestedModule
70
+ class NamedClass
71
+ def bar
72
+ 2
73
+ end
74
+ appsignal_instrument_method :bar
75
+ end
76
+ end
77
+ end
78
+ end
79
+ after { Object.send(:remove_const, :MyModule) }
80
+ let(:klass) { MyModule::NestedModule::NamedClass }
81
+
82
+ it "instruments the method and calls it" do
83
+ expect(Appsignal.active?).to be_truthy
84
+ expect(transaction).to receive(:start_event)
85
+ expect(transaction).to receive(:finish_event).with \
86
+ "bar.NamedClass.NestedModule.MyModule.other", nil, nil,
87
+ Appsignal::EventFormatter::DEFAULT
88
+ expect(instance.bar).to eq(2)
89
+ end
90
+ end
91
+
92
+ context "with custom name" do
93
+ let(:klass) do
94
+ Class.new do
95
+ def foo
96
+ 1
97
+ end
98
+ appsignal_instrument_method :foo, :name => "my_method.group"
99
+ end
100
+ end
101
+
102
+ it "instruments with custom name" do
103
+ expect(Appsignal.active?).to be_truthy
104
+ expect(transaction).to receive(:start_event)
105
+ expect(transaction).to receive(:finish_event).with \
106
+ "my_method.group", nil, nil, Appsignal::EventFormatter::DEFAULT
107
+ expect(instance.foo).to eq(1)
108
+ end
109
+ end
110
+
111
+ context "with a method given a block" do
112
+ let(:klass) do
113
+ Class.new do
114
+ def foo
115
+ yield
116
+ end
117
+ appsignal_instrument_method :foo
118
+ end
119
+ end
120
+
121
+ it "should yield the block" do
122
+ expect(instance.foo { 42 }).to eq(42)
123
+ end
124
+ end
125
+ end
126
+
127
+ context "when not active" do
128
+ let(:transaction) { Appsignal::Transaction.current }
129
+
130
+ it "does not instrument, but still calls the method" do
131
+ expect(Appsignal.active?).to be_falsy
132
+ expect(transaction).to_not receive(:start_event)
133
+ expect(call_with_arguments).to eq(["abc", { :foo => "bar" }])
134
+ end
135
+ end
136
+ end
137
+
138
+ context "with class method" do
139
+ let(:klass) do
140
+ Class.new do
141
+ def self.bar(param1, options = {})
142
+ [param1, options]
143
+ end
144
+ appsignal_instrument_class_method :bar
145
+ end
146
+ end
147
+ def call_with_arguments
148
+ klass.bar(
149
+ "abc",
150
+ :foo => "bar"
151
+ )
152
+ end
153
+
154
+ context "when active" do
155
+ let(:transaction) { http_request_transaction }
156
+ before do
157
+ Appsignal.config = project_fixture_config
158
+ expect(Appsignal::Transaction).to receive(:current).at_least(:once)
159
+ .and_return(transaction)
160
+ end
161
+ after { Appsignal.config = nil }
162
+
163
+ context "with anonymous class" do
164
+ it "instruments the method and calls it" do
165
+ expect(Appsignal.active?).to be_truthy
166
+ expect(transaction).to receive(:start_event)
167
+ expect(transaction).to receive(:finish_event).with \
168
+ "bar.class_method.AnonymousClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
169
+ expect(call_with_arguments).to eq(["abc", { :foo => "bar" }])
170
+ end
171
+ end
172
+
173
+ context "with named class" do
174
+ before do
175
+ class NamedClass
176
+ def self.bar
177
+ 2
178
+ end
179
+ appsignal_instrument_class_method :bar
180
+ end
181
+ end
182
+ after { Object.send(:remove_const, :NamedClass) }
183
+ let(:klass) { NamedClass }
184
+
185
+ it "instruments the method and calls it" do
186
+ expect(Appsignal.active?).to be_truthy
187
+ expect(transaction).to receive(:start_event)
188
+ expect(transaction).to receive(:finish_event).with \
189
+ "bar.class_method.NamedClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
190
+ expect(klass.bar).to eq(2)
191
+ end
192
+
193
+ context "with nested named class" do
194
+ before do
195
+ module MyModule
196
+ module NestedModule
197
+ class NamedClass
198
+ def self.bar
199
+ 2
200
+ end
201
+ appsignal_instrument_class_method :bar
202
+ end
203
+ end
204
+ end
205
+ end
206
+ after { Object.send(:remove_const, :MyModule) }
207
+ let(:klass) { MyModule::NestedModule::NamedClass }
208
+
209
+ it "instruments the method and calls it" do
210
+ expect(Appsignal.active?).to be_truthy
211
+ expect(transaction).to receive(:start_event)
212
+ expect(transaction).to receive(:finish_event).with \
213
+ "bar.class_method.NamedClass.NestedModule.MyModule.other", nil, nil,
214
+ Appsignal::EventFormatter::DEFAULT
215
+ expect(klass.bar).to eq(2)
216
+ end
217
+ end
218
+ end
219
+
220
+ context "with custom name" do
221
+ let(:klass) do
222
+ Class.new do
223
+ def self.bar
224
+ 2
225
+ end
226
+ appsignal_instrument_class_method :bar, :name => "my_method.group"
227
+ end
228
+ end
229
+
230
+ it "instruments with custom name" do
231
+ expect(Appsignal.active?).to be_truthy
232
+ expect(transaction).to receive(:start_event)
233
+ expect(transaction).to receive(:finish_event).with \
234
+ "my_method.group", nil, nil, Appsignal::EventFormatter::DEFAULT
235
+ expect(klass.bar).to eq(2)
236
+ end
237
+ end
238
+
239
+ context "with a method given a block" do
240
+ let(:klass) do
241
+ Class.new do
242
+ def self.bar
243
+ yield
244
+ end
245
+ appsignal_instrument_class_method :bar
246
+ end
247
+ end
248
+
249
+ it "should yield the block" do
250
+ expect(klass.bar { 42 }).to eq(42)
251
+ end
252
+ end
253
+ end
254
+
255
+ context "when not active" do
256
+ let(:transaction) { Appsignal::Transaction.current }
257
+
258
+ it "does not instrument, but still call the method" do
259
+ expect(Appsignal.active?).to be_falsy
260
+ expect(transaction).to_not receive(:start_event)
261
+ expect(call_with_arguments).to eq(["abc", { :foo => "bar" }])
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
@@ -1,18 +1,30 @@
1
1
  require "appsignal/integrations/object"
2
2
 
3
+ def is_ruby_19
4
+ RUBY_VERSION < "2.0"
5
+ end
6
+
3
7
  describe Object do
4
8
  describe "#instrument_method" do
5
9
  context "with instance method" do
6
10
  let(:klass) do
7
11
  Class.new do
8
- def foo
9
- 1
12
+ def foo(param1, options = {}, keyword_param: 1)
13
+ [param1, options, keyword_param]
10
14
  end
11
15
  appsignal_instrument_method :foo
12
16
  end
13
17
  end
14
18
  let(:instance) { klass.new }
15
19
 
20
+ def call_with_arguments
21
+ instance.foo(
22
+ "abc",
23
+ { :foo => "bar" },
24
+ :keyword_param => 2
25
+ )
26
+ end
27
+
16
28
  context "when active" do
17
29
  let(:transaction) { http_request_transaction }
18
30
  before do
@@ -27,7 +39,7 @@ describe Object do
27
39
  expect(transaction).to receive(:start_event)
28
40
  expect(transaction).to receive(:finish_event).with \
29
41
  "foo.AnonymousClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
30
- expect(instance.foo).to eq(1)
42
+ expect(call_with_arguments).to eq(["abc", { :foo => "bar" }, 2])
31
43
  end
32
44
  end
33
45
 
@@ -116,10 +128,10 @@ describe Object do
116
128
  context "when not active" do
117
129
  let(:transaction) { Appsignal::Transaction.current }
118
130
 
119
- it "should not instrument, but still call the method" do
131
+ it "does not instrument, but still calls the method" do
120
132
  expect(Appsignal.active?).to be_falsy
121
133
  expect(transaction).to_not receive(:start_event)
122
- expect(instance.foo).to eq(1)
134
+ expect(call_with_arguments).to eq(["abc", { :foo => "bar" }, 2])
123
135
  end
124
136
  end
125
137
  end
@@ -127,12 +139,19 @@ describe Object do
127
139
  context "with class method" do
128
140
  let(:klass) do
129
141
  Class.new do
130
- def self.bar
131
- 2
142
+ def self.bar(param1, options = {}, keyword_param: 1)
143
+ [param1, options, keyword_param]
132
144
  end
133
145
  appsignal_instrument_class_method :bar
134
146
  end
135
147
  end
148
+ def call_with_arguments
149
+ klass.bar(
150
+ "abc",
151
+ { :foo => "bar" },
152
+ :keyword_param => 2
153
+ )
154
+ end
136
155
 
137
156
  context "when active" do
138
157
  let(:transaction) { http_request_transaction }
@@ -149,7 +168,7 @@ describe Object do
149
168
  expect(transaction).to receive(:start_event)
150
169
  expect(transaction).to receive(:finish_event).with \
151
170
  "bar.class_method.AnonymousClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
152
- expect(klass.bar).to eq(2)
171
+ expect(call_with_arguments).to eq(["abc", { :foo => "bar" }, 2])
153
172
  end
154
173
  end
155
174
 
@@ -238,10 +257,10 @@ describe Object do
238
257
  context "when not active" do
239
258
  let(:transaction) { Appsignal::Transaction.current }
240
259
 
241
- it "should not instrument, but still call the method" do
260
+ it "does not instrument, but still call the method" do
242
261
  expect(Appsignal.active?).to be_falsy
243
262
  expect(transaction).to_not receive(:start_event)
244
- expect(klass.bar).to eq(2)
263
+ expect(call_with_arguments).to eq(["abc", { :foo => "bar" }, 2])
245
264
  end
246
265
  end
247
266
  end
@@ -23,6 +23,27 @@ RSpec.describe "Puma plugin" do
23
23
  def self.stats
24
24
  end
25
25
 
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
45
+ end
46
+
26
47
  class Plugin
27
48
  class << self
28
49
  attr_reader :plugin
@@ -68,6 +89,13 @@ RSpec.describe "Puma plugin" do
68
89
  wait_for("enough probe calls") { probe.calls >= 2 }
69
90
  end
70
91
 
92
+ it "marks the PumaProbe thread as fork-safe", :not_ruby19 do
93
+ out_stream = std_stream
94
+ capture_stdout(out_stream) { Puma.run }
95
+
96
+ expect(out_stream.read).not_to include("WARNING: Detected 1 Thread")
97
+ end
98
+
71
99
  context "without Puma.stats" do
72
100
  before { Puma.singleton_class.send(:remove_method, :stats) }
73
101
 
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: 2.11.3
4
+ version: 2.11.8
5
5
  platform: ruby
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-01-19 00:00:00.000000000 Z
13
+ date: 2021-02-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -233,6 +233,8 @@ files:
233
233
  - lib/appsignal/integrations/grape.rb
234
234
  - lib/appsignal/integrations/mongo_ruby_driver.rb
235
235
  - lib/appsignal/integrations/object.rb
236
+ - lib/appsignal/integrations/object_ruby_19.rb
237
+ - lib/appsignal/integrations/object_ruby_modern.rb
236
238
  - lib/appsignal/integrations/padrino.rb
237
239
  - lib/appsignal/integrations/que.rb
238
240
  - lib/appsignal/integrations/railtie.rb
@@ -322,6 +324,7 @@ files:
322
324
  - spec/lib/appsignal/integrations/data_mapper_spec.rb
323
325
  - spec/lib/appsignal/integrations/grape_spec.rb
324
326
  - spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb
327
+ - spec/lib/appsignal/integrations/object_19_spec.rb
325
328
  - spec/lib/appsignal/integrations/object_spec.rb
326
329
  - spec/lib/appsignal/integrations/padrino_spec.rb
327
330
  - spec/lib/appsignal/integrations/que_spec.rb
@@ -415,7 +418,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
415
418
  - !ruby/object:Gem::Version
416
419
  version: '0'
417
420
  requirements: []
418
- rubygems_version: 3.2.5
421
+ rubygems_version: 3.2.8
419
422
  signing_key:
420
423
  specification_version: 4
421
424
  summary: Logs performance and exception data from your app to appsignal.com
@@ -474,6 +477,7 @@ test_files:
474
477
  - spec/lib/appsignal/integrations/data_mapper_spec.rb
475
478
  - spec/lib/appsignal/integrations/grape_spec.rb
476
479
  - spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb
480
+ - spec/lib/appsignal/integrations/object_19_spec.rb
477
481
  - spec/lib/appsignal/integrations/object_spec.rb
478
482
  - spec/lib/appsignal/integrations/padrino_spec.rb
479
483
  - spec/lib/appsignal/integrations/que_spec.rb