appsignal 4.10.1-java → 4.10.2-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1484edc2b0257539896eed6be1f56407fcaf454ab7c200c6bf798fea67d75c05
4
- data.tar.gz: bc1cd76fce693264350284284af84e86bbc4956e84e36fc30af4b8d9b8ab5811
3
+ metadata.gz: 39bff03e12f0f39edebab29adeb15d316efb0fe813ca087b1b914d083081cbd6
4
+ data.tar.gz: 88b3ce4fabea7ad7c05306c0d0f5763d844c2dcf249c2168d46cc8f964646601
5
5
  SHA512:
6
- metadata.gz: ce745eea54bcb98d7c2cf87acd060a1c6e162a25090bc0c653491fd926ef6ef26dd6e4f4a88516d4ac2560d943b7f720c88fade34276481d51c0a1cf277b7318
7
- data.tar.gz: 35f4295a1a9685e44719a1a6723e3933a3777e495ffeb862884c4e1dc61979cb990ef093136ecea5a9040d05790c0925464eb0b9481fdfcd0f66da85747c3f7a
6
+ metadata.gz: e7984a466bf5356a1b6856508a18d44972cb06c3b509aa2f1113e079602b9188f5b4708fbcb83b4bcfd9ef604c0a6b65a97b70afc9656e3553fe3227734d8c69
7
+ data.tar.gz: 6f6f74a33b99c1ddccee87234580e7fb876bce230045a29b14942abc1460e6b6926e2274c96c121e2336d9c060699b05d7a89d410061ba25ace46eeb4f9de5a2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # AppSignal for Ruby gem Changelog
2
2
 
3
+ ## 4.10.2
4
+
5
+ _Published on 2026-09-09._
6
+
7
+ ### Added
8
+
9
+ - Add support for Grape 4. On Grape 4, every request through a Grape API raised a `NoMethodError`. Applications on Grape 3 and below were not affected.
10
+
11
+ On Grape 4, the action name and the reported path now describe the endpoint's full route, so they include the API prefix, the path version and the mount point. They also no longer end in a trailing slash when the endpoint declares no path of its own, so an endpoint reported as `GET::My::Api#/users/:id/` is now reported as `GET::My::Api#/users/:id`. Action names on Grape 3 and below do not change.
12
+
13
+ (patch [bdea5c1b](https://github.com/appsignal/appsignal-ruby/commit/bdea5c1b6f02c902d0ba195eff3544545bee2547))
14
+
15
+ ### Changed
16
+
17
+ - Update the agent to handle high traffic apps. On high-traffic apps that would exceed the maximum accepted internal payload size, send data to the Push API more frequently. (patch [a1c3033e](https://github.com/appsignal/appsignal-ruby/commit/a1c3033e1b255371a2d71d4519506a67dbfa2b82))
18
+
19
+ ### Fixed
20
+
21
+ - Load the host Rails application before the `appsignal demo` command reads the
22
+ AppSignal configuration. This matches the behavior of `appsignal diagnose`.
23
+
24
+ Thanks [@Guflly](https://github.com/Guflly) for your contribution!
25
+
26
+ (patch [3249a376](https://github.com/appsignal/appsignal-ruby/commit/3249a376a6fb7a10c990e0f49f7907dc40b03a12))
27
+ - Fix events showing as unknown in long-running applications. An application process that kept running for thirty days without restarting could lose the names and queries of the events it recorded, both in slow traces and in the "Slow events" panel. (patch [a1c3033e](https://github.com/appsignal/appsignal-ruby/commit/a1c3033e1b255371a2d71d4519506a67dbfa2b82))
28
+ - Prevent `gc` events from appearing in the "Slow events" panel. (patch [a1c3033e](https://github.com/appsignal/appsignal-ruby/commit/a1c3033e1b255371a2d71d4519506a67dbfa2b82))
29
+ - Report Delayed Job jobs that fail to load. Before this change, a job whose
30
+ payload could not be deserialized was not reported to AppSignal at all.
31
+
32
+ This can happen when a deploy removes or renames a job class while jobs of that
33
+ class are still queued. Those jobs are now reported as failed, with the
34
+ `Delayed::DeserializationError` they raised, and are named after the class
35
+ recorded in the job's handler. When even that cannot be read, they are named
36
+ `DelayedJobInternal`.
37
+
38
+ (patch [ea294440](https://github.com/appsignal/appsignal-ruby/commit/ea29444069ef78347ad1e8266869b7d9aa2caa50))
39
+ - Record a single event for a bulk enqueue of Active Job jobs. Before this change,
40
+ enqueuing jobs with `ActiveJob.perform_all_later` would record an event for the
41
+ batch and, if the adapter was instrumented with AppSignal, another event for each
42
+ job in it. A job that slices a large collection and enqueues it in batches was
43
+ therefore reported with an event per job enqueued, where before version 4.9.0 it
44
+ had one event per batch.
45
+
46
+ A bulk enqueue is now recorded as a single `enqueue_all.active_job` event, named
47
+ after the job class when every job in the batch shares one.
48
+
49
+ (patch [fe82dc60](https://github.com/appsignal/appsignal-ruby/commit/fe82dc60bc8b52d466c0c9b673adbae29f79449e))
50
+
3
51
  ## 4.10.1
4
52
 
5
53
  _Published on 2026-08-20._
data/README.md CHANGED
@@ -240,7 +240,8 @@ configurations you need to run the spec suite with a specific Gemfile.
240
240
  BUNDLE_GEMFILE=gemfiles/capistrano2.gemfile bundle exec rspec
241
241
  BUNDLE_GEMFILE=gemfiles/capistrano3.gemfile bundle exec rspec
242
242
  BUNDLE_GEMFILE=gemfiles/dry-monitor.gemfile bundle exec rspec
243
- BUNDLE_GEMFILE=gemfiles/grape.gemfile bundle exec rspec
243
+ BUNDLE_GEMFILE=gemfiles/grape-3.gemfile bundle exec rspec
244
+ BUNDLE_GEMFILE=gemfiles/grape-4.gemfile bundle exec rspec
244
245
  BUNDLE_GEMFILE=gemfiles/hanami.gemfile bundle exec rspec
245
246
  BUNDLE_GEMFILE=gemfiles/http5.gemfile bundle exec rspec
246
247
  BUNDLE_GEMFILE=gemfiles/no_dependencies.gemfile bundle exec rspec
data/appsignal.gemspec CHANGED
@@ -61,6 +61,13 @@ Gem::Specification.new do |gem|
61
61
  gem.add_dependency "rack", ">= 2.0.0"
62
62
 
63
63
  gem.add_development_dependency "pry"
64
+ # Reached through webmock's addressable. Version 7 requires Ruby 3.2 and
65
+ # version 6 requires 3.0, so an older Ruby cannot resolve the development
66
+ # dependencies unless this is held back. The cop guards the requirements of
67
+ # the released gem, which a development dependency is not part of.
68
+ # rubocop:disable Gemspec/RubyVersionGlobalsUsage
69
+ gem.add_development_dependency "public_suffix", "< 6" if RUBY_VERSION < "3.2"
70
+ # rubocop:enable Gemspec/RubyVersionGlobalsUsage
64
71
  gem.add_development_dependency "rake", ">= 12"
65
72
  gem.add_development_dependency "rspec", "~> 3.8"
66
73
  gem.add_development_dependency "rubocop", "~> 1.87.0"
data/build_matrix.yml CHANGED
@@ -2,7 +2,7 @@ github:
2
2
  name: Ruby gem CI
3
3
  "on":
4
4
  push:
5
- branches: ["main", "develop"]
5
+ branches: ["main", "develop", "*-stable"]
6
6
  pull_request:
7
7
  types: [opened, reopened, synchronize]
8
8
  schedule:
@@ -162,7 +162,13 @@ matrix:
162
162
  - "3.2.5"
163
163
  - "3.1.6"
164
164
  - "3.0.7"
165
- - gem: "grape"
165
+ - gem: "grape-3"
166
+ - gem: "grape-4"
167
+ only:
168
+ ruby:
169
+ - "4.0.0"
170
+ - "3.4.1"
171
+ - "3.3.4"
166
172
  - gem: "hanami-2.0"
167
173
  only:
168
174
  ruby:
data/ext/agent.rb CHANGED
@@ -6,7 +6,7 @@
6
6
  # Modifications to this file will be overwritten with the next agent release.
7
7
 
8
8
  APPSIGNAL_AGENT_CONFIG = {
9
- "version" => "0.36.12",
9
+ "version" => "0.37.0",
10
10
  "mirrors" => [
11
11
  "https://d135dj0rjqvssy.cloudfront.net",
12
12
  "https://appsignal-agent-releases.global.ssl.fastly.net"
@@ -14,131 +14,131 @@ APPSIGNAL_AGENT_CONFIG = {
14
14
  "triples" => {
15
15
  "x86_64-darwin" => {
16
16
  "static" => {
17
- "checksum" => "a63189a2ba6b500e038e5658f97478ceae27a956baa2f89a4e79ee5a1dadace6",
17
+ "checksum" => "1c05ef4cd4f0d0646bbc00a61568448cafd6c8cd23e152a4d7e22bab7e733cf1",
18
18
  "filename" => "appsignal-x86_64-darwin-all-static.tar.gz"
19
19
  },
20
20
  "dynamic" => {
21
- "checksum" => "ecfb108dcc4d10f442415debaceed6d2e568b44ab938cf0ed6fee3ccfbdc5379",
21
+ "checksum" => "0c74689a0bc204e0ce462c875f46ea458aa63705956f1adb9daa72dbce47e736",
22
22
  "filename" => "appsignal-x86_64-darwin-all-dynamic.tar.gz"
23
23
  }
24
24
  },
25
25
  "universal-darwin" => {
26
26
  "static" => {
27
- "checksum" => "a63189a2ba6b500e038e5658f97478ceae27a956baa2f89a4e79ee5a1dadace6",
27
+ "checksum" => "1c05ef4cd4f0d0646bbc00a61568448cafd6c8cd23e152a4d7e22bab7e733cf1",
28
28
  "filename" => "appsignal-x86_64-darwin-all-static.tar.gz"
29
29
  },
30
30
  "dynamic" => {
31
- "checksum" => "ecfb108dcc4d10f442415debaceed6d2e568b44ab938cf0ed6fee3ccfbdc5379",
31
+ "checksum" => "0c74689a0bc204e0ce462c875f46ea458aa63705956f1adb9daa72dbce47e736",
32
32
  "filename" => "appsignal-x86_64-darwin-all-dynamic.tar.gz"
33
33
  }
34
34
  },
35
35
  "aarch64-darwin" => {
36
36
  "static" => {
37
- "checksum" => "d23d6cc15e7df1c810b55dc33905002c5a1d6bc3f7cb10690d7e21398aabdac0",
37
+ "checksum" => "031d0f3c32302271274d1e602c9cecb2b037e9dd338001901ff4ed4d0e457075",
38
38
  "filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
39
39
  },
40
40
  "dynamic" => {
41
- "checksum" => "b4f9bf20f87fc7ddd8cc307646a956f734a2153323cc01487ced322263f32c2b",
41
+ "checksum" => "5aadc22281991a97250bd6bf12dd5dd764ffc4da72301d745f86d63542159436",
42
42
  "filename" => "appsignal-aarch64-darwin-all-dynamic.tar.gz"
43
43
  }
44
44
  },
45
45
  "arm64-darwin" => {
46
46
  "static" => {
47
- "checksum" => "d23d6cc15e7df1c810b55dc33905002c5a1d6bc3f7cb10690d7e21398aabdac0",
47
+ "checksum" => "031d0f3c32302271274d1e602c9cecb2b037e9dd338001901ff4ed4d0e457075",
48
48
  "filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
49
49
  },
50
50
  "dynamic" => {
51
- "checksum" => "b4f9bf20f87fc7ddd8cc307646a956f734a2153323cc01487ced322263f32c2b",
51
+ "checksum" => "5aadc22281991a97250bd6bf12dd5dd764ffc4da72301d745f86d63542159436",
52
52
  "filename" => "appsignal-aarch64-darwin-all-dynamic.tar.gz"
53
53
  }
54
54
  },
55
55
  "arm-darwin" => {
56
56
  "static" => {
57
- "checksum" => "d23d6cc15e7df1c810b55dc33905002c5a1d6bc3f7cb10690d7e21398aabdac0",
57
+ "checksum" => "031d0f3c32302271274d1e602c9cecb2b037e9dd338001901ff4ed4d0e457075",
58
58
  "filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
59
59
  },
60
60
  "dynamic" => {
61
- "checksum" => "b4f9bf20f87fc7ddd8cc307646a956f734a2153323cc01487ced322263f32c2b",
61
+ "checksum" => "5aadc22281991a97250bd6bf12dd5dd764ffc4da72301d745f86d63542159436",
62
62
  "filename" => "appsignal-aarch64-darwin-all-dynamic.tar.gz"
63
63
  }
64
64
  },
65
65
  "aarch64-linux" => {
66
66
  "static" => {
67
- "checksum" => "0a277e682f1568fbc54df849a46f21f155643b40c285a5bd30294c9d046e49c2",
67
+ "checksum" => "48499ed06fda433dc7347a4a3d23944d0bfb31604e5d7f52db0a0bbe437f8e03",
68
68
  "filename" => "appsignal-aarch64-linux-all-static.tar.gz"
69
69
  },
70
70
  "dynamic" => {
71
- "checksum" => "94e0193908955e7a3a274a37947e234c5b038aa26c0ea9b371a08fd63441a940",
71
+ "checksum" => "d943b05951cce5d797c0fe7aeacafc9b6870a0d72aa7869b9aa16f5add82fcc6",
72
72
  "filename" => "appsignal-aarch64-linux-all-dynamic.tar.gz"
73
73
  }
74
74
  },
75
75
  "i686-linux" => {
76
76
  "static" => {
77
- "checksum" => "0d4536a2b1c9b6915f0f8597bd858988e284e7dbc4a14488332ffa7e401a1134",
77
+ "checksum" => "41600ba4171cb8549ff9c8b675e9b4ef100eb7ef0a001a970964af04dc209738",
78
78
  "filename" => "appsignal-i686-linux-all-static.tar.gz"
79
79
  },
80
80
  "dynamic" => {
81
- "checksum" => "c5df66876e4611d5ef8798013f0188469cbdea6753a9b9d6cf54bd858e6331e9",
81
+ "checksum" => "ada062a08dc531421141330764e4a0b9a05b34b52b922c1c705ffbac604eb332",
82
82
  "filename" => "appsignal-i686-linux-all-dynamic.tar.gz"
83
83
  }
84
84
  },
85
85
  "x86-linux" => {
86
86
  "static" => {
87
- "checksum" => "0d4536a2b1c9b6915f0f8597bd858988e284e7dbc4a14488332ffa7e401a1134",
87
+ "checksum" => "41600ba4171cb8549ff9c8b675e9b4ef100eb7ef0a001a970964af04dc209738",
88
88
  "filename" => "appsignal-i686-linux-all-static.tar.gz"
89
89
  },
90
90
  "dynamic" => {
91
- "checksum" => "c5df66876e4611d5ef8798013f0188469cbdea6753a9b9d6cf54bd858e6331e9",
91
+ "checksum" => "ada062a08dc531421141330764e4a0b9a05b34b52b922c1c705ffbac604eb332",
92
92
  "filename" => "appsignal-i686-linux-all-dynamic.tar.gz"
93
93
  }
94
94
  },
95
95
  "x86_64-linux" => {
96
96
  "static" => {
97
- "checksum" => "eec59a3e46dd5d5baff044465b4e314f21edf6270500bf24e3bc50afcee5f2e2",
97
+ "checksum" => "8240c494573360ad147aa56a973fe809bf1f3976383b8cd990fc0824e5554913",
98
98
  "filename" => "appsignal-x86_64-linux-all-static.tar.gz"
99
99
  },
100
100
  "dynamic" => {
101
- "checksum" => "37471e17c5cb9f252d039cfbc0c01d570698c2245f6c8783fd2778e640b59a54",
101
+ "checksum" => "ec07ea8188c3589c7c9a401d7555b51fdd052f07b05fc62e3801288493f85ca8",
102
102
  "filename" => "appsignal-x86_64-linux-all-dynamic.tar.gz"
103
103
  }
104
104
  },
105
105
  "x86_64-linux-musl" => {
106
106
  "static" => {
107
- "checksum" => "4454ec095ad14813bedea20a110a26235dbc6e8e33538219fad325792c0d888e",
107
+ "checksum" => "39de4602e300f701ed28748bd1c523bedf44f2d0f6241fffe370485185a895cc",
108
108
  "filename" => "appsignal-x86_64-linux-musl-all-static.tar.gz"
109
109
  },
110
110
  "dynamic" => {
111
- "checksum" => "4c21039b077b0c292d530210e077d942694f4c533d3761ed11e27ca932215131",
111
+ "checksum" => "8a9fd74a6a7589b842a8297025656e7ccc995c9df40c09cf45b39382d3d4a507",
112
112
  "filename" => "appsignal-x86_64-linux-musl-all-dynamic.tar.gz"
113
113
  }
114
114
  },
115
115
  "aarch64-linux-musl" => {
116
116
  "static" => {
117
- "checksum" => "048a55f0f10087e7954559d6cd09b371a0b532673f7ca769bcfde5729e297a72",
117
+ "checksum" => "13fa41ad765a9c79f374dad4d131e7860afb8760c11508af1de982580798a3bc",
118
118
  "filename" => "appsignal-aarch64-linux-musl-all-static.tar.gz"
119
119
  },
120
120
  "dynamic" => {
121
- "checksum" => "8dc336cffcb2cffe85daa8c70b02b0af7e626ed81c8873cdbb627216a411d59a",
121
+ "checksum" => "8193f5925818164390aac737d15a3d86706fce7aad81c4d5e84a3b0552b407a5",
122
122
  "filename" => "appsignal-aarch64-linux-musl-all-dynamic.tar.gz"
123
123
  }
124
124
  },
125
125
  "x86_64-freebsd" => {
126
126
  "static" => {
127
- "checksum" => "d81d039a3c1936bd393a6140b46f48092da31600b0311eab819acd777521a588",
127
+ "checksum" => "c519b132c7a1de2a8dbb8c36d87fe91b4c0451d2c65d8bd4f44b748bb2ad2904",
128
128
  "filename" => "appsignal-x86_64-freebsd-all-static.tar.gz"
129
129
  },
130
130
  "dynamic" => {
131
- "checksum" => "cc345d4a791200e5af53c533e7ad83d278e98ee0becb5b54e502ba8934094372",
131
+ "checksum" => "fb5ea3aac557543de74842d668c66ede00d125e1e86f601fa5ffe002edbd6c62",
132
132
  "filename" => "appsignal-x86_64-freebsd-all-dynamic.tar.gz"
133
133
  }
134
134
  },
135
135
  "amd64-freebsd" => {
136
136
  "static" => {
137
- "checksum" => "d81d039a3c1936bd393a6140b46f48092da31600b0311eab819acd777521a588",
137
+ "checksum" => "c519b132c7a1de2a8dbb8c36d87fe91b4c0451d2c65d8bd4f44b748bb2ad2904",
138
138
  "filename" => "appsignal-x86_64-freebsd-all-static.tar.gz"
139
139
  },
140
140
  "dynamic" => {
141
- "checksum" => "cc345d4a791200e5af53c533e7ad83d278e98ee0becb5b54e502ba8934094372",
141
+ "checksum" => "fb5ea3aac557543de74842d668c66ede00d125e1e86f601fa5ffe002edbd6c62",
142
142
  "filename" => "appsignal-x86_64-freebsd-all-dynamic.tar.gz"
143
143
  }
144
144
  }
@@ -40,6 +40,8 @@ module Appsignal
40
40
  # @see https://docs.appsignal.com/support/debugging.html
41
41
  # Debugging AppSignal guide
42
42
  class Demo
43
+ extend CLI::Helpers
44
+
43
45
  class << self
44
46
  # @param options [Hash]
45
47
  # @option options :environment [String] environment to load
@@ -47,6 +49,9 @@ module Appsignal
47
49
  # @return [void]
48
50
  def run(options = {})
49
51
  ENV["APPSIGNAL_APP_ENV"] = options[:environment] if options[:environment]
52
+ # Load the host Rails app, if any, so that the AppSignal config file
53
+ # can use the application when it is read.
54
+ require_rails_app_if_present(options[:environment])
50
55
 
51
56
  puts "Sending demonstration sample data..."
52
57
  if Appsignal::Demo.transmit
@@ -189,10 +189,15 @@ module Appsignal
189
189
 
190
190
  def configure_appsignal(options)
191
191
  env_option = options.fetch(:environment, nil)
192
+ # Mark app as Rails app
193
+ data[:app][:rails] = true if rails_present?
192
194
  # Try and load the Rails app, if any.
193
195
  # This will configure AppSignal through the config file or an
194
196
  # initializer.
195
- require_rails_app_if_present(env_option)
197
+ require_rails_app_if_present(env_option) do |error|
198
+ data[:app][:load_error] =
199
+ "#{error.class}: #{error.message}\n#{error.backtrace.join("\n")}"
200
+ end
196
201
 
197
202
  # No config loaded yet, try loading as normal
198
203
  Appsignal._load_config!(env_option) unless Appsignal.config
@@ -627,43 +632,6 @@ module Appsignal
627
632
  puts " Read error: #{path[:read_error]}"
628
633
  print_empty_line
629
634
  end
630
-
631
- def print_empty_line
632
- puts "\n"
633
- end
634
-
635
- def require_rails_app_if_present(env_option)
636
- return unless rails_present?
637
-
638
- # Set the environment given as an option to the diagnose CLI so the
639
- # Rails app uses it when loaded.
640
- ENV["_APPSIGNAL_CONFIG_FILE_ENV"] = env_option
641
- # Mark app as Rails app
642
- data[:app][:rails] = true
643
- # Manually require the railtie, because it wasn't loaded when the CLI
644
- # started and AppSignal loaded, because the `Rails` constant wasn't
645
- # present.
646
- require "appsignal/integrations/railtie"
647
- # Start the Rails app, including railties and initializers.
648
- require Appsignal::Utils::RailsHelper.environment_config_path
649
- rescue LoadError, StandardError => error
650
- print_empty_line
651
- puts "ERROR: Error encountered while loading the Rails app"
652
- puts "#{error.class}: #{error.message}"
653
- puts error.backtrace
654
- data[:app][:load_error] =
655
- "#{error.class}: #{error.message}\n#{error.backtrace.join("\n")}"
656
- ensure
657
- ENV.delete("_APPSIGNAL_CONFIG_FILE_ENV")
658
- end
659
-
660
- def rails_present?
661
- # Try and load the Rails gem
662
- require "rails"
663
- true
664
- rescue LoadError
665
- false
666
- end
667
635
  end
668
636
  end
669
637
  end
@@ -36,6 +36,51 @@ module Appsignal
36
36
  "\e[#{color_code}m#{text}\e[#{reset_color_code}m"
37
37
  end
38
38
 
39
+ def print_empty_line
40
+ puts "\n"
41
+ end
42
+
43
+ def rails_present?
44
+ require "rails"
45
+ true
46
+ rescue LoadError
47
+ false
48
+ end
49
+
50
+ # The Rails gem being loadable says nothing about the directory the
51
+ # command runs in, so check for the app itself as well.
52
+ def rails_app_present?
53
+ rails_present? &&
54
+ File.exist?(Appsignal::Utils::RailsHelper.environment_config_path)
55
+ end
56
+
57
+ def load_rails_app(environment)
58
+ # Pass the environment given as a command line option to the app, so
59
+ # that the AppSignal config file uses it when the app loads it.
60
+ ENV["_APPSIGNAL_CONFIG_FILE_ENV"] = environment
61
+ # Require the railtie manually. It was not loaded when AppSignal
62
+ # loaded, because the `Rails` constant was not present at that point.
63
+ require "appsignal/integrations/railtie"
64
+ # Start the Rails app, including its railties and initializers.
65
+ require Appsignal::Utils::RailsHelper.environment_config_path
66
+ ensure
67
+ ENV.delete("_APPSIGNAL_CONFIG_FILE_ENV")
68
+ end
69
+
70
+ # Yields the error when the app fails to load, so that a command can
71
+ # report it in its own way.
72
+ def require_rails_app_if_present(environment)
73
+ return unless rails_app_present?
74
+
75
+ load_rails_app(environment)
76
+ rescue LoadError, StandardError => error
77
+ print_empty_line
78
+ puts "ERROR: Error encountered while loading the Rails app"
79
+ puts "#{error.class}: #{error.message}"
80
+ puts error.backtrace
81
+ yield error if block_given?
82
+ end
83
+
39
84
  def periods
40
85
  3.times do
41
86
  print "."
@@ -21,6 +21,59 @@ module Appsignal
21
21
  defined?(::ActiveJob)
22
22
  end
23
23
 
24
+ # The parameters of the Active Job method this hook wraps to record a
25
+ # bulk enqueue. The wrapper reads the array of jobs out of the second
26
+ # one, so it is only safe to wrap a method that still takes these.
27
+ EXPECTED_INSTRUMENT_ENQUEUE_ALL_PARAMETERS = [
28
+ [:req, :queue_adapter],
29
+ [:req, :jobs]
30
+ ].freeze
31
+
32
+ # Whether Active Job records a bulk enqueue through a method at all. It
33
+ # only does so from version 7.1 on. Checking for the method, rather than
34
+ # for the version, keeps us from wrapping one on a version that has no
35
+ # bulk enqueue path to instrument.
36
+ def self.instrument_enqueue_all_defined?
37
+ ::ActiveJob.singleton_class.private_method_defined?(:instrument_enqueue_all)
38
+ end
39
+
40
+ # Whether that method still takes the arguments the wrapper reads.
41
+ #
42
+ # The method is private, so a later version of Active Job can change
43
+ # what it takes. Wrapping one that takes something else raises an
44
+ # ArgumentError inside every `ActiveJob.perform_all_later` call an
45
+ # application makes, which breaks enqueuing and not just its
46
+ # instrumentation. Refusing to wrap it is therefore the safe direction,
47
+ # whatever the refusal costs us.
48
+ def self.instrument_enqueue_all_parameters_match?
49
+ instrument_enqueue_all_parameters == EXPECTED_INSTRUMENT_ENQUEUE_ALL_PARAMETERS
50
+ end
51
+
52
+ # Active Job's own definition of the method, looked up past the wrapper
53
+ # this hook prepends. Installing twice would otherwise find that wrapper
54
+ # rather than the method it wraps, and so check it against itself. Only
55
+ # the wrapper is skipped, so a version of Active Job that defines the
56
+ # method somewhere else is still found.
57
+ def self.instrument_enqueue_all_method
58
+ method = ::ActiveJob.singleton_class.instance_method(:instrument_enqueue_all)
59
+ method = method.super_method while method&.owner == ActiveJobBulkEnqueueInstrumentation
60
+ method
61
+ end
62
+
63
+ def self.instrument_enqueue_all_parameters
64
+ instrument_enqueue_all_method&.parameters
65
+ end
66
+
67
+ # Names both sides of the mismatch, so a log line is enough to tell what
68
+ # Active Job changed and what the wrapper was written against.
69
+ def self.instrument_enqueue_all_mismatch_message
70
+ "Not instrumenting Active Job bulk enqueues: " \
71
+ "`ActiveJob.instrument_enqueue_all` takes " \
72
+ "#{instrument_enqueue_all_parameters.inspect} in this version of " \
73
+ "Active Job, where AppSignal expects " \
74
+ "#{EXPECTED_INSTRUMENT_ENQUEUE_ALL_PARAMETERS.inspect}."
75
+ end
76
+
24
77
  def dependencies_present?
25
78
  self.class.dependencies_present? && Appsignal.config &&
26
79
  Appsignal.config[:instrument_active_job]
@@ -33,6 +86,32 @@ module Appsignal
33
86
  ::ActiveJob::Base
34
87
  .prepend ::Appsignal::Hooks::ActiveJobHook::ActiveJobEnqueueInstrumentation
35
88
 
89
+ # Wrap the method Active Job records a bulk enqueue through, but only
90
+ # when it is still the method the wrapper knows how to read. When it
91
+ # is not, the batch goes unrecorded: Rails' own notification stays
92
+ # suppressed, because a worse event is not worth reporting in place
93
+ # of the one we set out to report.
94
+ if !Appsignal::Hooks::ActiveJobHook.instrument_enqueue_all_defined?
95
+ # Nothing to instrument on a version with no bulk enqueue path, so
96
+ # there is nothing to report either.
97
+ Appsignal.internal_logger.debug(
98
+ "Not instrumenting Active Job bulk enqueues: this version of " \
99
+ "Active Job does not record them through " \
100
+ "`ActiveJob.instrument_enqueue_all`."
101
+ )
102
+ elsif !Appsignal::Hooks::ActiveJobHook.instrument_enqueue_all_parameters_match?
103
+ # A bulk enqueue path exists, but not one that can be wrapped
104
+ # without breaking `ActiveJob.perform_all_later` for the whole
105
+ # application. Report that at a level someone will see, because a
106
+ # batch that used to be recorded no longer is.
107
+ Appsignal.internal_logger.warn(
108
+ Appsignal::Hooks::ActiveJobHook.instrument_enqueue_all_mismatch_message
109
+ )
110
+ else
111
+ ::ActiveJob.singleton_class
112
+ .prepend ::Appsignal::Hooks::ActiveJobHook::ActiveJobBulkEnqueueInstrumentation
113
+ end
114
+
36
115
  next unless Appsignal::Hooks::ActiveJobHook.version_7_1_or_higher?
37
116
 
38
117
  # Only works on Active Job 7.1 and newer
@@ -80,6 +159,67 @@ module Appsignal
80
159
  end
81
160
  end
82
161
 
162
+ # Records an `enqueue_all.active_job` event when a batch of jobs is
163
+ # enqueued with `ActiveJob.perform_all_later`, so the batch shows up on the
164
+ # active transaction's timeline as one event.
165
+ #
166
+ # This wraps `instrument_enqueue_all` rather than `perform_all_later`, for
167
+ # two reasons. It is the method that records the batch, so it is called
168
+ # once for each queue adapter the batch spans, which is the same event
169
+ # count as the native notification it replaces. And it runs inside
170
+ # `perform_all_later`, after Active Job has split off the jobs it defers
171
+ # until the database transaction commits, so each of those halves is
172
+ # recorded when it is really enqueued.
173
+ #
174
+ # @!visibility private
175
+ module ActiveJobBulkEnqueueInstrumentation
176
+ private
177
+
178
+ def instrument_enqueue_all(_queue_adapter, jobs)
179
+ # Skip recording the event when enqueue events are suppressed, which is
180
+ # also the case when enqueue instrumentation is disabled. Same check as
181
+ # the single-job path above.
182
+ if Appsignal::Transaction.current? &&
183
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
184
+ return super
185
+ end
186
+
187
+ Appsignal.instrument("enqueue_all.active_job", bulk_enqueue_title(jobs)) do
188
+ # A bulk enqueue does not go through `ActiveJob::Base#enqueue`, so
189
+ # nothing has suppressed the adapter (Sidekiq, Resque, ...) yet, and
190
+ # its own enqueue instrumentation would record an event for every job
191
+ # in the batch. Suppress it so the batch is recorded once, as this
192
+ # event.
193
+ if Appsignal::Transaction.current?
194
+ Appsignal::Transaction.current.suppress_job_enqueue_events { super }
195
+ else
196
+ super
197
+ end
198
+ end
199
+ end
200
+
201
+ # The batch's job class, when every job in it has the same one. Active
202
+ # Job groups the jobs it enqueues by queue adapter rather than by class,
203
+ # so a batch can mix classes, and then there is no one class to name.
204
+ def bulk_enqueue_title(jobs)
205
+ job_class = shared_across(jobs) { |job| job.class.name }
206
+ return "bulk enqueue jobs" unless job_class
207
+
208
+ "bulk enqueue #{job_class} jobs"
209
+ end
210
+
211
+ # The one value every job in the batch shares, or nil when they differ
212
+ # or the batch is empty. Stops at the first job that disagrees, because
213
+ # a batch is as large as the caller made it and a single mismatch is
214
+ # enough to know.
215
+ def shared_across(jobs)
216
+ return if jobs.empty?
217
+
218
+ first = yield(jobs.first)
219
+ jobs.all? { |job| yield(job) == first } ? first : nil
220
+ end
221
+ end
222
+
83
223
  module ActiveJobClassInstrumentation
84
224
  def execute(job)
85
225
  enqueued_at = job["enqueued_at"]
@@ -9,10 +9,15 @@ module Appsignal
9
9
 
10
10
  # Events a dedicated AppSignal integration already records, so the
11
11
  # generic notifications path must not record them a second time. The
12
- # ActiveJob hook owns `enqueue.active_job` (it wraps the enqueue in its
13
- # own event, with Rails' native notification nested inside), and the
12
+ # ActiveJob hook owns `enqueue.active_job` and `enqueue_all.active_job`.
13
+ # It records its own event for a single enqueue, and one event for a
14
+ # whole batch, with Rails' native notification nested inside. The
14
15
  # Faraday integration owns `request.faraday`.
15
- SUPPRESSED_EVENT_NAMES = ["enqueue.active_job", "request.faraday"].freeze
16
+ SUPPRESSED_EVENT_NAMES = [
17
+ "enqueue.active_job",
18
+ "enqueue_all.active_job",
19
+ "request.faraday"
20
+ ].freeze
16
21
 
17
22
  def start_event(name)
18
23
  return unless record_event?(name)
@@ -11,6 +11,18 @@ module Appsignal
11
11
  enqueue_with_instrumentation(job, block)
12
12
  end
13
13
 
14
+ # Delayed Job asks a job for its own maximum run time before it invokes
15
+ # it, and answering that question reads the job's payload. A job whose
16
+ # payload will not deserialize therefore raises inside the worker and
17
+ # never reaches `invoke_job`, so the callback below does not run and the
18
+ # failure goes unreported. This callback sits one step further out, on
19
+ # the worker's whole run of the job, which is where such a job can still
20
+ # be seen.
21
+ lifecycle.around(:perform) do |worker, job, &block|
22
+ report_job_that_cannot_be_loaded(job)
23
+ block.call(worker, job)
24
+ end
25
+
14
26
  lifecycle.around(:invoke_job) do |job, &block|
15
27
  invoke_with_instrumentation(job, block)
16
28
  end
@@ -51,6 +63,34 @@ module Appsignal
51
63
  job.name
52
64
  end
53
65
 
66
+ # Reports a job whose payload cannot be read, and does nothing at all for
67
+ # a job that loads. That is what keeps every ordinary job instrumented by
68
+ # the `:invoke_job` callback alone: no transaction is opened here, so an
69
+ # ordinary job's reported duration and its reported error are exactly
70
+ # what they were before this callback existed.
71
+ #
72
+ # Reading the payload here reads it a moment before the worker would have
73
+ # read it anyway. Delayed Job memoizes a payload that loads, so a job
74
+ # that is fine is deserialized once either way.
75
+ #
76
+ # Nothing is re-raised. The worker reads the same payload immediately
77
+ # after this returns, raises the same error, and fails the job as it
78
+ # always has. Raising here instead would move that error out of the
79
+ # worker's own handling and into the loop that reserves jobs.
80
+ def self.report_job_that_cannot_be_loaded(job)
81
+ job.payload_object
82
+ nil
83
+ rescue Exception => error
84
+ warn_unreadable_payload_once(error)
85
+
86
+ transaction =
87
+ Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
88
+ transaction.set_action_if_nil(action_name_without_payload(job))
89
+ transaction.set_error(error)
90
+ add_job_metadata(transaction, job)
91
+ Appsignal::Transaction.complete_current!
92
+ end
93
+
54
94
  def self.invoke_with_instrumentation(job, block)
55
95
  transaction =
56
96
  Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
@@ -63,39 +103,106 @@ module Appsignal
63
103
  transaction.set_error(error)
64
104
  raise
65
105
  ensure
66
- payload = job.payload_object
67
- if payload.respond_to? :job_data
68
- # ActiveJob
69
- job_data = payload.job_data
70
- transaction.set_action_if_nil("#{job_data["job_class"]}#perform")
71
- transaction.add_params_if_nil(job_data.fetch("arguments", {}))
72
- else
73
- # Delayed Job
74
- transaction.set_action_if_nil(action_name_from_payload(payload, job.name))
75
- transaction.add_params_if_nil(extract_value(payload, :args, {}))
106
+ # Delayed Job raises when a job's handler will not deserialize, and it
107
+ # raises again on every further attempt to read it. Reading the payload
108
+ # here without a guard therefore replaces the error the job already
109
+ # failed with, and skips the rest of this block, so the transaction is
110
+ # never completed and the failure is never reported.
111
+ begin
112
+ payload = job.payload_object
113
+ if payload.respond_to? :job_data
114
+ # ActiveJob
115
+ job_data = payload.job_data
116
+ transaction.set_action_if_nil("#{job_data["job_class"]}#perform")
117
+ transaction.add_params_if_nil(job_data.fetch("arguments", {}))
118
+ else
119
+ # Delayed Job
120
+ transaction.set_action_if_nil(action_name_from_payload(payload, job.name))
121
+ transaction.add_params_if_nil(extract_value(payload, :args, {}))
122
+ end
123
+ rescue => error
124
+ warn_unreadable_payload_once(error)
125
+ transaction.set_action_if_nil(action_name_without_payload(job))
76
126
  end
77
127
 
78
- transaction.add_tags(
79
- :id => extract_value(job, :id, nil, true),
80
- :queue => extract_value(job, :queue),
81
- :priority => extract_value(job, :priority, 0),
82
- :attempts => extract_value(job, :attempts, 0)
83
- )
84
-
85
- transaction.set_queue_start(extract_value(job, :run_at)&.to_i&.* 1_000)
128
+ add_job_metadata(transaction, job)
86
129
 
87
130
  Appsignal::Transaction.complete_current!
88
131
  end
89
132
  end
90
133
 
134
+ def self.add_job_metadata(transaction, job)
135
+ transaction.add_tags(
136
+ :id => extract_value(job, :id, nil, true),
137
+ :queue => extract_value(job, :queue),
138
+ :priority => extract_value(job, :priority, 0),
139
+ :attempts => extract_value(job, :attempts, 0)
140
+ )
141
+
142
+ transaction.set_queue_start(extract_value(job, :run_at)&.to_i&.* 1_000)
143
+ end
144
+
145
+ # The name Delayed Job derives from the raw handler when the payload will
146
+ # not deserialize. It reads the class name out of the handler with a
147
+ # regular expression, which covers the job class a deploy removed. That
148
+ # expression raises for a handler it does not match, and it is skipped
149
+ # altogether when the payload raised something Delayed Job does not wrap,
150
+ # so fall back to a name that says the failure came from inside Delayed
151
+ # Job. The failure is then reported under a name that can be found, and
152
+ # every job this happens to is grouped together.
153
+ def self.action_name_without_payload(job)
154
+ with_perform_suffix(job.name)
155
+ rescue
156
+ "DelayedJobInternal"
157
+ end
158
+
159
+ # Guards the check-and-set below, so two threads that read an unreadable
160
+ # payload at the same time cannot both warn. A constant so it is created
161
+ # once at load time, because creating it lazily would race in turn.
162
+ WARN_ONCE_LOCK = Mutex.new
163
+
164
+ # A deploy that removes a job class leaves every job of that class unable
165
+ # to deserialize, so this can be reached once per job. Each of those jobs
166
+ # reports its own error to AppSignal, so the log only has to say once that
167
+ # it is happening.
168
+ def self.warn_unreadable_payload_once(error)
169
+ should_warn = WARN_ONCE_LOCK.synchronize do
170
+ next false if @warned_unreadable_payload
171
+
172
+ @warned_unreadable_payload = true
173
+ end
174
+ return unless should_warn
175
+
176
+ Appsignal.internal_logger.warn(
177
+ "Unable to read a Delayed Job job's payload: #{error.class}: " \
178
+ "#{error.message}. Jobs whose payload cannot be read are reported " \
179
+ "without parameters. They are named after the class in their raw " \
180
+ "handler, or DelayedJobInternal when that cannot be read either."
181
+ )
182
+ end
183
+
184
+ # @!visibility private
185
+ #
186
+ # Resets the warn-once state. Only used to keep test runs isolated.
187
+ def self.reset_unreadable_payload_warning!
188
+ WARN_ONCE_LOCK.synchronize { @warned_unreadable_payload = false }
189
+ end
190
+
91
191
  def self.action_name_from_payload(payload, default_name)
92
192
  # Attempt to find appsignal_name override
93
193
  class_and_method_name = extract_value(payload, :appsignal_name, nil)
94
194
  return class_and_method_name if class_and_method_name.is_a?(String)
95
- return default_name if default_name.split("#").length == 2
96
- return default_name if default_name.split(".").length == 2
97
195
 
98
- "#{default_name}#perform"
196
+ with_perform_suffix(default_name)
197
+ end
198
+
199
+ # An action name is a class and a method. A name that already names both,
200
+ # separated either way, is left alone.
201
+ def self.with_perform_suffix(name)
202
+ return name if name.split("#").length == 2
203
+ return name if name.split(".").length == 2
204
+
205
+ "#{name}#perform"
99
206
  end
100
207
 
101
208
  # rubocop:disable Style/OptionalBooleanParameter
@@ -15,26 +15,56 @@ module Appsignal
15
15
 
16
16
  def add_transaction_metadata_after(transaction, request)
17
17
  endpoint = request.env["api.endpoint"]
18
- unless endpoint&.options
18
+ request_method, klass, path = endpoint && endpoint_action(endpoint)
19
+ unless path
19
20
  super
20
21
  return
21
22
  end
22
23
 
24
+ transaction.set_action_if_nil("#{request_method}::#{klass}##{path}")
25
+
26
+ super
27
+
28
+ transaction.set_metadata("path", path)
29
+ end
30
+
31
+ # Returns the HTTP method, API class and path that make up the action
32
+ # name, or nil when the endpoint does not describe a route.
33
+ def endpoint_action(endpoint)
23
34
  options = endpoint.options
24
- request_method = options[:method].first.to_s.upcase
25
- klass = options[:for]
35
+ return unless options
36
+
37
+ if options.key?(:path)
38
+ # Only Grape 3 and older populate `options[:path]`. Their route is
39
+ # readable too, but its path is the full route template, so reading
40
+ # it would rename the actions these applications already report.
41
+ endpoint_action_from_options(endpoint, options)
42
+ else
43
+ # Grape 4 keeps these three in a value object behind a protected
44
+ # reader, leaving the route as the only public source.
45
+ endpoint_action_from_route(endpoint)
46
+ end
47
+ end
48
+
49
+ def endpoint_action_from_options(endpoint, options)
26
50
  namespace = endpoint.namespace
27
51
  namespace = "" if namespace == "/"
28
52
 
29
53
  path = options[:path].first.to_s
30
54
  path = "/#{path}" if path[0] != "/"
31
- path = "#{namespace}#{path}"
32
55
 
33
- transaction.set_action_if_nil("#{request_method}::#{klass}##{path}")
56
+ [
57
+ options[:method].first.to_s.upcase,
58
+ options[:for],
59
+ "#{namespace}#{path}"
60
+ ]
61
+ end
34
62
 
35
- super
63
+ def endpoint_action_from_route(endpoint)
64
+ route = endpoint.routes.first
65
+ return unless route
36
66
 
37
- transaction.set_metadata("path", path)
67
+ [route.request_method.to_s.upcase, endpoint.api, route.origin]
38
68
  end
39
69
  end
40
70
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Appsignal
4
4
  # @return [String]
5
- VERSION = "4.10.1"
5
+ VERSION = "4.10.2"
6
6
  end
data/sig/appsignal.rbi CHANGED
@@ -8,7 +8,7 @@
8
8
  module Appsignal
9
9
  extend Appsignal::Helpers::Metrics
10
10
  extend Appsignal::Helpers::Instrumentation
11
- VERSION = T.let("4.10.1", T.untyped)
11
+ VERSION = T.let("4.10.2", T.untyped)
12
12
 
13
13
  class << self
14
14
  # The loaded AppSignal configuration.
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: 4.10.1
4
+ version: 4.10.2
5
5
  platform: java
6
6
  authors:
7
7
  - Robert Beekman