skylight 7.1.1 → 7.2.0.beta
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 +9 -0
- data/lib/skylight/extensions/source_location.rb +56 -37
- data/lib/skylight/normalizers/action_controller/send_file.rb +11 -39
- data/lib/skylight/normalizers/grape/endpoint.rb +23 -3
- data/lib/skylight/normalizers/grape/endpoint_run.rb +1 -1
- data/lib/skylight/normalizers.rb +4 -10
- data/lib/skylight/probes/redis.rb +59 -17
- data/lib/skylight/version.rb +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: dccfccf293fe88129f1bb1e0ae18e6bc8f242e1d9cb06ce3cdf7af0d47f5c77f
|
|
4
|
+
data.tar.gz: 99feb9962ef72af53930bdd89f25fad9cfd2943abbe4c5f3806bb768975c61b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b78cdd01a03ccc74916c20de4a857592e52c95d087542b9adaa3c685429d8317f36879a25fb28f23ec07bcb81caf6b6829a7306b40c44dc7f000d00b20bd320
|
|
7
|
+
data.tar.gz: dfcd97b684460f645c3b8e828cec9f326e0b896204114f87a633f3b4c0657cf8761961d7fd47bc908f36ab62f26e7f87240179f1e6f818d9f17c1dae365c6665
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 7.2.0.beta (November 4, 2026)
|
|
2
|
+
- BUGFIX Don't trigger the `Mime::SET` deprecation warning on Rails 8.2.
|
|
3
|
+
- IMPROVEMENT The `redis` probe now instruments redis-client, including when it's used without the redis gem (e.g. by Sidekiq 7+). With redis 5+, which is built on redis-client, blocking commands (e.g. `BLPOP`) are now instrumented too.
|
|
4
|
+
- IMPROVEMENT Redis connections, including TLS and the connection handshake, are now recorded as their own `CONNECT` span instead of being counted toward the command that opened them.
|
|
5
|
+
- IMPROVEMENT With redis 5+, `PIPELINE` and `MULTI` spans now cover only the round trip to Redis, not the time spent running the block. Empty pipelines are no longer recorded.
|
|
6
|
+
- BUGFIX Don't report a probe installation error when a `Redis` constant is defined by something other than the redis gem.
|
|
7
|
+
- BUGFIX Reduce the overhead of source location tracking. File paths are now classified once and cached, rather than re-processed for every span.
|
|
8
|
+
- BUGFIX Fix Grape endpoint names on Grape 4.0+.
|
|
9
|
+
|
|
1
10
|
## 7.1.1 (April 17, 2026)
|
|
2
11
|
- BUGFIX Fix native extension build on GCC 15 by using `append_cflags` instead of mutating `$CFLAGS` directly.
|
|
3
12
|
- BUGFIX Replace K&R-style empty parameter lists (`()`) with `(void)` in C sources to build cleanly under Fedora's `-Werror=old-style-definition`.
|
|
@@ -13,11 +13,23 @@ module Skylight
|
|
|
13
13
|
META_KEYS = %i[source_location source_file source_line].freeze
|
|
14
14
|
MAX_CALLER_DEPTH = 75
|
|
15
15
|
|
|
16
|
+
# Upper bound for each path cache. These are keyed by file path, so in practice they're bounded by
|
|
17
|
+
# the number of files that show up in backtraces; the limit only guards against pathological growth.
|
|
18
|
+
PATH_CACHE_MAX_SIZE = 10_000
|
|
19
|
+
|
|
16
20
|
def initialize(*)
|
|
17
21
|
super
|
|
18
22
|
cache_size = (config[:source_location_cache_size] || 1000).to_i
|
|
19
|
-
@caller_cache = Util::LruCache.new(cache_size)
|
|
20
23
|
@instance_method_source_location_cache = Util::LruCache.new(cache_size)
|
|
24
|
+
|
|
25
|
+
# path (by value) => [sanitized_path, include_line] or false
|
|
26
|
+
@path_info_cache = {}
|
|
27
|
+
|
|
28
|
+
# Same as above, but keyed by object identity. Ruby interns the path of each loaded file,
|
|
29
|
+
# so every backtrace location in a given file returns the same String object from
|
|
30
|
+
# `absolute_path`, which makes this a cheap lookup while walking frames.
|
|
31
|
+
@frame_path_info_cache = {}.compare_by_identity
|
|
32
|
+
|
|
21
33
|
gem_require_trie # memoize this at startup
|
|
22
34
|
end
|
|
23
35
|
|
|
@@ -53,7 +65,7 @@ module Skylight
|
|
|
53
65
|
meta[:source_line] = instrument_location.lineno
|
|
54
66
|
else
|
|
55
67
|
warn "Ignoring source_line without source_file" if source_line
|
|
56
|
-
if (location = find_caller
|
|
68
|
+
if (location = find_caller)
|
|
57
69
|
meta[:source_file] = location.absolute_path
|
|
58
70
|
meta[:source_line] = location.lineno
|
|
59
71
|
end
|
|
@@ -75,10 +87,10 @@ module Skylight
|
|
|
75
87
|
[opts[:source_file], opts[:source_line]]
|
|
76
88
|
end
|
|
77
89
|
|
|
78
|
-
sl ||= source_location(payload, meta
|
|
90
|
+
sl ||= source_location(payload, meta)
|
|
79
91
|
|
|
80
92
|
if sl
|
|
81
|
-
|
|
93
|
+
t { "normalizer source_location=#{sl}" }
|
|
82
94
|
meta[:source_file], meta[:source_line] = sl
|
|
83
95
|
end
|
|
84
96
|
|
|
@@ -105,7 +117,7 @@ module Skylight
|
|
|
105
117
|
warn "Ignoring source_line without source_file; source_line=#{source_line}"
|
|
106
118
|
end
|
|
107
119
|
|
|
108
|
-
|
|
120
|
+
t { "source_location=#{meta[:source_location]}" } if meta[:source_location]
|
|
109
121
|
end
|
|
110
122
|
|
|
111
123
|
def allowed_meta_keys
|
|
@@ -122,27 +134,21 @@ module Skylight
|
|
|
122
134
|
|
|
123
135
|
# from normalizers.rb
|
|
124
136
|
# Returns an array of file and line
|
|
125
|
-
def source_location(payload, meta
|
|
137
|
+
def source_location(payload, meta)
|
|
126
138
|
# FIXME: what should precedence be?
|
|
127
139
|
if meta.is_a?(Hash) && meta[:source_location]
|
|
128
140
|
meta.delete(:source_location)
|
|
129
141
|
elsif payload.is_a?(Hash) && payload[:sk_source_location]
|
|
130
142
|
payload[:sk_source_location]
|
|
131
|
-
elsif (location = find_caller
|
|
143
|
+
elsif (location = find_caller)
|
|
132
144
|
[location.absolute_path, location.lineno]
|
|
133
145
|
end
|
|
134
146
|
end
|
|
135
147
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if cache_key
|
|
141
|
-
localized_cache_key = [cache_key, locations.map(&:lineno)].hash
|
|
142
|
-
@caller_cache.fetch(localized_cache_key) { find_caller_inner(locations) }
|
|
143
|
-
else
|
|
144
|
-
find_caller_inner(locations)
|
|
145
|
-
end
|
|
148
|
+
# Returns the first location (starting at 4 to skip Skylight extension processing logic)
|
|
149
|
+
# that is in a non-ignored gem or in the project.
|
|
150
|
+
def find_caller
|
|
151
|
+
::Kernel.caller_locations(4..MAX_CALLER_DEPTH).find { |l| frame_path_info(l.absolute_path) }
|
|
146
152
|
end
|
|
147
153
|
|
|
148
154
|
def project_path?(path)
|
|
@@ -189,23 +195,45 @@ module Skylight
|
|
|
189
195
|
end
|
|
190
196
|
|
|
191
197
|
def sanitize_source_location(path, line)
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
return unless (info = path_info(path))
|
|
199
|
+
|
|
200
|
+
sanitized_path, include_line = info
|
|
201
|
+
include_line && line ? "#{sanitized_path}:#{line}" : sanitized_path
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def path_info(path)
|
|
207
|
+
return false unless path
|
|
208
|
+
|
|
209
|
+
@path_info_cache.fetch(path) do
|
|
210
|
+
@path_info_cache.clear if @path_info_cache.size >= PATH_CACHE_MAX_SIZE
|
|
211
|
+
@path_info_cache[path] = compute_path_info(path)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def frame_path_info(path)
|
|
216
|
+
return false unless path
|
|
217
|
+
|
|
218
|
+
@frame_path_info_cache.fetch(path) do
|
|
219
|
+
@frame_path_info_cache.clear if @frame_path_info_cache.size >= PATH_CACHE_MAX_SIZE
|
|
220
|
+
@frame_path_info_cache[path] = path_info(path)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Returns [sanitized_path, include_line], or false if the path should not be reported.
|
|
225
|
+
# Gems are reported by name only; project files relative to the root, with a line number.
|
|
226
|
+
def compute_path_info(path)
|
|
227
|
+
# Do this first since gems may be vendored in the app repo.
|
|
194
228
|
if (gem_name = find_source_gem(path))
|
|
195
|
-
|
|
196
|
-
line = nil
|
|
229
|
+
[gem_name, false].freeze
|
|
197
230
|
elsif project_path?(path)
|
|
198
|
-
|
|
199
|
-
path = Pathname.new(path).relative_path_from(config.root).to_s
|
|
231
|
+
[Pathname.new(path).relative_path_from(config.root).to_s.freeze, true].freeze
|
|
200
232
|
else
|
|
201
|
-
|
|
233
|
+
false
|
|
202
234
|
end
|
|
203
|
-
|
|
204
|
-
line ? "#{path}:#{line}" : path
|
|
205
235
|
end
|
|
206
236
|
|
|
207
|
-
private
|
|
208
|
-
|
|
209
237
|
def gem_require_trie
|
|
210
238
|
@gem_require_trie ||=
|
|
211
239
|
begin
|
|
@@ -248,15 +276,6 @@ module Skylight
|
|
|
248
276
|
nil
|
|
249
277
|
end
|
|
250
278
|
|
|
251
|
-
def find_caller_inner(locations)
|
|
252
|
-
# Start at file before this one
|
|
253
|
-
# NOTE: We could start farther back now to avoid more Skylight files
|
|
254
|
-
locations.find do |l|
|
|
255
|
-
absolute_path = l.absolute_path
|
|
256
|
-
find_source_gem(absolute_path) || project_path?(absolute_path)
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
279
|
# walks up the inheritance tree until it finds the last method
|
|
261
280
|
# without a super_method definition.
|
|
262
281
|
def find_instance_method_super(constant, method_name)
|
|
@@ -1,49 +1,21 @@
|
|
|
1
1
|
module Skylight
|
|
2
2
|
module Normalizers
|
|
3
3
|
module ActionController
|
|
4
|
-
|
|
4
|
+
class SendFile < Normalizer
|
|
5
|
+
register "send_file.action_controller"
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
require "action_dispatch/http/mime_type"
|
|
9
|
-
require "action_dispatch/http/mime_types"
|
|
10
|
-
require "rack/utils"
|
|
11
|
-
rescue LoadError
|
|
12
|
-
enabled = false
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
if enabled
|
|
16
|
-
class SendFile < Normalizer
|
|
17
|
-
register "send_file.action_controller"
|
|
18
|
-
|
|
19
|
-
CAT = "app.controller.send_file".freeze
|
|
20
|
-
TITLE = "send file".freeze
|
|
21
|
-
|
|
22
|
-
def normalize(_trace, _name, _payload)
|
|
23
|
-
title = TITLE
|
|
24
|
-
|
|
25
|
-
# depending on normalization, we probably want this to eventually
|
|
26
|
-
# include the full path, but we need to make sure we have a good
|
|
27
|
-
# deduping strategy first.
|
|
28
|
-
desc = nil
|
|
29
|
-
|
|
30
|
-
[CAT, title, desc]
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
7
|
+
CAT = "app.controller.send_file".freeze
|
|
8
|
+
TITLE = "send file".freeze
|
|
34
9
|
|
|
35
|
-
|
|
36
|
-
|
|
10
|
+
def normalize(_trace, _name, _payload)
|
|
11
|
+
title = TITLE
|
|
37
12
|
|
|
38
|
-
|
|
39
|
-
|
|
13
|
+
# depending on normalization, we probably want this to eventually
|
|
14
|
+
# include the full path, but we need to make sure we have a good
|
|
15
|
+
# deduping strategy first.
|
|
16
|
+
desc = nil
|
|
40
17
|
|
|
41
|
-
|
|
42
|
-
Mime::SET.each_with_object({}) do |mime, hash|
|
|
43
|
-
hash[mime.symbol] = mime.to_s.dup.freeze
|
|
44
|
-
hash
|
|
45
|
-
end
|
|
46
|
-
end
|
|
18
|
+
[CAT, title, desc]
|
|
47
19
|
end
|
|
48
20
|
end
|
|
49
21
|
end
|
|
@@ -9,13 +9,33 @@ module Skylight
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
11
|
def get_method(endpoint)
|
|
12
|
-
|
|
13
|
-
method =
|
|
12
|
+
methods = http_methods(endpoint)
|
|
13
|
+
method = methods.first
|
|
14
|
+
method = "#{method}..." if methods.length > 1
|
|
14
15
|
method
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def get_path(endpoint)
|
|
18
|
-
endpoint.
|
|
19
|
+
paths(endpoint).join("/")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Grape 4+ no longer keeps an endpoint's methods, paths and API in `options`
|
|
23
|
+
|
|
24
|
+
def http_methods(endpoint)
|
|
25
|
+
# There's no public reader for these. Routes have them, but the endpoint here
|
|
26
|
+
# is a per-request copy that rebuilds its routes on every call, and automatic
|
|
27
|
+
# OPTIONS and 405 responses run on a route that has no endpoint to ask.
|
|
28
|
+
endpoint.options[:method] || endpoint.send(:config).http_methods
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def paths(endpoint)
|
|
32
|
+
endpoint.options[:path] ||
|
|
33
|
+
# Grape 4.0 has no public reader for these (Endpoint#path arrived in 4.1)
|
|
34
|
+
(endpoint.respond_to?(:path) ? endpoint.path : endpoint.send(:config).path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def api(endpoint)
|
|
38
|
+
endpoint.options[:for] || endpoint.api
|
|
19
39
|
end
|
|
20
40
|
|
|
21
41
|
def get_namespace(endpoint)
|
data/lib/skylight/normalizers.rb
CHANGED
|
@@ -65,12 +65,11 @@ module Skylight
|
|
|
65
65
|
|
|
66
66
|
def normalize_with_meta(trace, name, payload)
|
|
67
67
|
# If we have a normal response but no meta, add it
|
|
68
|
-
cat, title, desc, meta =
|
|
68
|
+
cat, title, desc, meta = normalize(trace, name, payload)
|
|
69
69
|
return cat if cat == :skip
|
|
70
70
|
|
|
71
71
|
meta ||= {}
|
|
72
|
-
|
|
73
|
-
process_meta(trace, name, payload, meta, cache_key: cache_key)
|
|
72
|
+
process_meta(trace, name, payload, meta)
|
|
74
73
|
|
|
75
74
|
[cat, title, desc, meta]
|
|
76
75
|
end
|
|
@@ -80,13 +79,8 @@ module Skylight
|
|
|
80
79
|
|
|
81
80
|
private
|
|
82
81
|
|
|
83
|
-
def process_meta(trace, _name, payload, meta
|
|
84
|
-
trace.instrumenter.extensions.process_normalizer_meta(
|
|
85
|
-
payload,
|
|
86
|
-
meta,
|
|
87
|
-
cache_key: cache_key,
|
|
88
|
-
**process_meta_options(payload)
|
|
89
|
-
)
|
|
82
|
+
def process_meta(trace, _name, payload, meta)
|
|
83
|
+
trace.instrumenter.extensions.process_normalizer_meta(payload, meta, **process_meta_options(payload))
|
|
90
84
|
end
|
|
91
85
|
|
|
92
86
|
def process_meta_options(_payload)
|
|
@@ -8,6 +8,9 @@ module Skylight
|
|
|
8
8
|
|
|
9
9
|
MULTI_OPTS = { category: "db.redis.multi".freeze, title: "MULTI".freeze, internal: true }.freeze
|
|
10
10
|
|
|
11
|
+
CONNECT_OPTS = { category: "db.redis.connect".freeze, title: "CONNECT".freeze, internal: true }.freeze
|
|
12
|
+
|
|
13
|
+
# redis < 5
|
|
11
14
|
module ClientInstrumentation
|
|
12
15
|
def call(command, *)
|
|
13
16
|
command_name = command[0]
|
|
@@ -20,18 +23,6 @@ module Skylight
|
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
|
|
23
|
-
module ClientInstrumentationV5
|
|
24
|
-
def call_v(command)
|
|
25
|
-
command_name = command[0]
|
|
26
|
-
|
|
27
|
-
return super if command_name == :auth
|
|
28
|
-
|
|
29
|
-
opts = { category: "db.redis.command", title: command_name.upcase.to_s, internal: true }
|
|
30
|
-
|
|
31
|
-
Skylight.instrument(opts) { super }
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
26
|
module Instrumentation
|
|
36
27
|
def pipelined(*)
|
|
37
28
|
Skylight.instrument(PIPELINED_OPTS) { super }
|
|
@@ -44,6 +35,17 @@ module Skylight
|
|
|
44
35
|
|
|
45
36
|
class Probe
|
|
46
37
|
def install
|
|
38
|
+
# A `Redis` namespace from something other than the redis gem; nothing to patch.
|
|
39
|
+
# (redis-client, if present, is installed by its own registration below.)
|
|
40
|
+
return unless defined?(::Redis::Client)
|
|
41
|
+
|
|
42
|
+
# redis 5+ is built on redis-client, which is instrumented below. Patching Redis
|
|
43
|
+
# as well would record each command twice.
|
|
44
|
+
if defined?(::RedisClient) && ::Redis::Client < ::RedisClient
|
|
45
|
+
Skylight::Probes.install_probe(Skylight::Probes.registered.fetch(:redis_client))
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
47
49
|
version = defined?(::Redis::VERSION) ? Gem::Version.new(::Redis::VERSION) : nil
|
|
48
50
|
|
|
49
51
|
if !version || version < Gem::Version.new("3.0.0")
|
|
@@ -52,17 +54,57 @@ module Skylight
|
|
|
52
54
|
return
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
::Redis::Client.prepend(ClientInstrumentation)
|
|
58
|
+
::Redis.prepend(Instrumentation)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# redis-client is used directly by e.g. Sidekiq 7+, and by redis 5+ under the hood.
|
|
63
|
+
module RedisClient
|
|
64
|
+
# Registered globally, so this applies to every RedisClient instance.
|
|
65
|
+
module Middleware
|
|
66
|
+
def call(command, _config)
|
|
67
|
+
command_name = command[0].to_s
|
|
68
|
+
|
|
69
|
+
return super if command_name.casecmp?("auth")
|
|
70
|
+
|
|
71
|
+
opts = { category: "db.redis.command", title: command_name.upcase, internal: true }
|
|
72
|
+
|
|
73
|
+
Skylight.instrument(opts) { super }
|
|
59
74
|
end
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
def call_pipelined(commands, _config)
|
|
77
|
+
# Transactions are sent as a pipeline wrapped in MULTI ... EXEC
|
|
78
|
+
opts = commands.dig(0, 0).to_s.casecmp?("multi") ? MULTI_OPTS : PIPELINED_OPTS
|
|
79
|
+
|
|
80
|
+
Skylight.instrument(opts) { super }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Covers the whole connection setup: the socket (and TLS) connect, plus the handshake
|
|
85
|
+
# (HELLO/AUTH/SELECT, etc.). The handshake goes through the middleware as an ordinary
|
|
86
|
+
# pipeline, with nothing to tell it apart from the user's, so it's muted here.
|
|
87
|
+
# `connect` is private, so accept any arguments in case its signature changes.
|
|
88
|
+
module ConnectInstrumentation
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def connect(*, **)
|
|
92
|
+
Skylight.instrument(CONNECT_OPTS) { Skylight.mute { super } }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class Probe
|
|
97
|
+
def install
|
|
98
|
+
::RedisClient.register(Middleware)
|
|
99
|
+
::RedisClient.prepend(ConnectInstrumentation)
|
|
100
|
+
end
|
|
62
101
|
end
|
|
63
102
|
end
|
|
64
103
|
end
|
|
65
104
|
|
|
105
|
+
# A single `redis` probe covers both gems: redis-client is often used without redis
|
|
106
|
+
# (e.g. by Sidekiq 7+), and either may be loaded first.
|
|
66
107
|
register(:redis, "Redis", "redis", Redis::Probe.new)
|
|
108
|
+
register(:redis_client, "RedisClient", %w[redis-client redis_client], Redis::RedisClient::Probe.new)
|
|
67
109
|
end
|
|
68
110
|
end
|
data/lib/skylight/version.rb
CHANGED