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 +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +2 -1
- data/appsignal.gemspec +7 -0
- data/build_matrix.yml +8 -2
- data/ext/agent.rb +27 -27
- data/lib/appsignal/cli/demo.rb +5 -0
- data/lib/appsignal/cli/diagnose.rb +6 -38
- data/lib/appsignal/cli/helpers.rb +45 -0
- data/lib/appsignal/hooks/active_job.rb +140 -0
- data/lib/appsignal/integrations/active_support_notifications.rb +8 -3
- data/lib/appsignal/integrations/delayed_job_plugin.rb +128 -21
- data/lib/appsignal/rack/grape_middleware.rb +37 -7
- data/lib/appsignal/version.rb +1 -1
- data/sig/appsignal.rbi +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39bff03e12f0f39edebab29adeb15d316efb0fe813ca087b1b914d083081cbd6
|
|
4
|
+
data.tar.gz: 88b3ce4fabea7ad7c05306c0d0f5763d844c2dcf249c2168d46cc8f964646601
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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" => "
|
|
17
|
+
"checksum" => "1c05ef4cd4f0d0646bbc00a61568448cafd6c8cd23e152a4d7e22bab7e733cf1",
|
|
18
18
|
"filename" => "appsignal-x86_64-darwin-all-static.tar.gz"
|
|
19
19
|
},
|
|
20
20
|
"dynamic" => {
|
|
21
|
-
"checksum" => "
|
|
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" => "
|
|
27
|
+
"checksum" => "1c05ef4cd4f0d0646bbc00a61568448cafd6c8cd23e152a4d7e22bab7e733cf1",
|
|
28
28
|
"filename" => "appsignal-x86_64-darwin-all-static.tar.gz"
|
|
29
29
|
},
|
|
30
30
|
"dynamic" => {
|
|
31
|
-
"checksum" => "
|
|
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" => "
|
|
37
|
+
"checksum" => "031d0f3c32302271274d1e602c9cecb2b037e9dd338001901ff4ed4d0e457075",
|
|
38
38
|
"filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
|
|
39
39
|
},
|
|
40
40
|
"dynamic" => {
|
|
41
|
-
"checksum" => "
|
|
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" => "
|
|
47
|
+
"checksum" => "031d0f3c32302271274d1e602c9cecb2b037e9dd338001901ff4ed4d0e457075",
|
|
48
48
|
"filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
|
|
49
49
|
},
|
|
50
50
|
"dynamic" => {
|
|
51
|
-
"checksum" => "
|
|
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" => "
|
|
57
|
+
"checksum" => "031d0f3c32302271274d1e602c9cecb2b037e9dd338001901ff4ed4d0e457075",
|
|
58
58
|
"filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
|
|
59
59
|
},
|
|
60
60
|
"dynamic" => {
|
|
61
|
-
"checksum" => "
|
|
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" => "
|
|
67
|
+
"checksum" => "48499ed06fda433dc7347a4a3d23944d0bfb31604e5d7f52db0a0bbe437f8e03",
|
|
68
68
|
"filename" => "appsignal-aarch64-linux-all-static.tar.gz"
|
|
69
69
|
},
|
|
70
70
|
"dynamic" => {
|
|
71
|
-
"checksum" => "
|
|
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" => "
|
|
77
|
+
"checksum" => "41600ba4171cb8549ff9c8b675e9b4ef100eb7ef0a001a970964af04dc209738",
|
|
78
78
|
"filename" => "appsignal-i686-linux-all-static.tar.gz"
|
|
79
79
|
},
|
|
80
80
|
"dynamic" => {
|
|
81
|
-
"checksum" => "
|
|
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" => "
|
|
87
|
+
"checksum" => "41600ba4171cb8549ff9c8b675e9b4ef100eb7ef0a001a970964af04dc209738",
|
|
88
88
|
"filename" => "appsignal-i686-linux-all-static.tar.gz"
|
|
89
89
|
},
|
|
90
90
|
"dynamic" => {
|
|
91
|
-
"checksum" => "
|
|
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" => "
|
|
97
|
+
"checksum" => "8240c494573360ad147aa56a973fe809bf1f3976383b8cd990fc0824e5554913",
|
|
98
98
|
"filename" => "appsignal-x86_64-linux-all-static.tar.gz"
|
|
99
99
|
},
|
|
100
100
|
"dynamic" => {
|
|
101
|
-
"checksum" => "
|
|
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" => "
|
|
107
|
+
"checksum" => "39de4602e300f701ed28748bd1c523bedf44f2d0f6241fffe370485185a895cc",
|
|
108
108
|
"filename" => "appsignal-x86_64-linux-musl-all-static.tar.gz"
|
|
109
109
|
},
|
|
110
110
|
"dynamic" => {
|
|
111
|
-
"checksum" => "
|
|
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" => "
|
|
117
|
+
"checksum" => "13fa41ad765a9c79f374dad4d131e7860afb8760c11508af1de982580798a3bc",
|
|
118
118
|
"filename" => "appsignal-aarch64-linux-musl-all-static.tar.gz"
|
|
119
119
|
},
|
|
120
120
|
"dynamic" => {
|
|
121
|
-
"checksum" => "
|
|
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" => "
|
|
127
|
+
"checksum" => "c519b132c7a1de2a8dbb8c36d87fe91b4c0451d2c65d8bd4f44b748bb2ad2904",
|
|
128
128
|
"filename" => "appsignal-x86_64-freebsd-all-static.tar.gz"
|
|
129
129
|
},
|
|
130
130
|
"dynamic" => {
|
|
131
|
-
"checksum" => "
|
|
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" => "
|
|
137
|
+
"checksum" => "c519b132c7a1de2a8dbb8c36d87fe91b4c0451d2c65d8bd4f44b748bb2ad2904",
|
|
138
138
|
"filename" => "appsignal-x86_64-freebsd-all-static.tar.gz"
|
|
139
139
|
},
|
|
140
140
|
"dynamic" => {
|
|
141
|
-
"checksum" => "
|
|
141
|
+
"checksum" => "fb5ea3aac557543de74842d668c66ede00d125e1e86f601fa5ffe002edbd6c62",
|
|
142
142
|
"filename" => "appsignal-x86_64-freebsd-all-dynamic.tar.gz"
|
|
143
143
|
}
|
|
144
144
|
}
|
data/lib/appsignal/cli/demo.rb
CHANGED
|
@@ -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`
|
|
13
|
-
# own event
|
|
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 = [
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
56
|
+
[
|
|
57
|
+
options[:method].first.to_s.upcase,
|
|
58
|
+
options[:for],
|
|
59
|
+
"#{namespace}#{path}"
|
|
60
|
+
]
|
|
61
|
+
end
|
|
34
62
|
|
|
35
|
-
|
|
63
|
+
def endpoint_action_from_route(endpoint)
|
|
64
|
+
route = endpoint.routes.first
|
|
65
|
+
return unless route
|
|
36
66
|
|
|
37
|
-
|
|
67
|
+
[route.request_method.to_s.upcase, endpoint.api, route.origin]
|
|
38
68
|
end
|
|
39
69
|
end
|
|
40
70
|
end
|
data/lib/appsignal/version.rb
CHANGED
data/sig/appsignal.rbi
CHANGED