rails-profiler 0.35.0 → 0.35.2
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 +54 -0
- data/app/views/layouts/profiler/application.html.erb +1 -1
- data/app/views/layouts/profiler/embedded.html.erb +1 -1
- data/exe/profiler-mcp +8 -1
- data/lib/profiler/body_content.rb +58 -0
- data/lib/profiler/cluster/master_client.rb +45 -1
- data/lib/profiler/console_profiler.rb +1 -1
- data/lib/profiler/instrumentation/net_http_instrumentation.rb +20 -7
- data/lib/profiler/instrumentation/thread_context_propagation.rb +9 -2
- data/lib/profiler/job_profiler.rb +1 -1
- data/lib/profiler/mcp/server.rb +1 -1
- data/lib/profiler/mcp/tools/get_profile_detail.rb +2 -2
- data/lib/profiler/mcp/tools/get_profile_http.rb +3 -0
- data/lib/profiler/mcp/tools/query_jobs.rb +1 -1
- data/lib/profiler/mcp/tools/run_tests.rb +4 -1
- data/lib/profiler/middleware/profiler_middleware.rb +1 -1
- data/lib/profiler/models/profile.rb +2 -5
- data/lib/profiler/railtie.rb +7 -0
- data/lib/profiler/version.rb +27 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d04dbf2927645f4fbec0524e6171a4086a5cbc54206a3595a903229611941a5b
|
|
4
|
+
data.tar.gz: 4caba281e20e0a097204b6107e90be8ec74450f91bb96c26bbfac28737006fed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e911c0a48a0f9e3f558e665cc1defac6c55a038f0d2f0620a6c445e41c5b3fce41bb4c8df07efa3135d382afad79d1c86d3e78b41966c800b954d67487c2cd68
|
|
7
|
+
data.tar.gz: 2962593dd780fad90a9b1ca9db957e0a8555e137acff0de9260fa3b57b9997c6b08e5e3f0a52f69cce5908c7cbd07aca3200dbcc0d8f22cf64f3405fe3547858
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,59 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.35.2] - 2026-10-17
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Performance:** The patch on `Thread#initialize` that carries the profiling context into new
|
|
29
|
+
threads is now installed by the railtie, only when the profiler is `enabled`; a disabled profiler
|
|
30
|
+
no longer touches `Thread`.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Cluster:** A worker forked from a process that had already started the cluster client (Puma in
|
|
35
|
+
cluster mode with `preload_app!`, a preloaded Unicorn) now gets its own heartbeat thread and
|
|
36
|
+
registers again with the master, through a `Process._fork` hook (Ruby 3.1 and later). Where the
|
|
37
|
+
hook is not available, call `Profiler::Cluster::MasterClient.restart_after_fork` from Puma's
|
|
38
|
+
`on_worker_boot`; see `docs/cluster.md`.
|
|
39
|
+
- **HTTP and request bodies:** A body is now kept as base64 when its bytes are binary (a NUL byte,
|
|
40
|
+
invalid UTF-8, mostly control characters), whatever `Content-Type` it was served with, and `font/`,
|
|
41
|
+
`application/x-protobuf` and `application/gzip` are recognized as binary types. A binary string
|
|
42
|
+
is no longer turned into `?` characters. A body whose `Content-Type` declares a charset other than UTF-8 (`ISO-8859-1`...) stays text, decoded from that charset. A `Net::HTTP` response read as a stream (`read_body`
|
|
43
|
+
with a block) is recorded with no body and `response_body_not_captured`, instead of the `inspect`
|
|
44
|
+
of the stream adapter.
|
|
45
|
+
- **Version:** `Profiler.version`, the version recorded in profiles and announced by the MCP server,
|
|
46
|
+
comes from `git describe` when the gem is mounted by path from a git checkout, so
|
|
47
|
+
`gem_version` is no longer always `0.0.0` there. A published gem keeps the version it was built
|
|
48
|
+
with, and `0.0.0` remains the fallback.
|
|
49
|
+
- **MCP:** The `profiler-mcp` executable reads the profiles the application's file storage wrote
|
|
50
|
+
under `tmp/rails-profiler` in the current directory; it read an empty memory store of its own and
|
|
51
|
+
always answered that there were no profiles.
|
|
52
|
+
- **Profile pages:** the `profiler-version` meta tag of the page layouts now reports `Profiler.version`, the same value the API returns, instead of the raw `Profiler::VERSION` constant. When the gem is mounted from a git checkout, every profile no longer shows a false "different version" warning.
|
|
53
|
+
|
|
54
|
+
## [0.35.1] - 2026-10-17
|
|
55
|
+
|
|
56
|
+
<!-- stamped -->
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Release:** The version a merge publishes is now read from the subject line of each commit only,
|
|
61
|
+
never from its body, and `BREAKING-CHANGE:` (with a hyphen) publishes a major version just as
|
|
62
|
+
`BREAKING CHANGE:` already did.
|
|
63
|
+
- **Canary:** A prerelease is now numbered after the version its branch is going to publish, not
|
|
64
|
+
after the last release, so RubyGems no longer sorts a recent canary as older than the release it
|
|
65
|
+
precedes.
|
|
66
|
+
|
|
67
|
+
### Security
|
|
68
|
+
|
|
69
|
+
- **Release:** `rake release` and its subtasks now refuse to run from a developer machine and point
|
|
70
|
+
to the `release` CI job, the only publisher. `rake build` and `rake install` keep working.
|
|
71
|
+
- **CI:** The `canary` job no longer publishes a prerelease on every push to a `feature/`,
|
|
72
|
+
`bugfix/`, `hotfix/`, `fix/` or `breaking/` branch; it is now manual and publishes only when
|
|
73
|
+
someone plays it.
|
|
74
|
+
|
|
22
75
|
## [0.35.0] - 2026-10-17
|
|
23
76
|
|
|
24
77
|
<!-- stamped -->
|
|
@@ -74,6 +127,7 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
74
127
|
(`charset=utf-7`, `charset=internal`) no longer loses its profile; the body is read as UTF-8 with
|
|
75
128
|
invalid bytes replaced.
|
|
76
129
|
|
|
130
|
+
|
|
77
131
|
## [0.34.4] - 2026-10-16
|
|
78
132
|
|
|
79
133
|
<!-- stamped -->
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
|
-
<meta name="profiler-version" content="<%= Profiler
|
|
9
|
+
<meta name="profiler-version" content="<%= Profiler.version %>">
|
|
10
10
|
<meta name="profiler-name" content="<%= Profiler.configuration.resolved_name %>">
|
|
11
11
|
<meta name="profiler-is-master" content="<%= Profiler.configuration.master? %>">
|
|
12
12
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
|
-
<meta name="profiler-version" content="<%= Profiler
|
|
9
|
+
<meta name="profiler-version" content="<%= Profiler.version %>">
|
|
10
10
|
|
|
11
11
|
<link rel="stylesheet" href="/_profiler/assets/profiler.css">
|
|
12
12
|
<script src="/_profiler/assets/profiler.js" defer></script>
|
data/exe/profiler-mcp
CHANGED
|
@@ -4,10 +4,17 @@
|
|
|
4
4
|
# The MCP server over stdio, without the application: its initializers do not run here, so
|
|
5
5
|
# config.mcp_read_only set there has no effect. Set PROFILER_MCP_READ_ONLY=1 in the environment the
|
|
6
6
|
# MCP client gives this process to leave out the write tools (Profiler::MCP::Server.read_only?).
|
|
7
|
-
# `bundle exec rake profiler:mcp` boots the application and reads both.
|
|
7
|
+
# `bundle exec rake profiler:mcp` boots the application and reads both. It reads the profiles of
|
|
8
|
+
# the file storage under tmp/rails-profiler in the current directory: run it from the application's
|
|
9
|
+
# root. An application on another storage is read through `rake profiler:mcp` or the HTTP endpoint.
|
|
8
10
|
|
|
9
11
|
require "bundler/setup"
|
|
10
12
|
require "profiler"
|
|
11
13
|
require "profiler/mcp/server"
|
|
12
14
|
|
|
15
|
+
# Without the application there is no initializer to pick a storage, and the default is :memory:
|
|
16
|
+
# a store of this process alone, which never holds a profile the application wrote. Read the files
|
|
17
|
+
# the application's file storage wrote under ./tmp/rails-profiler, from the application's root.
|
|
18
|
+
Profiler.configure { |config| config.storage = :file }
|
|
19
|
+
|
|
13
20
|
Profiler::MCP::Server.new.start(transport: :stdio)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
# Tells a body that is text from one that is not, from the bytes themselves and not only from
|
|
5
|
+
# the Content-Type the sender declared: a binary payload served as text/plain is common, and
|
|
6
|
+
# forcing it into UTF-8 loses bytes it cannot get back.
|
|
7
|
+
module BodyContent
|
|
8
|
+
BINARY_CONTENT_TYPES = %r{
|
|
9
|
+
\A(?:image/(?!svg)|audio/|video/|font/|
|
|
10
|
+
application/(?:pdf|octet-stream|zip|gzip|x-gzip|x-protobuf|protobuf|x-msgpack|msgpack|
|
|
11
|
+
wasm|x-font|font-|vnd\.ms-fontobject|x-tar|x-7z-compressed|x-bzip2|zstd))
|
|
12
|
+
}xi
|
|
13
|
+
|
|
14
|
+
# Enough to tell: the head of a body is where a magic number or a NUL byte shows up.
|
|
15
|
+
SAMPLE_BYTES = 8192
|
|
16
|
+
# A text holds few control characters: tab, newline, carriage return and escape set aside.
|
|
17
|
+
MAX_CONTROL_RATIO = 0.05
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def binary_type?(content_type)
|
|
22
|
+
content_type.to_s.split(";").first.to_s.strip.match?(BINARY_CONTENT_TYPES)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# True for a body that cannot be kept as text: a NUL byte, bytes that are not UTF-8 (a cut
|
|
26
|
+
# in the middle of the last character excepted), or mostly control characters.
|
|
27
|
+
#
|
|
28
|
+
# A body whose Content-Type declares a charset other than UTF-8 is not judged on its UTF-8
|
|
29
|
+
# validity: its high bytes belong to that charset (ISO-8859-1, Windows-1252...) and are
|
|
30
|
+
# decoded from it afterwards. The NUL byte and control character tests still apply to it.
|
|
31
|
+
def binary_bytes?(body, content_type = nil)
|
|
32
|
+
return false if body.nil? || body.empty?
|
|
33
|
+
|
|
34
|
+
sample = body.byteslice(0, SAMPLE_BYTES).dup.force_encoding(Encoding::UTF_8)
|
|
35
|
+
return true if sample.include?("\0")
|
|
36
|
+
|
|
37
|
+
if !sample.valid_encoding? && !foreign_charset?(content_type)
|
|
38
|
+
# The sample may end inside a multi-byte character, whichever way it was cut.
|
|
39
|
+
sample = (1..3).filter_map { |n| sample.byteslice(0, sample.bytesize - n)&.force_encoding(Encoding::UTF_8) }
|
|
40
|
+
.find(&:valid_encoding?)
|
|
41
|
+
return true if sample.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sample = sample.scrub("") unless sample.valid_encoding?
|
|
45
|
+
controls = sample.count("\x01-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F\x7F")
|
|
46
|
+
sample.length.positive? && controls.fdiv(sample.length) > MAX_CONTROL_RATIO
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def binary?(body, content_type)
|
|
50
|
+
binary_type?(content_type) || binary_bytes?(body, content_type)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def foreign_charset?(content_type)
|
|
54
|
+
charset = content_type.to_s[/charset\s*=\s*"?([^\s";]+)/i, 1]
|
|
55
|
+
!charset.nil? && !charset.match?(/\A(?:utf-?8|us-ascii)\z/i)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -8,7 +8,38 @@ require_relative "security"
|
|
|
8
8
|
module Profiler
|
|
9
9
|
module Cluster
|
|
10
10
|
class MasterClient
|
|
11
|
+
# Process._fork (Ruby 3.1+) runs in the parent and in the child alike. A thread does not
|
|
12
|
+
# survive a fork, so a worker forked from a process that had started the client (Puma in
|
|
13
|
+
# cluster mode with preload_app!, a preloaded Unicorn) would never heartbeat.
|
|
14
|
+
module ForkHook
|
|
15
|
+
def _fork
|
|
16
|
+
pid = super
|
|
17
|
+
MasterClient.restart_after_fork if pid.zero?
|
|
18
|
+
pid
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# The client the railtie started in this process, for the fork hook to restart.
|
|
24
|
+
attr_accessor :current
|
|
25
|
+
|
|
26
|
+
def install_fork_hook!
|
|
27
|
+
return false if @fork_hook_installed || !Process.respond_to?(:_fork)
|
|
28
|
+
|
|
29
|
+
Process.singleton_class.prepend(ForkHook)
|
|
30
|
+
@fork_hook_installed = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def restart_after_fork
|
|
34
|
+
current&.restart_after_fork
|
|
35
|
+
rescue => e
|
|
36
|
+
Profiler.log_warn("Cluster: could not restart the heartbeat after a fork", e)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
11
40
|
def start
|
|
41
|
+
self.class.current = self
|
|
42
|
+
self.class.install_fork_hook!
|
|
12
43
|
@registered = false
|
|
13
44
|
begin
|
|
14
45
|
register!
|
|
@@ -16,6 +47,17 @@ module Profiler
|
|
|
16
47
|
rescue => e
|
|
17
48
|
log_warn("could not register with master at startup, will retry in the heartbeat loop", e)
|
|
18
49
|
end
|
|
50
|
+
@started = true
|
|
51
|
+
start_heartbeat_thread
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# In the child of a fork: the heartbeat thread of the parent is gone and the parent's
|
|
55
|
+
# registration is not the child's own, so register again from the new thread.
|
|
56
|
+
# Does nothing while a heartbeat thread is alive, nor before start.
|
|
57
|
+
def restart_after_fork
|
|
58
|
+
return if @started.nil? || @thread&.alive?
|
|
59
|
+
|
|
60
|
+
@registered = false
|
|
19
61
|
start_heartbeat_thread
|
|
20
62
|
end
|
|
21
63
|
|
|
@@ -61,7 +103,9 @@ module Profiler
|
|
|
61
103
|
end
|
|
62
104
|
|
|
63
105
|
def start_heartbeat_thread
|
|
64
|
-
|
|
106
|
+
return if @thread&.alive?
|
|
107
|
+
|
|
108
|
+
@thread = Thread.new do
|
|
65
109
|
interval = Profiler.configuration.cluster_heartbeat_interval
|
|
66
110
|
loop do
|
|
67
111
|
sleep interval
|
|
@@ -38,7 +38,7 @@ module Profiler
|
|
|
38
38
|
def run(&block)
|
|
39
39
|
profile = Models::Profile.new
|
|
40
40
|
profile.profile_type = "console"
|
|
41
|
-
profile.gem_version = Profiler
|
|
41
|
+
profile.gem_version = Profiler.version
|
|
42
42
|
profile.path = Redaction.truncate(@expression, 200)
|
|
43
43
|
profile.method = "CONSOLE"
|
|
44
44
|
|
|
@@ -6,6 +6,7 @@ require "zlib"
|
|
|
6
6
|
require "stringio"
|
|
7
7
|
require "securerandom"
|
|
8
8
|
require_relative "../redaction"
|
|
9
|
+
require_relative "../body_content"
|
|
9
10
|
require_relative "../configuration"
|
|
10
11
|
|
|
11
12
|
module Profiler
|
|
@@ -59,7 +60,10 @@ module Profiler
|
|
|
59
60
|
response = super
|
|
60
61
|
|
|
61
62
|
duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2)
|
|
62
|
-
|
|
63
|
+
# read_body with a block hands each chunk to the block and keeps none: the response then
|
|
64
|
+
# holds a Net::ReadAdapter, whose to_s is an inspect and whose bytes are gone.
|
|
65
|
+
streamed = NetHttpInstrumentation.streamed_body?(response)
|
|
66
|
+
resp_body_raw = streamed ? "" : response.body.to_s
|
|
63
67
|
resp_content_encoding = response["content-encoding"].to_s.strip.downcase
|
|
64
68
|
resp_body = NetHttpInstrumentation.decompress_body(resp_body_raw, resp_content_encoding, limit)
|
|
65
69
|
resp_truncated = limit ? resp_body.bytesize > limit : false
|
|
@@ -76,8 +80,9 @@ module Profiler
|
|
|
76
80
|
response_headers: Redaction.filter_headers(response.to_hash.transform_values { |v| v.join(", ") }),
|
|
77
81
|
response_body: processed_resp[:body],
|
|
78
82
|
response_body_encoding: processed_resp[:encoding],
|
|
79
|
-
response_size: resp_body_raw.bytesize,
|
|
80
|
-
response_body_truncated: resp_truncated
|
|
83
|
+
response_size: streamed ? (declared_length(response) || 0) : resp_body_raw.bytesize,
|
|
84
|
+
response_body_truncated: resp_truncated,
|
|
85
|
+
response_body_not_captured: streamed
|
|
81
86
|
)
|
|
82
87
|
|
|
83
88
|
fg = Thread.current[:profiler_flamegraph_collector]
|
|
@@ -96,6 +101,11 @@ module Profiler
|
|
|
96
101
|
|
|
97
102
|
private
|
|
98
103
|
|
|
104
|
+
def declared_length(response)
|
|
105
|
+
length = response["content-length"].to_s
|
|
106
|
+
length.match?(/\A\d+\z/) ? length.to_i : nil
|
|
107
|
+
end
|
|
108
|
+
|
|
99
109
|
def build_url(host, port, path, ssl)
|
|
100
110
|
scheme = ssl ? "https" : "http"
|
|
101
111
|
standard_port = (ssl && port == 443) || (!ssl && port == 80)
|
|
@@ -110,7 +120,7 @@ module Profiler
|
|
|
110
120
|
deprecate_constant :SKIP_HOSTS
|
|
111
121
|
|
|
112
122
|
TEXT_CONTENT_TYPES = /\A(text\/|application\/(json|xml|xhtml|javascript|x-www-form-urlencoded)|image\/svg)/i
|
|
113
|
-
BINARY_CONTENT_TYPES =
|
|
123
|
+
BINARY_CONTENT_TYPES = BodyContent::BINARY_CONTENT_TYPES
|
|
114
124
|
|
|
115
125
|
def self.install!
|
|
116
126
|
return if @installed
|
|
@@ -255,12 +265,15 @@ module Profiler
|
|
|
255
265
|
inflater&.close
|
|
256
266
|
end
|
|
257
267
|
|
|
268
|
+
# A response read with read_body(&block) was consumed as a stream: its body is gone.
|
|
269
|
+
def self.streamed_body?(response)
|
|
270
|
+
defined?(Net::ReadAdapter) && response.body.is_a?(Net::ReadAdapter)
|
|
271
|
+
end
|
|
272
|
+
|
|
258
273
|
def self.process_body(body, content_type)
|
|
259
274
|
return { body: nil, encoding: "text" } if body.nil? || body.empty?
|
|
260
275
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if mime.match?(BINARY_CONTENT_TYPES)
|
|
276
|
+
if BodyContent.binary?(body, content_type)
|
|
264
277
|
# Masked on the raw bytes, before the encoding hides them from any later search.
|
|
265
278
|
{ body: Base64.strict_encode64(Redaction.hide_credentials(body.b)), encoding: "base64" }
|
|
266
279
|
else
|
|
@@ -37,8 +37,15 @@ module Profiler
|
|
|
37
37
|
super(*args, &wrapper)
|
|
38
38
|
end
|
|
39
39
|
ruby2_keywords :initialize
|
|
40
|
+
|
|
41
|
+
# Called by the railtie once the application has decided `enabled`: the patch is on
|
|
42
|
+
# Thread#initialize for every thread of the process, so a disabled profiler leaves it out.
|
|
43
|
+
def self.install!
|
|
44
|
+
return if @installed
|
|
45
|
+
|
|
46
|
+
Thread.prepend(self)
|
|
47
|
+
@installed = true
|
|
48
|
+
end
|
|
40
49
|
end
|
|
41
50
|
end
|
|
42
51
|
end
|
|
43
|
-
|
|
44
|
-
Thread.prepend(Profiler::Instrumentation::ThreadContextPropagation)
|
|
@@ -55,7 +55,7 @@ module Profiler
|
|
|
55
55
|
def run(&block)
|
|
56
56
|
profile = Models::Profile.new
|
|
57
57
|
profile.profile_type = "job"
|
|
58
|
-
profile.gem_version = Profiler
|
|
58
|
+
profile.gem_version = Profiler.version
|
|
59
59
|
profile.path = @job_class
|
|
60
60
|
profile.method = "JOB"
|
|
61
61
|
profile.parent_token = @parent_token if @parent_token
|
data/lib/profiler/mcp/server.rb
CHANGED
|
@@ -98,8 +98,8 @@ module Profiler
|
|
|
98
98
|
lines << "**Allocated objects:** #{profile.allocated_objects}" if profile.allocated_objects
|
|
99
99
|
lines << "**Time:** #{profile.started_at}"
|
|
100
100
|
if profile.gem_version
|
|
101
|
-
if profile.gem_version != Profiler
|
|
102
|
-
lines << "**Gem Version:** #{profile.gem_version} ⚠️ (current: #{Profiler
|
|
101
|
+
if profile.gem_version != Profiler.version
|
|
102
|
+
lines << "**Gem Version:** #{profile.gem_version} ⚠️ (current: #{Profiler.version})"
|
|
103
103
|
else
|
|
104
104
|
lines << "**Gem Version:** #{profile.gem_version}"
|
|
105
105
|
end
|
|
@@ -91,6 +91,9 @@ module Profiler
|
|
|
91
91
|
lines << "- _Request body not captured: a stream that cannot be rewound is sent unread._"
|
|
92
92
|
end
|
|
93
93
|
lines << "- _Request body truncated: only the first bytes were kept (max_captured_body_bytes)._" if req["request_body_truncated"]
|
|
94
|
+
if req["response_body_not_captured"]
|
|
95
|
+
lines << "- _Response body not captured: it was read as a stream (read_body with a block)._"
|
|
96
|
+
end
|
|
94
97
|
lines << "- _Response body truncated: only the first bytes were kept (max_captured_body_bytes)._" if req["response_body_truncated"]
|
|
95
98
|
lines << "- **Error:** #{req['error']}" if req["error"]
|
|
96
99
|
|
|
@@ -73,7 +73,7 @@ module Profiler
|
|
|
73
73
|
when "duration" then profile.duration ? "#{profile.duration.round(2)}ms" : "-"
|
|
74
74
|
when "gem_version"
|
|
75
75
|
v = profile.gem_version || "-"
|
|
76
|
-
v != Profiler
|
|
76
|
+
v != Profiler.version ? "#{v} ⚠️" : v
|
|
77
77
|
when "token" then profile.token.to_s
|
|
78
78
|
when "parent_token" then profile.parent_token || "-"
|
|
79
79
|
end
|
|
@@ -94,7 +94,10 @@ module Profiler
|
|
|
94
94
|
timed_out = false
|
|
95
95
|
|
|
96
96
|
loop do
|
|
97
|
-
|
|
97
|
+
if Time.now > deadline
|
|
98
|
+
timed_out = true
|
|
99
|
+
break
|
|
100
|
+
end
|
|
98
101
|
run_data = proxy.get_json("/_profiler/api/test_runner/runs/#{run_id}")
|
|
99
102
|
break if Profiler::TestRunner::RunStore::TERMINAL_STATUSES.include?(run_data["status"])
|
|
100
103
|
sleep 2
|
|
@@ -29,7 +29,7 @@ module Profiler
|
|
|
29
29
|
begin
|
|
30
30
|
begin
|
|
31
31
|
profile = Models::Profile.new(build_request(env), params: false)
|
|
32
|
-
profile.gem_version = Profiler
|
|
32
|
+
profile.gem_version = Profiler.version
|
|
33
33
|
Profiler::CurrentContext.token = profile.token
|
|
34
34
|
|
|
35
35
|
# Capture request body before app processes it
|
|
@@ -5,6 +5,7 @@ require "securerandom"
|
|
|
5
5
|
require "json"
|
|
6
6
|
require "zlib"
|
|
7
7
|
require_relative "../redaction"
|
|
8
|
+
require_relative "../body_content"
|
|
8
9
|
require_relative "../allocation_counter"
|
|
9
10
|
|
|
10
11
|
module Profiler
|
|
@@ -256,7 +257,7 @@ module Profiler
|
|
|
256
257
|
def process_body(raw, content_type)
|
|
257
258
|
return { body: nil, encoding: "text" } if raw.nil? || raw.empty?
|
|
258
259
|
|
|
259
|
-
if
|
|
260
|
+
if BodyContent.binary?(raw, content_type)
|
|
260
261
|
# Masked on the raw bytes, before the encoding hides them from any later search. A
|
|
261
262
|
# compressed format (zip, png, gzip...) does not hold the secret as it is.
|
|
262
263
|
{ body: Base64.strict_encode64(Redaction.hide_credentials(raw.b)), encoding: "base64" }
|
|
@@ -315,10 +316,6 @@ module Profiler
|
|
|
315
316
|
text.bytesize > Profiler.configuration.compress_body_threshold
|
|
316
317
|
end
|
|
317
318
|
|
|
318
|
-
def binary_content_type?(ct)
|
|
319
|
-
ct.to_s.match?(%r{image/(?!svg)|application/(?:pdf|octet-stream|zip)|audio/|video/})
|
|
320
|
-
end
|
|
321
|
-
|
|
322
319
|
LEGACY_FILTERED_PARAMS = %w[password password_confirmation token secret].freeze
|
|
323
320
|
|
|
324
321
|
# The key added to params cut at max_captured_body_bytes, so that the cut shows wherever
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -77,6 +77,13 @@ module Profiler
|
|
|
77
77
|
app.middleware.delete Profiler::Middleware::CorsMiddleware
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# Thread#initialize is patched for every thread of the process: only when the profiler is on.
|
|
81
|
+
initializer "profiler.install_thread_context_propagation" do
|
|
82
|
+
next unless Profiler.configuration.enabled
|
|
83
|
+
|
|
84
|
+
Profiler::Instrumentation::ThreadContextPropagation.install!
|
|
85
|
+
end
|
|
86
|
+
|
|
80
87
|
initializer "profiler.setup_test_profiler" do
|
|
81
88
|
next unless Profiler.configuration.enabled && Profiler.configuration.track_tests
|
|
82
89
|
|
data/lib/profiler/version.rb
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Profiler
|
|
4
|
-
VERSION = "0.35.
|
|
4
|
+
VERSION = "0.35.2"
|
|
5
|
+
|
|
6
|
+
GEM_ROOT = File.expand_path("../..", __dir__)
|
|
7
|
+
|
|
8
|
+
# The version the gem reports (profiles, the MCP server). A published gem has it written into
|
|
9
|
+
# VERSION by the release job, and that is what this returns. VERSION stays at 0.0.0 on the
|
|
10
|
+
# default branch, so a gem mounted by path from a git checkout derives it from `git describe`
|
|
11
|
+
# instead; anything else (no git, no checkout, no tag, an installed gem) keeps VERSION.
|
|
12
|
+
def self.version
|
|
13
|
+
return @version if defined?(@version)
|
|
14
|
+
|
|
15
|
+
@version = VERSION == "0.0.0" ? (checkout_version || VERSION) : VERSION
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# "0.34.2" on a tag, "0.34.2-5-g1a2b3c4" five commits after it. Nil unless the gem lives in
|
|
19
|
+
# its own git checkout: the .git next to lib/ is what tells it from a copy someone else's
|
|
20
|
+
# repository happens to contain.
|
|
21
|
+
def self.checkout_version
|
|
22
|
+
return nil unless File.exist?(File.join(GEM_ROOT, ".git")) && File.directory?(File.join(GEM_ROOT, "lib"))
|
|
23
|
+
|
|
24
|
+
described = IO.popen(["git", "-C", GEM_ROOT, "describe", "--tags", "--match", "v[0-9]*"],
|
|
25
|
+
err: File::NULL, &:read)
|
|
26
|
+
described = described.to_s.strip.delete_prefix("v")
|
|
27
|
+
$?&.success? && described.match?(/\A\d+\.\d+\.\d+/) ? described : nil
|
|
28
|
+
rescue SystemCallError, IOError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
5
31
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.35.
|
|
4
|
+
version: 0.35.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
@@ -145,6 +145,7 @@ files:
|
|
|
145
145
|
- lib/profiler.rb
|
|
146
146
|
- lib/profiler/ajax_data.rb
|
|
147
147
|
- lib/profiler/allocation_counter.rb
|
|
148
|
+
- lib/profiler/body_content.rb
|
|
148
149
|
- lib/profiler/boot_env.rb
|
|
149
150
|
- lib/profiler/cluster/master_client.rb
|
|
150
151
|
- lib/profiler/cluster/security.rb
|