rails-profiler 0.31.4 → 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 +103 -0
- data/app/assets/builds/profiler.js +9 -1
- 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 +3 -3
- data/lib/profiler/cluster/master_client.rb +7 -15
- data/lib/profiler/cluster/security.rb +3 -3
- data/lib/profiler/cluster/slave_proxy.rb +7 -4
- data/lib/profiler/collectors/lifecycle.rb +2 -2
- data/lib/profiler/collectors/log_collector.rb +54 -5
- data/lib/profiler/collectors/mailer_collector.rb +8 -4
- data/lib/profiler/collectors/request_collector.rb +6 -1
- data/lib/profiler/collectors/scoped_notifications.rb +1 -1
- data/lib/profiler/configuration.rb +15 -4
- data/lib/profiler/console_profiler.rb +1 -1
- data/lib/profiler/engine.rb +2 -1
- data/lib/profiler/env_override_store.rb +4 -4
- data/lib/profiler/explain_runner.rb +5 -1
- data/lib/profiler/instrumentation/net_http_instrumentation.rb +138 -40
- data/lib/profiler/job_profiler.rb +1 -1
- data/lib/profiler/local_request.rb +2 -6
- data/lib/profiler/mcp/body_formatter.rb +2 -9
- data/lib/profiler/mcp/file_cache.rb +2 -1
- data/lib/profiler/mcp/server.rb +1 -1
- data/lib/profiler/mcp/tools/get_profile_detail.rb +5 -1
- data/lib/profiler/mcp/tools/get_profile_http.rb +6 -1
- data/lib/profiler/mcp/tools/query_console_profiles.rb +5 -1
- data/lib/profiler/mcp/tools/query_jobs.rb +5 -1
- data/lib/profiler/mcp/tools/query_mailers.rb +5 -1
- data/lib/profiler/mcp/tools/query_profiles.rb +5 -1
- data/lib/profiler/mcp/tools/query_test_profiles.rb +5 -1
- data/lib/profiler/mcp/tools/run_tests.rb +4 -1
- data/lib/profiler/middleware/profiler_middleware.rb +22 -19
- data/lib/profiler/middleware/streamed_profile.rb +1 -1
- data/lib/profiler/middleware/toolbar_injector.rb +21 -2
- data/lib/profiler/models/profile.rb +84 -16
- data/lib/profiler/process_snapshot.rb +8 -3
- data/lib/profiler/railtie.rb +1 -1
- data/lib/profiler/redaction.rb +1 -3
- data/lib/profiler/sse/redis_event_bus.rb +2 -1
- data/lib/profiler/storage/base_store.rb +4 -2
- data/lib/profiler/storage/blob_store.rb +1 -1
- data/lib/profiler/storage/file_store.rb +8 -3
- data/lib/profiler/storage/private_files.rb +3 -3
- data/lib/profiler/storage/redis_store.rb +2 -2
- data/lib/profiler/storage/sqlite_store.rb +3 -3
- data/lib/profiler/storage/summary.rb +8 -4
- data/lib/profiler/storage/unavailable.rb +1 -2
- data/lib/profiler/test_helpers/reporter.rb +1 -1
- data/lib/profiler/test_profiler.rb +1 -1
- data/lib/profiler/test_runner/runner.rb +3 -7
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +126 -3
- 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,109 @@ 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
|
+
|
|
52
|
+
## [0.31.5] - 2026-10-05
|
|
53
|
+
|
|
54
|
+
<!-- stamped -->
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **Logging:** Send the profiler's own errors and warnings to the application's log instead of
|
|
59
|
+
`$stderr`. A profile that could not be saved, a collector that failed, a store that could not be
|
|
60
|
+
read, a cluster node out of reach: about thirty places wrote to `$stderr`, which is not where
|
|
61
|
+
`log/development.log` is, so these errors went unseen. They now go through one entry point,
|
|
62
|
+
`Profiler.log_error`, to `config.logger` when the application sets one (read on each message),
|
|
63
|
+
otherwise `Rails.logger`, otherwise `$stderr`. Every line starts with `[Profiler]` (the cluster's
|
|
64
|
+
lines, `[Profiler Cluster]` before, now start with `[Profiler] Cluster:`), carries the error's
|
|
65
|
+
class and message, masked then cut at 1,000 bytes, and has the cluster secret masked. A logger that
|
|
66
|
+
raises never fails a request, a job or the boot: the line then goes to `$stderr`. A slave whose
|
|
67
|
+
`Rails.logger` was still nil raised instead of logging. Several failures that left no trace at
|
|
68
|
+
all are now logged, once per place and error class for the life of the process: a store lookup or broadcast, the Redis
|
|
69
|
+
version read of the toolbar, the MCP body cache, the mailer collector reading a mail, the route
|
|
70
|
+
lookup of the Request tab, a file profile that cannot be read to rebuild the index. The request middleware saves its
|
|
71
|
+
profiles through `Profiler.save_profile`, like jobs, console commands and tests; a page whose
|
|
72
|
+
profile could not be saved gets neither the `X-Profiler-Token` header nor the toolbar, which
|
|
73
|
+
would point to a missing profile. The profiler's own lines no longer appear in the Logs tab of
|
|
74
|
+
the request during which they are written, nor count as its errors. The same holds while the store is
|
|
75
|
+
unavailable, for pages and for streamed responses, which no longer send `X-Profiler-Token` for a
|
|
76
|
+
profile that will be dropped. The storage warnings (a store that cannot be created, said once
|
|
77
|
+
per cause; a Redis profile that cannot be indexed; a SQLite summary that cannot be read) go to
|
|
78
|
+
the application's log too, and the `run_tests` MCP tool answers an unavailable store as an error
|
|
79
|
+
instead of a run without profiles.
|
|
80
|
+
- **Outbound HTTP:** Record the calls to services on the same machine. `127.0.0.1`, `localhost`
|
|
81
|
+
and `::1` were always left out, so in development the calls to a neighbouring service never
|
|
82
|
+
showed. `config.http_skip_hosts` is now the whole list, empty by default. The profiler's own
|
|
83
|
+
calls are never recorded, whatever the list says: a slave's registration and heartbeats, and the
|
|
84
|
+
master's requests to its slaves (the slave proxy, the fan-out of the profile pages and the MCP
|
|
85
|
+
tools that query a slave), marked where they are made, and any call to the host and port of
|
|
86
|
+
`config.master_url`. `NetHttpInstrumentation::SKIP_HOSTS` stays, deprecated, with its former value
|
|
87
|
+
(`%w[127.0.0.1 localhost ::1]`); nothing reads it any more.
|
|
88
|
+
- **Outbound HTTP:** Keep the bodies of outbound `Net::HTTP` calls to the first
|
|
89
|
+
`config.max_captured_body_bytes`, sent and received, instead of copying them whole; a compressed
|
|
90
|
+
answer is inflated only up to that size, give or take one 16 KB buffer. A `body_stream` is read
|
|
91
|
+
only when it can be put back where it was (a `StringIO`, a file), up to that size and no further
|
|
92
|
+
than its `Content-Length`: a pipe or a socket was read whole and then sent empty, it is now sent
|
|
93
|
+
unread and its body is not captured. A `body_stream` without `pos`, such as a Faraday multipart
|
|
94
|
+
body or a `RestClient::Payload`, is not captured either, where earlier versions replaced it with
|
|
95
|
+
a copy read whole. The size is the `Content-Length` or the stream's own size, otherwise a
|
|
96
|
+
minimum. The HTTP tab and the `get_profile_http` MCP tool say when a body was cut or not
|
|
97
|
+
captured; each call carries `request_body_truncated`, `response_body_truncated`,
|
|
98
|
+
`request_body_not_captured` and `request_size_is_minimum`. Text bodies read in part stay UTF-8.
|
|
99
|
+
- **Middleware:** Cap the `params` a profile keeps at `config.max_captured_body_bytes`: long values
|
|
100
|
+
are cut and the entries past it left out, with a `"[profiler]"` key saying so, and the profile
|
|
101
|
+
carries `params_truncated`. The application still gets all of its params. With the log line
|
|
102
|
+
capped below, a 5 MB JSON request no longer takes about 15 MB in its profile.
|
|
103
|
+
- **Logs:** Cap what a profile keeps of its request's log lines at the new
|
|
104
|
+
`config.max_captured_log_bytes` (1 MB by default). A request, or a stream held open, that logged
|
|
105
|
+
without end grew its thread's buffer without bound; a single line, such as the
|
|
106
|
+
`Parameters: ...` line of a large request, is cut at the cap. The Logs tab ends with the number
|
|
107
|
+
of lines left out, and the collected data carries `truncated` and `dropped`.
|
|
108
|
+
- **Dump:** `Profiler.dump` returns its argument when the profiler is disabled too; it returned
|
|
109
|
+
`nil`, so a call left in the code changed the value of the expression in production.
|
|
110
|
+
- **Engine:** Stop failing the boot of an application that loads `ActionController::API` (an
|
|
111
|
+
`api_only` application, or a single API controller): the engine called `helper` on it, which
|
|
112
|
+
`ActionController::API` does not have (`NoMethodError`).
|
|
113
|
+
- **Toolbar:** Find where the toolbar goes about four times faster on a page of many tags (0.8 ms
|
|
114
|
+
instead of 4 ms for a page of 20 KB and 4,000 tags, 10 ms instead of 25 ms for 320 KB of tags
|
|
115
|
+
with attributes): ordinary text and tags are skipped in one pass. The place is the same, and a
|
|
116
|
+
`</body>` in an attribute, a comment, a script or a CDATA section, an unclosed quote or a
|
|
117
|
+
`<plaintext>` are handled as before.
|
|
118
|
+
- **Upgrading:** to get the previous behaviour back, in `config/initializers/profiler.rb`:
|
|
119
|
+
- the profiler's messages on `$stderr`: `config.logger = Logger.new($stderr)`;
|
|
120
|
+
- the local hosts left out of the HTTP tab:
|
|
121
|
+
`config.http_skip_hosts += Profiler::Configuration::LOCAL_HTTP_HOSTS`;
|
|
122
|
+
- whole outbound bodies and params: `config.max_captured_body_bytes = nil` (also keeps the
|
|
123
|
+
incoming bodies whole); every log line: `config.max_captured_log_bytes = nil`.
|
|
124
|
+
|
|
22
125
|
## [0.31.4] - 2026-10-05
|
|
23
126
|
|
|
24
127
|
<!-- stamped -->
|
|
@@ -4414,6 +4414,13 @@
|
|
|
4414
4414
|
parts.push(` ${JSON.stringify(req.url)}`);
|
|
4415
4415
|
return parts.join(" \\\n");
|
|
4416
4416
|
}
|
|
4417
|
+
function bodyNotes(req) {
|
|
4418
|
+
const notes = [];
|
|
4419
|
+
if (req.request_body_not_captured) notes.push("Request body not captured: a stream that cannot be rewound is sent unread.");
|
|
4420
|
+
if (req.request_body_truncated) notes.push("Request body truncated: only the beginning was kept (max_captured_body_bytes).");
|
|
4421
|
+
if (req.response_body_truncated) notes.push("Response body truncated: only the beginning was kept (max_captured_body_bytes).");
|
|
4422
|
+
return notes;
|
|
4423
|
+
}
|
|
4417
4424
|
function HttpRequestDetail({ req, index, threshold }) {
|
|
4418
4425
|
const [open, setOpen] = d2(false);
|
|
4419
4426
|
const isError = req.status >= 400 || req.status === 0;
|
|
@@ -4436,12 +4443,13 @@
|
|
|
4436
4443
|
/* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
|
|
4437
4444
|
/* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: [
|
|
4438
4445
|
"\u2191 ",
|
|
4439
|
-
formatBytes(req.request_size)
|
|
4446
|
+
req.request_size == null ? "size unknown" : `${req.request_size_is_minimum ? "at least " : ""}${formatBytes(req.request_size)}`,
|
|
4440
4447
|
" \xB7 \u2193 ",
|
|
4441
4448
|
formatBytes(req.response_size)
|
|
4442
4449
|
] }),
|
|
4443
4450
|
req.backtrace && req.backtrace.length > 0 && /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", style: "margin-left:12px", children: req.backtrace[0] })
|
|
4444
4451
|
] }),
|
|
4452
|
+
bodyNotes(req).map((note) => /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: note }) })),
|
|
4445
4453
|
req.error && /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--error", children: req.error }) }),
|
|
4446
4454
|
open && /* @__PURE__ */ u3(
|
|
4447
4455
|
HttpReqRespDetail,
|
|
@@ -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
|
)
|
|
@@ -100,7 +100,7 @@ module Profiler
|
|
|
100
100
|
return profile
|
|
101
101
|
end
|
|
102
102
|
rescue => e
|
|
103
|
-
|
|
103
|
+
Profiler.log_warn("Cluster: cached slave #{cached_slave} failed for #{token}", e)
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
Profiler.token_cache.invalidate(token)
|
|
@@ -130,7 +130,7 @@ module Profiler
|
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
132
|
rescue => e
|
|
133
|
-
|
|
133
|
+
Profiler.log_warn("Cluster: fan-out error for slave #{slave_name} (#{token})", e)
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
end
|
|
@@ -14,7 +14,7 @@ module Profiler
|
|
|
14
14
|
register!
|
|
15
15
|
@registered = true
|
|
16
16
|
rescue => e
|
|
17
|
-
log_warn("
|
|
17
|
+
log_warn("could not register with master at startup, will retry in the heartbeat loop", e)
|
|
18
18
|
end
|
|
19
19
|
start_heartbeat_thread
|
|
20
20
|
end
|
|
@@ -25,7 +25,7 @@ module Profiler
|
|
|
25
25
|
config = Profiler.configuration
|
|
26
26
|
uri = master_uri("register")
|
|
27
27
|
body = { name: config.resolved_name, url: config.self_url }.to_json
|
|
28
|
-
resp = Net::HTTP.post(uri, body, request_headers)
|
|
28
|
+
resp = Profiler.untracked_http { Net::HTTP.post(uri, body, request_headers) }
|
|
29
29
|
unless resp.code.to_i.between?(200, 299)
|
|
30
30
|
raise "Master returned #{resp.code}: #{resp.body.to_s.slice(0, 200)}"
|
|
31
31
|
end
|
|
@@ -37,7 +37,7 @@ module Profiler
|
|
|
37
37
|
config = Profiler.configuration
|
|
38
38
|
uri = master_uri("heartbeat")
|
|
39
39
|
body = { name: config.resolved_name }.to_json
|
|
40
|
-
resp = Net::HTTP.post(uri, body, request_headers)
|
|
40
|
+
resp = Profiler.untracked_http { Net::HTTP.post(uri, body, request_headers) }
|
|
41
41
|
raise "Heartbeat rejected #{resp.code}" unless resp.code.to_i.between?(200, 299)
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -74,25 +74,17 @@ module Profiler
|
|
|
74
74
|
end
|
|
75
75
|
rescue => e
|
|
76
76
|
@registered = false
|
|
77
|
-
log_warn("
|
|
77
|
+
log_warn("communication with the master failed, will retry", e)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def log_info(msg)
|
|
83
|
-
|
|
84
|
-
Rails.logger.info("[Profiler Cluster] #{msg}")
|
|
85
|
-
else
|
|
86
|
-
$stderr.puts("[Profiler Cluster] #{msg}")
|
|
87
|
-
end
|
|
83
|
+
Profiler.log_info("Cluster: #{msg}")
|
|
88
84
|
end
|
|
89
85
|
|
|
90
|
-
def log_warn(msg)
|
|
91
|
-
|
|
92
|
-
Rails.logger.warn("[Profiler Cluster] #{msg}")
|
|
93
|
-
else
|
|
94
|
-
$stderr.puts("[Profiler Cluster] WARN #{msg}")
|
|
95
|
-
end
|
|
86
|
+
def log_warn(msg, error = nil)
|
|
87
|
+
Profiler.log_warn("Cluster: #{msg}", error)
|
|
96
88
|
end
|
|
97
89
|
end
|
|
98
90
|
end
|
|
@@ -44,9 +44,9 @@ module Profiler
|
|
|
44
44
|
return unless config.cluster_master? || config.slave?
|
|
45
45
|
return unless (problem = secret_problem) && secret_required?
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
Profiler.log_warn("Cluster: #{problem}: registrations, heartbeats and proxied requests " \
|
|
48
|
+
"are refused. Generate one with `ruby -rsecurerandom -e 'puts SecureRandom.hex(32)'` " \
|
|
49
|
+
"and give the same value to every node.", logger: logger)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Constant-time comparison of the secret a request carries with the configured one.
|
|
@@ -115,10 +115,13 @@ module Profiler
|
|
|
115
115
|
Security.outgoing_headers.each { |name, value| req[name] = value }
|
|
116
116
|
# Net::HTTP never follows a redirect; a 3xx is reported, without its body, rather than
|
|
117
117
|
# handed back as if the slave had answered.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
# The profiler's own call, never recorded in the profile of the request that makes it.
|
|
119
|
+
resp = Profiler.untracked_http do
|
|
120
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
121
|
+
open_timeout: @open_timeout, read_timeout: @read_timeout,
|
|
122
|
+
use_ssl: uri.scheme == "https") do |http|
|
|
123
|
+
http.request(req)
|
|
124
|
+
end
|
|
122
125
|
end
|
|
123
126
|
return {} if resp.code == "204"
|
|
124
127
|
if resp.code.to_s.start_with?("3")
|
|
@@ -13,7 +13,7 @@ module Profiler
|
|
|
13
13
|
collectors.each { |collector| collector.subscribe if collector.respond_to?(:subscribe) }
|
|
14
14
|
true
|
|
15
15
|
rescue => e
|
|
16
|
-
|
|
16
|
+
Profiler.log_error("#{label}: collector subscribe failed", e)
|
|
17
17
|
release_all(collectors)
|
|
18
18
|
false
|
|
19
19
|
end
|
|
@@ -23,7 +23,7 @@ module Profiler
|
|
|
23
23
|
collectors&.each do |collector|
|
|
24
24
|
collector.unsubscribe if collector.respond_to?(:unsubscribe)
|
|
25
25
|
rescue => e
|
|
26
|
-
|
|
26
|
+
Profiler.log_error("collector #{collector.class} release failed", e)
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -8,6 +8,43 @@ module Profiler
|
|
|
8
8
|
class LogCollector < BaseCollector
|
|
9
9
|
SEVERITY_LABELS = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].freeze
|
|
10
10
|
|
|
11
|
+
# The lines of one request, up to max_captured_log_bytes of messages: past it, a line is cut
|
|
12
|
+
# to what is left, and the next ones are only counted. A request, or a stream held open,
|
|
13
|
+
# that logs without end no longer grows its thread's buffer without bound.
|
|
14
|
+
class Buffer < Array
|
|
15
|
+
attr_reader :bytes, :dropped
|
|
16
|
+
|
|
17
|
+
def initialize(limit)
|
|
18
|
+
super()
|
|
19
|
+
@limit = limit
|
|
20
|
+
@bytes = 0
|
|
21
|
+
@dropped = 0
|
|
22
|
+
@truncated = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def truncated?
|
|
26
|
+
@truncated
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def record(entry)
|
|
30
|
+
message = entry[:message]
|
|
31
|
+
if @limit
|
|
32
|
+
room = @limit - @bytes
|
|
33
|
+
if room <= 0
|
|
34
|
+
@dropped += 1
|
|
35
|
+
@truncated = true
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
if message.bytesize > room
|
|
39
|
+
message = Redaction.cut_bytes(message, room).scrub("")
|
|
40
|
+
@truncated = true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
@bytes += message.bytesize
|
|
44
|
+
self << entry.merge(message: message)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
11
48
|
# Records into the thread's buffer, which only exists while a LogCollector of this thread
|
|
12
49
|
# is subscribed: lines logged by other threads, or after release, are not kept. A logger
|
|
13
50
|
# bound to one collector's buffer records only while that buffer is the thread's current
|
|
@@ -21,17 +58,20 @@ module Profiler
|
|
|
21
58
|
def add(severity, message = nil, progname = nil)
|
|
22
59
|
logs = Thread.current[:profiler_logs]
|
|
23
60
|
return true unless logs
|
|
61
|
+
# The profiler's own messages (Profiler.log) are not the application's.
|
|
62
|
+
return true if Thread.current[:profiler_logging]
|
|
24
63
|
return true if @buffer && !logs.equal?(@buffer)
|
|
25
64
|
|
|
26
65
|
msg = message || progname
|
|
27
66
|
msg = yield if block_given? && msg.nil?
|
|
28
67
|
return true if msg.nil?
|
|
29
68
|
|
|
30
|
-
|
|
69
|
+
entry = {
|
|
31
70
|
level: SEVERITY_LABELS[severity] || "UNKNOWN",
|
|
32
71
|
message: msg.to_s.strip,
|
|
33
72
|
timestamp: Time.now.iso8601(3)
|
|
34
73
|
}
|
|
74
|
+
logs.respond_to?(:record) ? logs.record(entry) : logs << entry
|
|
35
75
|
true
|
|
36
76
|
end
|
|
37
77
|
|
|
@@ -78,7 +118,7 @@ module Profiler
|
|
|
78
118
|
end
|
|
79
119
|
|
|
80
120
|
def subscribe
|
|
81
|
-
@logs = claim_thread_slot(:profiler_logs,
|
|
121
|
+
@logs = claim_thread_slot(:profiler_logs, Buffer.new(Profiler.configuration.max_captured_log_bytes))
|
|
82
122
|
|
|
83
123
|
if defined?(Rails) && Rails.logger
|
|
84
124
|
if Rails.logger.respond_to?(:broadcast_to)
|
|
@@ -90,7 +130,7 @@ module Profiler
|
|
|
90
130
|
end
|
|
91
131
|
end
|
|
92
132
|
rescue => e
|
|
93
|
-
|
|
133
|
+
Profiler.log_error("LogCollector: subscribe failed", e)
|
|
94
134
|
end
|
|
95
135
|
|
|
96
136
|
# Collect reads only what the collector gathered itself.
|
|
@@ -113,12 +153,21 @@ module Profiler
|
|
|
113
153
|
|
|
114
154
|
errors = logs.count { |l| %w[ERROR FATAL].include?(l[:level]) }
|
|
115
155
|
warnings = logs.count { |l| l[:level] == "WARN" }
|
|
156
|
+
dropped = logs.respond_to?(:dropped) ? logs.dropped : 0
|
|
157
|
+
truncated = logs.respond_to?(:truncated?) && logs.truncated?
|
|
158
|
+
lines = logs.to_a
|
|
159
|
+
if dropped.positive?
|
|
160
|
+
lines += [{ level: "WARN", timestamp: Time.now.iso8601(3),
|
|
161
|
+
message: "[Profiler] #{dropped} more log lines were not recorded (max_captured_log_bytes)" }]
|
|
162
|
+
end
|
|
116
163
|
|
|
117
164
|
store_data({
|
|
118
|
-
count: logs.size,
|
|
165
|
+
count: logs.size + dropped,
|
|
119
166
|
errors: errors,
|
|
120
167
|
warnings: warnings,
|
|
121
|
-
logs:
|
|
168
|
+
logs: lines,
|
|
169
|
+
truncated: truncated,
|
|
170
|
+
dropped: dropped
|
|
122
171
|
})
|
|
123
172
|
end
|
|
124
173
|
|
|
@@ -213,7 +213,8 @@ module Profiler
|
|
|
213
213
|
serialize_assign(filtered)
|
|
214
214
|
end
|
|
215
215
|
end
|
|
216
|
-
rescue StandardError
|
|
216
|
+
rescue StandardError => e
|
|
217
|
+
Profiler.log_error_once(:mailer_assigns, "MailerCollector: could not read the mailer's assigns", e)
|
|
217
218
|
{}
|
|
218
219
|
end
|
|
219
220
|
|
|
@@ -232,7 +233,8 @@ module Profiler
|
|
|
232
233
|
text = Profiler::Redaction.truncate(mail.body.decoded, MAX_BODY_SIZE, "")
|
|
233
234
|
end
|
|
234
235
|
[html.nil? || html.empty? ? nil : html, text.nil? || text.empty? ? nil : text]
|
|
235
|
-
rescue StandardError
|
|
236
|
+
rescue StandardError => e
|
|
237
|
+
Profiler.log_error_once(:mailer_body, "MailerCollector: could not read the mail's body", e)
|
|
236
238
|
[nil, nil]
|
|
237
239
|
end
|
|
238
240
|
|
|
@@ -250,7 +252,8 @@ module Profiler
|
|
|
250
252
|
return nil unless mail.is_a?(String)
|
|
251
253
|
|
|
252
254
|
Mail.new(mail)
|
|
253
|
-
rescue StandardError
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
Profiler.log_error_once(:mailer_parse, "MailerCollector: could not parse the mail", e)
|
|
254
257
|
nil
|
|
255
258
|
end
|
|
256
259
|
|
|
@@ -291,7 +294,8 @@ module Profiler
|
|
|
291
294
|
"filename" => att.filename.to_s,
|
|
292
295
|
"size" => att.body.decoded.bytesize
|
|
293
296
|
}
|
|
294
|
-
rescue StandardError
|
|
297
|
+
rescue StandardError => e
|
|
298
|
+
Profiler.log_error_once(:mailer_attachment, "MailerCollector: could not read an attachment", e)
|
|
295
299
|
{ "filename" => att.filename.to_s, "size" => 0 }
|
|
296
300
|
end
|
|
297
301
|
end
|
|
@@ -33,6 +33,7 @@ module Profiler
|
|
|
33
33
|
allocated_objects: @profile.allocated_objects,
|
|
34
34
|
memory: @profile.memory, # deprecated, see Profile#memory
|
|
35
35
|
params: @profile.params,
|
|
36
|
+
params_truncated: @profile.params_truncated || false,
|
|
36
37
|
headers: @profile.headers,
|
|
37
38
|
response_headers: @profile.response_headers,
|
|
38
39
|
request_body: @profile.request_body,
|
|
@@ -103,7 +104,11 @@ module Profiler
|
|
|
103
104
|
route_params: Profiler::Redaction.filter_hash(route_params),
|
|
104
105
|
controller_action: controller_action
|
|
105
106
|
}
|
|
106
|
-
rescue StandardError
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
# A path no route matches (a 404) has no route to show.
|
|
109
|
+
unless defined?(ActionController::RoutingError) && e.is_a?(ActionController::RoutingError)
|
|
110
|
+
Profiler.log_error_once(:request_route, "RequestCollector: could not read the matched route", e)
|
|
111
|
+
end
|
|
107
112
|
{}
|
|
108
113
|
end
|
|
109
114
|
|
|
@@ -219,7 +219,7 @@ module Profiler
|
|
|
219
219
|
handler.call(name, started, finished, id, payload)
|
|
220
220
|
rescue StandardError => e
|
|
221
221
|
# Never into the application's query, nor at the expense of the other collectors.
|
|
222
|
-
|
|
222
|
+
Profiler.log_error("collector failed on #{event}", e)
|
|
223
223
|
end
|
|
224
224
|
end
|
|
225
225
|
end
|
|
@@ -33,6 +33,10 @@ module Profiler
|
|
|
33
33
|
|
|
34
34
|
BACKEND_LOCK = Mutex.new
|
|
35
35
|
|
|
36
|
+
# The hosts earlier versions always left out of the outbound HTTP tab. Add them back with
|
|
37
|
+
# config.http_skip_hosts += Profiler::Configuration::LOCAL_HTTP_HOSTS.
|
|
38
|
+
LOCAL_HTTP_HOSTS = [/\A127\.0\.0\.1\z/, /\Alocalhost\z/i, /\A::1\z/].freeze
|
|
39
|
+
|
|
36
40
|
attr_accessor :storage_options, :collectors,
|
|
37
41
|
:skip_paths, :slow_query_threshold, :max_queries_warning, :sql_backtrace,
|
|
38
42
|
:track_memory, :allocated_objects_warning_threshold,
|
|
@@ -46,12 +50,12 @@ module Profiler
|
|
|
46
50
|
:apply_env_overrides_when_disabled, :restrict_storage_permissions,
|
|
47
51
|
:test_runner_allow_undiscovered_files,
|
|
48
52
|
:track_mailers, :capture_mail_body, :sanitize_mailer_recipients, :mailer_skip_actions,
|
|
49
|
-
:compress_bodies, :compress_body_threshold, :max_captured_body_bytes,
|
|
53
|
+
:compress_bodies, :compress_body_threshold, :max_captured_body_bytes, :max_captured_log_bytes,
|
|
50
54
|
:redact_sensitive_data, :filter_parameters, :env_allowlist,
|
|
51
55
|
:name, :master_url, :self_url,
|
|
52
56
|
:cluster_heartbeat_interval, :cluster_offline_threshold,
|
|
53
57
|
:cluster_master, :cluster_secret, :cluster_require_secret,
|
|
54
|
-
:cluster_allow_insecure_http
|
|
58
|
+
:cluster_allow_insecure_http, :logger
|
|
55
59
|
|
|
56
60
|
attr_reader :authorize_block, :enabled, :track_tests, :max_profiles
|
|
57
61
|
|
|
@@ -89,6 +93,8 @@ module Profiler
|
|
|
89
93
|
@ajax_skip_paths = [/^\/_profiler/]
|
|
90
94
|
@track_http = true
|
|
91
95
|
@slow_http_threshold = 500 # milliseconds
|
|
96
|
+
# Every host whose outbound calls are left out (Strings and Regexps matched against the host).
|
|
97
|
+
# The profiler's own calls (cluster, slave proxy) are never recorded, whatever this says.
|
|
92
98
|
@http_skip_hosts = []
|
|
93
99
|
@http_backtrace_depth = 40
|
|
94
100
|
@track_jobs = true
|
|
@@ -107,6 +113,8 @@ module Profiler
|
|
|
107
113
|
# The request and response bodies kept in a profile stop at this many bytes; the
|
|
108
114
|
# application still reads and sends all of them. nil keeps whole bodies.
|
|
109
115
|
@max_captured_body_bytes = 256 * 1024
|
|
116
|
+
# What a profile keeps of the lines its request logs, in bytes. nil keeps every line.
|
|
117
|
+
@max_captured_log_bytes = 1024 * 1024
|
|
110
118
|
@redact_sensitive_data = true
|
|
111
119
|
@filter_parameters = DEFAULT_FILTER_PARAMETERS.dup
|
|
112
120
|
@env_allowlist = DEFAULT_ENV_ALLOWLIST.dup
|
|
@@ -125,6 +133,9 @@ module Profiler
|
|
|
125
133
|
@cluster_require_secret = true
|
|
126
134
|
@cluster_allowed_slave_urls = []
|
|
127
135
|
@cluster_allow_insecure_http = false
|
|
136
|
+
# Where the profiler's own errors go (Profiler.log): Rails.logger when nil, or $stderr when
|
|
137
|
+
# there is none.
|
|
138
|
+
@logger = nil
|
|
128
139
|
end
|
|
129
140
|
|
|
130
141
|
# Always a Pathname, whether set from a String or left to its default.
|
|
@@ -146,8 +157,8 @@ module Profiler
|
|
|
146
157
|
end
|
|
147
158
|
|
|
148
159
|
def memory_warning_threshold=(bytes)
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
Profiler.log_warn("memory_warning_threshold is deprecated, set allocated_objects_warning_threshold " \
|
|
161
|
+
"(a number of objects) instead")
|
|
151
162
|
@allocated_objects_warning_threshold = bytes && bytes / AllocationCounter::LEGACY_BYTES_PER_OBJECT
|
|
152
163
|
end
|
|
153
164
|
|
|
@@ -76,7 +76,7 @@ module Profiler
|
|
|
76
76
|
collector.collect if collector.respond_to?(:collect)
|
|
77
77
|
profile.add_collector_metadata(collector)
|
|
78
78
|
rescue => e
|
|
79
|
-
|
|
79
|
+
Profiler.log_error("ConsoleProfiler: collector #{collector.class} failed", e)
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
data/lib/profiler/engine.rb
CHANGED
|
@@ -9,8 +9,9 @@ module Profiler
|
|
|
9
9
|
config.profiler = ActiveSupport::OrderedOptions.new
|
|
10
10
|
|
|
11
11
|
initializer "profiler.helpers" do
|
|
12
|
+
# The hook runs for ActionController::API as well, which has no view helpers.
|
|
12
13
|
ActiveSupport.on_load(:action_controller) do
|
|
13
|
-
helper Profiler::Engine.helpers
|
|
14
|
+
helper Profiler::Engine.helpers if respond_to?(:helper)
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
end
|
|
@@ -108,10 +108,10 @@ module Profiler
|
|
|
108
108
|
|
|
109
109
|
@boot_warning_logged = true
|
|
110
110
|
noun, verb = count == 1 ? %w[override was] : %w[overrides were]
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
Profiler.log_warn("#{count} persisted environment #{noun} in #{override_file_path} " \
|
|
112
|
+
"#{verb} not applied: #{BLOCKED_REASONS.fetch(reason)}.", logger: logger)
|
|
113
113
|
rescue => e
|
|
114
|
-
|
|
114
|
+
Profiler.log_error("EnvOverrideStore: failed to check overrides at boot", e)
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
# Set by the test runner in the environment of the test process, to which it already gave
|
|
@@ -158,7 +158,7 @@ module Profiler
|
|
|
158
158
|
end
|
|
159
159
|
end
|
|
160
160
|
rescue => e
|
|
161
|
-
|
|
161
|
+
Profiler.log_error("EnvOverrideStore: failed to apply overrides", e)
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
# Returns active overrides (excludes RESTORE entries, already being reverted)
|