rails-profiler 0.31.5 → 0.31.6
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 +30 -0
- data/app/controllers/profiler/api/console_controller.rb +1 -1
- data/app/controllers/profiler/api/jobs_controller.rb +1 -1
- data/app/controllers/profiler/api/profiles_controller.rb +1 -1
- data/app/controllers/profiler/api/tests_controller.rb +1 -1
- data/app/controllers/profiler/api/toolbar_controller.rb +1 -0
- data/app/controllers/profiler/application_controller.rb +2 -1
- data/app/controllers/profiler/profiles_controller.rb +1 -1
- data/lib/profiler/mcp/body_formatter.rb +2 -9
- data/lib/profiler/mcp/tools/get_profile_detail.rb +5 -1
- data/lib/profiler/models/profile.rb +24 -14
- data/lib/profiler/storage/summary.rb +8 -4
- data/lib/profiler/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: f8c9d9f31f3f32dc5ada20eb78eb6bc205c0985e084942edf18b283cfafe81be
|
|
4
|
+
data.tar.gz: 49fb9afa0dd2fa52033f151b077ec971db08fe45818fe64448559ba95f4face3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79bd6095dd2deb184ee662c398183d5e63b9b9af30f5bda76b1806353adce5137627c2e33afc5c95aadc451dd540a9d243c1cc1df3eb72eccc2001331362581f
|
|
7
|
+
data.tar.gz: bd488d9da999d2f54be42b4002030f88bb51b069c956b78c85f0aadc4e87faa3f6ec3a2bb26c9921b73bea0fbaa84e16fd8e3f5967cfc8276ada75fa44a9b331
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,36 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.31.6] - 2026-10-05
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Storage:** With `config.compress_bodies` (on by default), a text body larger than
|
|
29
|
+
`config.compress_body_threshold` (10 KB) is now stored gzip+base64, as the README said: the
|
|
30
|
+
profile was decompressed again before every save, so the file, Redis and memory stores kept the
|
|
31
|
+
body as text, next to the compressed copy the request collector keeps, and the compression only
|
|
32
|
+
cost time. A 132 KB HTML page now takes 86 KB in those stores instead of 171 KB (273 KB without
|
|
33
|
+
compression); SQLite, which only keeps that copy, 44 KB instead of 36 KB at the faster level.
|
|
34
|
+
Compressing it costs 1.6 ms on the request instead of 4.1 ms (the fastest zlib level), saving
|
|
35
|
+
the profile 0.2 ms instead of 1.2 ms; the bodies are decompressed only when a profile is shown,
|
|
36
|
+
about 0.7 ms. The profile page, its API, the job, console and test APIs, the lists the cluster
|
|
37
|
+
proxy reads and the MCP tools still give the bodies as text; the toolbar, which shows no body,
|
|
38
|
+
gets them as stored (86 KB instead of 171 KB for that page).
|
|
39
|
+
- **Storage:** The profile list summaries kept by the file, SQLite and Redis stores no longer
|
|
40
|
+
carry the first 500 characters of the request and response bodies. Summaries written by 0.31.4
|
|
41
|
+
and 0.31.5 keep them until their profiles are evicted; no list shows them.
|
|
42
|
+
- **MCP:** The `curl` command of `get_profile_detail` carries the request body as it was sent: a
|
|
43
|
+
body larger than `compress_body_threshold` came out as its compressed base64 form, and a binary
|
|
44
|
+
body as base64 text. A binary body is now left out of the command.
|
|
45
|
+
- **Upgrading:** the profiles already stored, all as text, are read as before, with no migration.
|
|
46
|
+
An earlier version reading a store written by this one shows the compressed bodies as text too,
|
|
47
|
+
and a cluster master and its slaves may run either version: the profile API they read still
|
|
48
|
+
gives the bodies as text. A body that cannot be decompressed is shown as it is stored, never as
|
|
49
|
+
an error. To keep the bodies readable as text in the stored files or Redis keys, set
|
|
50
|
+
`config.compress_bodies = false`.
|
|
51
|
+
|
|
22
52
|
## [0.31.5] - 2026-10-05
|
|
23
53
|
|
|
24
54
|
<!-- stamped -->
|
|
@@ -14,7 +14,7 @@ module Profiler
|
|
|
14
14
|
return render json: { error: "Job profile not found" }, status: :not_found
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
render json: profile.to_h.merge(
|
|
17
|
+
render json: profile.to_h(decode_bodies: true).merge(
|
|
18
18
|
child_jobs: build_child_jobs(profile),
|
|
19
19
|
parent_profile: build_parent_summary(profile)
|
|
20
20
|
)
|
|
@@ -28,7 +28,7 @@ module Profiler
|
|
|
28
28
|
# The route table and ENV are the process's, not stored in the profile
|
|
29
29
|
Profiler::ProcessSnapshot.hydrate(profile)
|
|
30
30
|
|
|
31
|
-
render json: profile.to_h.merge(
|
|
31
|
+
render json: profile.to_h(decode_bodies: true).merge(
|
|
32
32
|
child_jobs: build_child_jobs(profile),
|
|
33
33
|
parent_profile: build_parent_summary(profile)
|
|
34
34
|
)
|
|
@@ -19,6 +19,7 @@ module Profiler
|
|
|
19
19
|
# The route table and ENV are the process's, not stored in the profile
|
|
20
20
|
Profiler::ProcessSnapshot.hydrate(profile)
|
|
21
21
|
|
|
22
|
+
# The bodies as stored (gzip+base64 past compress_body_threshold): the toolbar shows none.
|
|
22
23
|
render json: {
|
|
23
24
|
profile: profile.to_h.merge(child_jobs: build_child_jobs(profile)),
|
|
24
25
|
events_cursor: events_cursor
|
|
@@ -74,9 +74,10 @@ module Profiler
|
|
|
74
74
|
render_page(Profiler.storage.list(limit: limit + 1, offset: offset, type: type, summary: summary), limit, offset)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# The bodies as text: the cluster proxy lists whole profiles (all_types), a summary has none.
|
|
77
78
|
def render_page(profiles, limit, offset)
|
|
78
79
|
render json: {
|
|
79
|
-
profiles: profiles.first(limit).map(
|
|
80
|
+
profiles: profiles.first(limit).map { |profile| profile.to_h(decode_bodies: true) },
|
|
80
81
|
limit: limit,
|
|
81
82
|
offset: offset,
|
|
82
83
|
has_more: profiles.size > limit
|
|
@@ -25,7 +25,7 @@ module Profiler
|
|
|
25
25
|
# The route table and ENV are the process's, not stored in the profile
|
|
26
26
|
Profiler::ProcessSnapshot.hydrate(@profile)
|
|
27
27
|
|
|
28
|
-
@profile_data = @profile.to_h.merge(
|
|
28
|
+
@profile_data = @profile.to_h(decode_bodies: true).merge(
|
|
29
29
|
child_jobs: build_child_jobs(@profile),
|
|
30
30
|
parent_profile: build_parent_summary(@profile)
|
|
31
31
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "zlib"
|
|
3
|
+
require_relative "../models/profile"
|
|
5
4
|
|
|
6
5
|
module Profiler
|
|
7
6
|
module MCP
|
|
@@ -19,13 +18,7 @@ module Profiler
|
|
|
19
18
|
return " *(binary, base64 encoded)*" if encoding == "base64"
|
|
20
19
|
return nil if body.nil? || body.empty?
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
begin
|
|
24
|
-
body = Zlib::Inflate.inflate(Base64.strict_decode64(body))
|
|
25
|
-
rescue Zlib::Error, ArgumentError
|
|
26
|
-
# fall through with original body
|
|
27
|
-
end
|
|
28
|
-
end
|
|
21
|
+
body, = Models::Profile.decode_body(body, encoding)
|
|
29
22
|
|
|
30
23
|
if params["save_bodies"]
|
|
31
24
|
path = FileCache.save(token, name, body)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../slave_support"
|
|
4
4
|
require_relative "../../process_snapshot"
|
|
5
|
+
require_relative "../../models/profile"
|
|
5
6
|
|
|
6
7
|
require "shellwords"
|
|
7
8
|
require "cgi"
|
|
@@ -666,7 +667,10 @@ module Profiler
|
|
|
666
667
|
def self.generate_curl(profile, req_data)
|
|
667
668
|
headers = req_data&.dig("headers") || {}
|
|
668
669
|
params = req_data&.dig("params") || {}
|
|
669
|
-
|
|
670
|
+
# As captured: a binary body cannot be typed on a command line.
|
|
671
|
+
req_body, req_encoding = Models::Profile.decode_body(req_data&.dig("request_body"),
|
|
672
|
+
req_data&.dig("request_body_encoding"))
|
|
673
|
+
req_body = nil unless req_encoding.nil? || req_encoding == "text"
|
|
670
674
|
|
|
671
675
|
parts = ["curl -X #{profile.method}"]
|
|
672
676
|
|
|
@@ -117,9 +117,15 @@ module Profiler
|
|
|
117
117
|
end
|
|
118
118
|
private :collector_metadata
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
# The profile as the stores keep it: a body compressed by compress_bodies stays gzip+base64.
|
|
121
|
+
# decode_bodies: true gives the bodies as text, for the responses that show them.
|
|
122
|
+
def to_h(decode_bodies: false)
|
|
123
|
+
req_body, req_enc = @request_body, @request_body_encoding
|
|
124
|
+
resp_body, resp_enc = @response_body, @response_body_encoding
|
|
125
|
+
if decode_bodies
|
|
126
|
+
req_body, req_enc = self.class.decode_body(req_body, req_enc)
|
|
127
|
+
resp_body, resp_enc = self.class.decode_body(resp_body, resp_enc)
|
|
128
|
+
end
|
|
123
129
|
|
|
124
130
|
{
|
|
125
131
|
profile_type: @profile_type,
|
|
@@ -155,6 +161,7 @@ module Profiler
|
|
|
155
161
|
}
|
|
156
162
|
end
|
|
157
163
|
|
|
164
|
+
# The stored form, as #to_h.
|
|
158
165
|
def to_json(*args)
|
|
159
166
|
to_h.to_json(*args)
|
|
160
167
|
end
|
|
@@ -212,6 +219,18 @@ module Profiler
|
|
|
212
219
|
profile
|
|
213
220
|
end
|
|
214
221
|
|
|
222
|
+
# A body kept gzip+base64 as text, with its encoding then "text"; any other body as it is.
|
|
223
|
+
# One that cannot be decompressed (cut, damaged) is returned as it is: unreadable at worst.
|
|
224
|
+
def self.decode_body(body, encoding)
|
|
225
|
+
return [body, encoding] unless encoding == "gzip+base64"
|
|
226
|
+
return [body, encoding] if body.nil? || body.empty?
|
|
227
|
+
|
|
228
|
+
decoded = Zlib::Inflate.inflate(Base64.strict_decode64(body))
|
|
229
|
+
[decoded.force_encoding(Encoding::UTF_8).scrub, "text"]
|
|
230
|
+
rescue Zlib::Error, ArgumentError
|
|
231
|
+
[body, encoding]
|
|
232
|
+
end
|
|
233
|
+
|
|
215
234
|
def self.deep_stringify_keys(obj)
|
|
216
235
|
case obj
|
|
217
236
|
when Hash
|
|
@@ -235,7 +254,8 @@ module Profiler
|
|
|
235
254
|
else
|
|
236
255
|
text = Redaction.filter_body(raw, content_type).encode("UTF-8", invalid: :replace, undef: :replace)
|
|
237
256
|
if compress_body?(text)
|
|
238
|
-
|
|
257
|
+
# The fastest level: paid on the request, a quarter larger than the default level.
|
|
258
|
+
{ body: Base64.strict_encode64(Zlib::Deflate.deflate(text, Zlib::BEST_SPEED)), encoding: "gzip+base64" }
|
|
239
259
|
else
|
|
240
260
|
{ body: text, encoding: "text" }
|
|
241
261
|
end
|
|
@@ -253,16 +273,6 @@ module Profiler
|
|
|
253
273
|
text.bytesize > Profiler.configuration.compress_body_threshold
|
|
254
274
|
end
|
|
255
275
|
|
|
256
|
-
def decode_body(body, encoding)
|
|
257
|
-
return [body, encoding] unless encoding == "gzip+base64"
|
|
258
|
-
return [body, encoding] if body.nil? || body.empty?
|
|
259
|
-
|
|
260
|
-
decoded = Zlib::Inflate.inflate(Base64.strict_decode64(body))
|
|
261
|
-
[decoded, "text"]
|
|
262
|
-
rescue Zlib::Error, ArgumentError
|
|
263
|
-
[body, encoding]
|
|
264
|
-
end
|
|
265
|
-
|
|
266
276
|
def binary_content_type?(ct)
|
|
267
277
|
ct.to_s.match?(%r{image/(?!svg)|application/(?:pdf|octet-stream|zip)|audio/|video/})
|
|
268
278
|
end
|
|
@@ -7,16 +7,19 @@ module Profiler
|
|
|
7
7
|
# What the profile lists show of a profile, for the stores to keep apart from the profile
|
|
8
8
|
# itself: the top-level fields without the bodies, params, headers and tabs, and for each
|
|
9
9
|
# collector its scalar values only (database.total_queries, job.queue, test.test_name...),
|
|
10
|
-
# nested lists and hashes
|
|
11
|
-
# an exception message). Built from
|
|
12
|
-
# redaction have run: a summary is a subset of the
|
|
13
|
-
# start) and holds no value the profile does not.
|
|
10
|
+
# nested lists and hashes and the request collector's copy of the bodies left out, strings
|
|
11
|
+
# cut at MAX_SCALAR_LENGTH (a console return value, an exception message). Built from
|
|
12
|
+
# Profile#to_h, after the collectors and the redaction have run: a summary is a subset of the
|
|
13
|
+
# stored profile (a long string cut to its start) and holds no value the profile does not.
|
|
14
14
|
module Summary
|
|
15
15
|
MAX_SCALAR_LENGTH = 500
|
|
16
16
|
|
|
17
17
|
DROPPED = %i[params headers response_headers request_body request_body_encoding
|
|
18
18
|
response_body response_body_encoding tabs].freeze
|
|
19
19
|
|
|
20
|
+
# The request collector keeps a copy of the bodies: no more listed than the profile's own.
|
|
21
|
+
DROPPED_SCALARS = %w[request_body request_body_encoding response_body response_body_encoding].freeze
|
|
22
|
+
|
|
20
23
|
module_function
|
|
21
24
|
|
|
22
25
|
# From a Profile, or the Hash its #to_h returns.
|
|
@@ -37,6 +40,7 @@ module Profiler
|
|
|
37
40
|
def scalars(values)
|
|
38
41
|
values.each_with_object({}) do |(key, value), kept|
|
|
39
42
|
next unless scalar?(value)
|
|
43
|
+
next if DROPPED_SCALARS.include?(key.to_s)
|
|
40
44
|
|
|
41
45
|
kept[key.to_s] = value.is_a?(String) && value.length > MAX_SCALAR_LENGTH ? value[0, MAX_SCALAR_LENGTH] : value
|
|
42
46
|
end
|
data/lib/profiler/version.rb
CHANGED