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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb1aa9e97569dad5f875188d9b9950ce95e2f760cecbf47e56d1a111a3bb3d4b
4
- data.tar.gz: 077b6c9517691116e216022f9f5e810edc7173c262a66d99446b49341efbe51a
3
+ metadata.gz: dccfccf293fe88129f1bb1e0ae18e6bc8f242e1d9cb06ce3cdf7af0d47f5c77f
4
+ data.tar.gz: 99feb9962ef72af53930bdd89f25fad9cfd2943abbe4c5f3806bb768975c61b7
5
5
  SHA512:
6
- metadata.gz: 28a23bdc8e24bd10150a23d917bb669cdebfafb5422046f9c1fe7c0aebee7a12e2f31ca8aee12430e50e32956ef54581b63481f81616fdcf2edecf41ab86ddc2
7
- data.tar.gz: c41a8135ee576517a9f1f4f096fc9bb30f024e45e1918a3c19ca21e480f21b9ddecba70527734a660fe9723bc25feffcaae605bc0966d4d19a488b308d66166b
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(cache_key: opts.hash))
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, cache_key: opts[:cache_key])
90
+ sl ||= source_location(payload, meta)
79
91
 
80
92
  if sl
81
- trace("normalizer source_location=#{sl}")
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
- trace("source_location=#{meta[:source_location]}") if meta[:source_location]
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, cache_key: nil)
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(cache_key: cache_key))
143
+ elsif (location = find_caller)
132
144
  [location.absolute_path, location.lineno]
133
145
  end
134
146
  end
135
147
 
136
- def find_caller(cache_key: nil)
137
- # starting at 4 to skip Skylight extension processing logic
138
- locations = ::Kernel.caller_locations(4..MAX_CALLER_DEPTH)
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
- # Do this first since gems may be vendored in the app repo. However, it might be slower.
193
- # Should we cache matches?
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
- path = gem_name
196
- line = nil
229
+ [gem_name, false].freeze
197
230
  elsif project_path?(path)
198
- # Get relative path to root
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
- return
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
- enabled = true
4
+ class SendFile < Normalizer
5
+ register "send_file.action_controller"
5
6
 
6
- # Temporary hacks
7
- begin
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
- OCTET_STREAM = "application/octet-stream".freeze
36
- ATTACHMENT = "attachment".freeze
10
+ def normalize(_trace, _name, _payload)
11
+ title = TITLE
37
12
 
38
- def initialize(*)
39
- super
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
- @mimes =
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
- method = endpoint.options[:method].first
13
- method = "#{method}..." if endpoint.options[:method].length > 1
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.options[:path].join("/")
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)
@@ -28,7 +28,7 @@ module Skylight
28
28
  end
29
29
 
30
30
  def base_app_name(endpoint)
31
- ep = endpoint.options[:for]
31
+ ep = api(endpoint)
32
32
  return ep.name if ep.name
33
33
 
34
34
  if ep.respond_to?(:base) && ep.base.respond_to?(:name)
@@ -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 = ret = normalize(trace, name, payload)
68
+ cat, title, desc, meta = normalize(trace, name, payload)
69
69
  return cat if cat == :skip
70
70
 
71
71
  meta ||= {}
72
- cache_key = ret.hash
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, cache_key: nil)
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
- if ::Redis::Client.method_defined?(:call_v)
56
- ::Redis::Client.prepend(ClientInstrumentationV5)
57
- else
58
- ::Redis::Client.prepend(ClientInstrumentation)
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
- ::Redis.prepend(Instrumentation)
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
@@ -3,5 +3,5 @@ module Skylight
3
3
  # for compatibility with semver when it is parsed by the rust agent.
4
4
  # This string will be transformed in the gemspec to "5.0.0.alpha"
5
5
  # to conform with rubygems.
6
- VERSION = "7.1.1".freeze
6
+ VERSION = "7.2.0-beta".freeze
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skylight
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.1
4
+ version: 7.2.0.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tilde, Inc.