rails-profiler 0.31.0 → 0.31.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 +139 -0
- data/app/assets/builds/profiler-toolbar.js +13 -6
- data/app/assets/builds/profiler.js +53 -33
- data/app/controllers/profiler/api/ajax_controller.rb +6 -0
- data/app/controllers/profiler/api/env_vars_controller.rb +10 -0
- data/lib/profiler/allocation_counter.rb +21 -0
- data/lib/profiler/cluster/slave_proxy.rb +6 -0
- data/lib/profiler/collectors/base_collector.rb +31 -0
- data/lib/profiler/collectors/cache_collector.rb +5 -0
- data/lib/profiler/collectors/database_collector.rb +5 -0
- data/lib/profiler/collectors/exception_collector.rb +5 -0
- data/lib/profiler/collectors/flamegraph_collector.rb +5 -0
- data/lib/profiler/collectors/http_collector.rb +6 -1
- data/lib/profiler/collectors/log_collector.rb +5 -0
- data/lib/profiler/collectors/request_collector.rb +14 -11
- data/lib/profiler/collectors/view_collector.rb +5 -0
- data/lib/profiler/configuration.rb +33 -7
- data/lib/profiler/console_profiler.rb +3 -18
- data/lib/profiler/env_override_store.rb +54 -43
- data/lib/profiler/job_profiler.rb +3 -16
- data/lib/profiler/mcp/body_formatter.rb +4 -4
- data/lib/profiler/mcp/file_cache.rb +19 -7
- data/lib/profiler/mcp/resources/recent_requests.rb +2 -0
- data/lib/profiler/mcp/tools/delete_env_var.rb +3 -1
- data/lib/profiler/mcp/tools/get_profile_detail.rb +13 -1
- data/lib/profiler/mcp/tools/get_test_profile_detail.rb +1 -1
- data/lib/profiler/mcp/tools/list_env_vars.rb +3 -0
- data/lib/profiler/mcp/tools/reset_all_env_vars.rb +3 -0
- data/lib/profiler/mcp/tools/reset_env_var.rb +3 -0
- data/lib/profiler/mcp/tools/set_env_var.rb +2 -0
- data/lib/profiler/middleware/capturing_body.rb +112 -0
- data/lib/profiler/middleware/profiler_middleware.rb +261 -53
- data/lib/profiler/middleware/streamed_profile.rb +135 -0
- data/lib/profiler/middleware/toolbar_injector.rb +3 -10
- data/lib/profiler/models/profile.rb +69 -5
- data/lib/profiler/redaction.rb +10 -0
- data/lib/profiler/storage/base_store.rb +5 -1
- data/lib/profiler/storage/blob_store.rb +28 -4
- data/lib/profiler/storage/file_store.rb +21 -15
- data/lib/profiler/storage/memory_store.rb +6 -0
- data/lib/profiler/storage/private_files.rb +135 -0
- data/lib/profiler/storage/redis_store.rb +6 -0
- data/lib/profiler/storage/sqlite_store.rb +26 -8
- data/lib/profiler/storage/token.rb +24 -0
- data/lib/profiler/test_profiler.rb +3 -18
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +12 -0
- metadata +10 -5
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "pathname"
|
|
3
4
|
require_relative "local_request"
|
|
5
|
+
require_relative "allocation_counter"
|
|
4
6
|
|
|
5
7
|
module Profiler
|
|
6
8
|
# Header the profiler's own clients send on every request. A page on another origin cannot
|
|
@@ -31,7 +33,7 @@ module Profiler
|
|
|
31
33
|
|
|
32
34
|
attr_accessor :storage_options, :collectors,
|
|
33
35
|
:skip_paths, :slow_query_threshold, :max_queries_warning,
|
|
34
|
-
:track_memory, :
|
|
36
|
+
:track_memory, :allocated_objects_warning_threshold,
|
|
35
37
|
:mcp_enabled, :mcp_transport, :mcp_port,
|
|
36
38
|
:authorization_mode, :max_profiles, :extension_cors_enabled,
|
|
37
39
|
:cors_allowed_origins, :api_forgery_protection, :frame_ancestors,
|
|
@@ -39,18 +41,16 @@ module Profiler
|
|
|
39
41
|
:track_http, :slow_http_threshold, :http_skip_hosts, :http_backtrace_depth,
|
|
40
42
|
:track_jobs,
|
|
41
43
|
:track_console,
|
|
42
|
-
:apply_env_overrides_when_disabled,
|
|
44
|
+
:apply_env_overrides_when_disabled, :restrict_storage_permissions,
|
|
43
45
|
:test_runner_allow_undiscovered_files,
|
|
44
46
|
:track_mailers, :capture_mail_body, :sanitize_mailer_recipients, :mailer_skip_actions,
|
|
45
|
-
:compress_bodies, :compress_body_threshold,
|
|
47
|
+
:compress_bodies, :compress_body_threshold, :max_captured_body_bytes,
|
|
46
48
|
:redact_sensitive_data, :filter_parameters, :env_allowlist,
|
|
47
49
|
:name, :master_url, :self_url,
|
|
48
50
|
:cluster_heartbeat_interval, :cluster_offline_threshold,
|
|
49
51
|
:cluster_master, :cluster_secret, :cluster_require_secret,
|
|
50
52
|
:cluster_allow_insecure_http
|
|
51
53
|
|
|
52
|
-
attr_writer :tmp_path
|
|
53
|
-
|
|
54
54
|
attr_reader :authorize_block, :enabled, :track_tests
|
|
55
55
|
|
|
56
56
|
def initialize
|
|
@@ -63,7 +63,9 @@ module Profiler
|
|
|
63
63
|
@slow_query_threshold = 100 # milliseconds
|
|
64
64
|
@max_queries_warning = 50
|
|
65
65
|
@track_memory = true
|
|
66
|
-
|
|
66
|
+
# Not compared with anything yet. The default is the former 100 MB memory threshold read
|
|
67
|
+
# as the objects it stood for.
|
|
68
|
+
@allocated_objects_warning_threshold = 100 * 1024 * 1024 / AllocationCounter::LEGACY_BYTES_PER_OBJECT
|
|
67
69
|
@mcp_enabled = false
|
|
68
70
|
@mcp_transport = :stdio
|
|
69
71
|
@mcp_port = 3001
|
|
@@ -96,10 +98,16 @@ module Profiler
|
|
|
96
98
|
@mailer_skip_actions = []
|
|
97
99
|
@compress_bodies = true
|
|
98
100
|
@compress_body_threshold = 10 * 1024 # 10 KB
|
|
101
|
+
# The request and response bodies kept in a profile stop at this many bytes; the
|
|
102
|
+
# application still reads and sends all of them. nil keeps whole bodies.
|
|
103
|
+
@max_captured_body_bytes = 256 * 1024
|
|
99
104
|
@redact_sensitive_data = true
|
|
100
105
|
@filter_parameters = DEFAULT_FILTER_PARAMETERS.dup
|
|
101
106
|
@env_allowlist = DEFAULT_ENV_ALLOWLIST.dup
|
|
102
107
|
@tmp_path = nil
|
|
108
|
+
# The profiler's directories 0700 and files 0600 (Profiler::Storage::PrivateFiles). false
|
|
109
|
+
# leaves the modes to the umask, for a web process and workers running under two users.
|
|
110
|
+
@restrict_storage_permissions = true
|
|
103
111
|
@name = nil
|
|
104
112
|
@master_url = nil
|
|
105
113
|
@self_url = nil
|
|
@@ -113,12 +121,30 @@ module Profiler
|
|
|
113
121
|
@cluster_allow_insecure_http = false
|
|
114
122
|
end
|
|
115
123
|
|
|
124
|
+
# Always a Pathname, whether set from a String or left to its default.
|
|
116
125
|
def tmp_path
|
|
117
126
|
@tmp_path || default_tmp_path
|
|
118
127
|
end
|
|
119
128
|
|
|
129
|
+
def tmp_path=(value)
|
|
130
|
+
@tmp_path = value.nil? ? nil : Pathname.new(value)
|
|
131
|
+
end
|
|
132
|
+
|
|
120
133
|
attr_reader :cluster_allowed_slave_urls
|
|
121
134
|
|
|
135
|
+
# Deprecated: the figure it was meant for was never a byte count (see AllocationCounter).
|
|
136
|
+
# Read and written as allocated_objects_warning_threshold times the bytes per object the
|
|
137
|
+
# former figure used.
|
|
138
|
+
def memory_warning_threshold
|
|
139
|
+
@allocated_objects_warning_threshold&.*(AllocationCounter::LEGACY_BYTES_PER_OBJECT)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def memory_warning_threshold=(bytes)
|
|
143
|
+
warn "[Profiler] memory_warning_threshold is deprecated, set allocated_objects_warning_threshold " \
|
|
144
|
+
"(a number of objects) instead"
|
|
145
|
+
@allocated_objects_warning_threshold = bytes && bytes / AllocationCounter::LEGACY_BYTES_PER_OBJECT
|
|
146
|
+
end
|
|
147
|
+
|
|
122
148
|
# :any, or a list of slave URLs and anchored Regexp patterns (see Profiler::Cluster::Security).
|
|
123
149
|
# A Regexp that is not anchored with \A and \z is refused here, at boot, rather than ignored.
|
|
124
150
|
def cluster_allowed_slave_urls=(value)
|
|
@@ -226,7 +252,7 @@ module Profiler
|
|
|
226
252
|
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
227
253
|
Rails.root.join("tmp", "rails-profiler")
|
|
228
254
|
else
|
|
229
|
-
File.expand_path("tmp/rails-profiler", Dir.pwd)
|
|
255
|
+
Pathname.new(File.expand_path("tmp/rails-profiler", Dir.pwd))
|
|
230
256
|
end
|
|
231
257
|
end
|
|
232
258
|
|
|
@@ -48,7 +48,7 @@ module Profiler
|
|
|
48
48
|
|
|
49
49
|
exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
allocations_before = AllocationCounter.current if Profiler.configuration.track_memory
|
|
52
52
|
|
|
53
53
|
console_status = "completed"
|
|
54
54
|
|
|
@@ -66,7 +66,7 @@ module Profiler
|
|
|
66
66
|
ensure
|
|
67
67
|
Profiler::CurrentContext.token = previous_token
|
|
68
68
|
if Profiler.configuration.track_memory
|
|
69
|
-
profile.
|
|
69
|
+
profile.allocated_objects = AllocationCounter.current - allocations_before
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
profile.finish(console_status == "completed" ? 200 : 500)
|
|
@@ -80,26 +80,11 @@ module Profiler
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
Profiler.
|
|
83
|
+
Profiler.save_profile(profile, from: "ConsoleProfiler")
|
|
84
84
|
end
|
|
85
85
|
ensure
|
|
86
86
|
# After collect, and also when collect or the storage failed.
|
|
87
87
|
Collectors::Lifecycle.release_all(collectors)
|
|
88
88
|
end
|
|
89
|
-
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
def current_memory
|
|
93
|
-
return 0 unless defined?(GC.stat)
|
|
94
|
-
|
|
95
|
-
stats = GC.stat
|
|
96
|
-
if stats.key?(:total_allocated_size)
|
|
97
|
-
stats[:total_allocated_size]
|
|
98
|
-
elsif stats.key?(:total_allocated_objects)
|
|
99
|
-
stats[:total_allocated_objects] * 40
|
|
100
|
-
else
|
|
101
|
-
0
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
89
|
end
|
|
105
90
|
end
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require_relative "storage/private_files"
|
|
5
6
|
|
|
6
7
|
module Profiler
|
|
7
8
|
class EnvOverrideStore
|
|
9
|
+
# Raised by the methods a caller asked for (set, delete, reset, reset_all, clear and
|
|
10
|
+
# all_overrides) when the overrides file cannot be read or written. The caller then knows that
|
|
11
|
+
# nothing was changed. apply! and apply_at_boot! still only warn: they run at boot and before
|
|
12
|
+
# the application's jobs, where an override must never make the application fail.
|
|
13
|
+
class Error < Profiler::Error; end
|
|
14
|
+
|
|
8
15
|
DELETED_SENTINEL = "__profiler_deleted__"
|
|
9
16
|
RESTORE_SENTINEL = "__profiler_restore__"
|
|
10
17
|
|
|
@@ -30,53 +37,53 @@ module Profiler
|
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
def set(key, value)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
surfacing("set #{key}") do
|
|
41
|
+
remember_process_original(key)
|
|
42
|
+
update_overrides do |overrides|
|
|
43
|
+
overrides[key] = { "value" => value.to_s, "original" => original_for(overrides, key) }
|
|
44
|
+
end
|
|
36
45
|
end
|
|
37
|
-
rescue => e
|
|
38
|
-
warn "[Profiler] EnvOverrideStore: failed to set #{key}: #{e.message}"
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def delete(key)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
surfacing("delete #{key}") do
|
|
50
|
+
remember_process_original(key)
|
|
51
|
+
update_overrides do |overrides|
|
|
52
|
+
overrides[key] = { "value" => DELETED_SENTINEL, "original" => original_for(overrides, key) }
|
|
53
|
+
end
|
|
45
54
|
end
|
|
46
|
-
rescue => e
|
|
47
|
-
warn "[Profiler] EnvOverrideStore: failed to delete #{key}: #{e.message}"
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
# Returns whether ENV was written in this process.
|
|
51
58
|
def reset(key)
|
|
52
59
|
entry = nil
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
surfacing("reset #{key}") do
|
|
61
|
+
update_overrides do |overrides|
|
|
62
|
+
entry = overrides[key]
|
|
63
|
+
# Keep a RESTORE entry so Sidekiq workers pick it up on next job
|
|
64
|
+
overrides[key] = { "value" => RESTORE_SENTINEL, "original" => entry["original"] } if entry
|
|
65
|
+
end
|
|
57
66
|
end
|
|
58
67
|
|
|
59
68
|
# Apply immediately to the current (web) process
|
|
60
69
|
restore_env(key, entry, blocked: blocked_reason)
|
|
61
|
-
rescue => e
|
|
62
|
-
warn "[Profiler] EnvOverrideStore: failed to reset #{key}: #{e.message}"
|
|
63
70
|
end
|
|
64
71
|
|
|
65
72
|
def reset_all
|
|
66
73
|
originals = {}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
surfacing("reset the overrides") do
|
|
75
|
+
update_overrides do |overrides|
|
|
76
|
+
overrides.each do |key, entry|
|
|
77
|
+
originals[key] = entry.is_a?(Hash) ? entry["original"] : nil
|
|
78
|
+
# Leave a RESTORE sentinel for Sidekiq workers to pick up
|
|
79
|
+
overrides[key] = { "value" => RESTORE_SENTINEL, "original" => originals[key] }
|
|
80
|
+
end
|
|
72
81
|
end
|
|
73
82
|
end
|
|
74
83
|
|
|
75
84
|
# Apply immediately to the current (web) process
|
|
76
85
|
blocked = blocked_reason
|
|
77
86
|
originals.each { |key, original| restore_env(key, { "original" => original }, blocked: blocked) }
|
|
78
|
-
rescue => e
|
|
79
|
-
warn "[Profiler] EnvOverrideStore: failed to reset all: #{e.message}"
|
|
80
87
|
end
|
|
81
88
|
|
|
82
89
|
# Why the persisted overrides must not reach ENV in this process, or nil when they may.
|
|
@@ -154,27 +161,36 @@ module Profiler
|
|
|
154
161
|
warn "[Profiler] EnvOverrideStore: failed to apply overrides: #{e.message}"
|
|
155
162
|
end
|
|
156
163
|
|
|
157
|
-
# Returns active overrides (excludes RESTORE entries
|
|
164
|
+
# Returns active overrides (excludes RESTORE entries, already being reverted)
|
|
165
|
+
# A file that does not parse reads as no override; one that cannot be read raises Error.
|
|
158
166
|
def all_overrides
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
surfacing("read the overrides") do
|
|
168
|
+
load_overrides.reject do |_, entry|
|
|
169
|
+
value = entry.is_a?(Hash) ? entry["value"] : entry
|
|
170
|
+
value == RESTORE_SENTINEL
|
|
171
|
+
end.transform_values do |entry|
|
|
172
|
+
entry.is_a?(Hash) ? entry : { "value" => entry, "original" => nil }
|
|
173
|
+
end
|
|
164
174
|
end
|
|
165
|
-
rescue => e
|
|
166
|
-
warn "[Profiler] EnvOverrideStore: failed to load overrides: #{e.message}"
|
|
167
|
-
{}
|
|
168
175
|
end
|
|
169
176
|
|
|
170
177
|
def clear
|
|
171
|
-
with_lock { FileUtils.rm_f(override_file_path) }
|
|
172
|
-
rescue => e
|
|
173
|
-
warn "[Profiler] EnvOverrideStore: failed to clear: #{e.message}"
|
|
178
|
+
surfacing("clear the overrides") { with_lock { FileUtils.rm_f(override_file_path) } }
|
|
174
179
|
end
|
|
175
180
|
|
|
176
181
|
private
|
|
177
182
|
|
|
183
|
+
# The message names the file relative to tmp_path, and an Errno by its description only: it
|
|
184
|
+
# goes back to the Env tab, the MCP tools and the test runner output, without absolute paths.
|
|
185
|
+
def surfacing(action)
|
|
186
|
+
yield
|
|
187
|
+
rescue Error
|
|
188
|
+
raise
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
reason = e.is_a?(SystemCallError) ? e.class.new.message : e.class.name
|
|
191
|
+
raise Error, "could not #{action} in env_overrides.json under tmp_path: #{reason}"
|
|
192
|
+
end
|
|
193
|
+
|
|
178
194
|
def remember_process_original(key)
|
|
179
195
|
@process_originals_lock.synchronize do
|
|
180
196
|
@process_originals[key] = ENV[key] unless @process_originals.key?(key)
|
|
@@ -227,8 +243,8 @@ module Profiler
|
|
|
227
243
|
# An exclusive flock on a file next to the overrides file. Not reentrant: never nest it.
|
|
228
244
|
def with_lock
|
|
229
245
|
path = override_file_path
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
Storage::PrivateFiles.tmp_dir
|
|
247
|
+
Storage::PrivateFiles.open("#{path}.lock") do |lock|
|
|
232
248
|
lock.flock(File::LOCK_EX)
|
|
233
249
|
yield
|
|
234
250
|
end
|
|
@@ -237,12 +253,7 @@ module Profiler
|
|
|
237
253
|
# Written to a temporary file then renamed over the old one, so that a reader never sees a
|
|
238
254
|
# half-written file, which would parse as no override at all.
|
|
239
255
|
def save_overrides(overrides)
|
|
240
|
-
|
|
241
|
-
tmp = "#{path}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
242
|
-
File.write(tmp, JSON.generate(overrides))
|
|
243
|
-
File.rename(tmp, path)
|
|
244
|
-
ensure
|
|
245
|
-
FileUtils.rm_f(tmp) if tmp
|
|
256
|
+
Storage::PrivateFiles.write(override_file_path, JSON.generate(overrides))
|
|
246
257
|
end
|
|
247
258
|
end
|
|
248
259
|
end
|
|
@@ -71,7 +71,7 @@ module Profiler
|
|
|
71
71
|
|
|
72
72
|
exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
allocations_before = AllocationCounter.current if Profiler.configuration.track_memory
|
|
75
75
|
|
|
76
76
|
job_status = "completed"
|
|
77
77
|
error_message = nil
|
|
@@ -92,7 +92,7 @@ module Profiler
|
|
|
92
92
|
Thread.current[:profiler_current_job_class] = previous_job_class
|
|
93
93
|
Profiler::CurrentContext.token = previous_token
|
|
94
94
|
if Profiler.configuration.track_memory
|
|
95
|
-
profile.
|
|
95
|
+
profile.allocated_objects = AllocationCounter.current - allocations_before
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
job_collector.update_status(job_status, error_message)
|
|
@@ -107,7 +107,7 @@ module Profiler
|
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
Profiler.
|
|
110
|
+
Profiler.save_profile(profile, from: "JobProfiler")
|
|
111
111
|
end
|
|
112
112
|
ensure
|
|
113
113
|
# After collect, and also when collect or the storage failed.
|
|
@@ -131,18 +131,5 @@ module Profiler
|
|
|
131
131
|
arg.to_s
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
|
-
|
|
135
|
-
def current_memory
|
|
136
|
-
return 0 unless defined?(GC.stat)
|
|
137
|
-
|
|
138
|
-
stats = GC.stat
|
|
139
|
-
if stats.key?(:total_allocated_size)
|
|
140
|
-
stats[:total_allocated_size]
|
|
141
|
-
elsif stats.key?(:total_allocated_objects)
|
|
142
|
-
stats[:total_allocated_objects] * 40
|
|
143
|
-
else
|
|
144
|
-
0
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
134
|
end
|
|
148
135
|
end
|
|
@@ -9,10 +9,10 @@ module Profiler
|
|
|
9
9
|
# Formats a body string for MCP output.
|
|
10
10
|
#
|
|
11
11
|
# params keys used:
|
|
12
|
-
# "save_bodies" (boolean)
|
|
13
|
-
# "max_body_size" (number)
|
|
14
|
-
# "json_path" (string)
|
|
15
|
-
# "xml_path" (string)
|
|
12
|
+
# "save_bodies" (boolean): save to {tmp_path}/mcp-cache/{token}/{name} and return path
|
|
13
|
+
# "max_body_size" (number): truncate inline body at N chars
|
|
14
|
+
# "json_path" (string): JSONPath to extract from body when save_bodies is true
|
|
15
|
+
# "xml_path" (string): XPath to extract from body when save_bodies is true
|
|
16
16
|
#
|
|
17
17
|
# Returns a formatted string ready to embed in markdown, or nil if body is blank.
|
|
18
18
|
def self.format_body(token, name, body, encoding, params)
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "../storage/private_files"
|
|
5
|
+
require_relative "../storage/token"
|
|
4
6
|
|
|
5
7
|
module Profiler
|
|
6
8
|
module MCP
|
|
9
|
+
# The bodies the MCP tools save on request (save_bodies), under tmp_path/mcp-cache/<token>/<name>.
|
|
10
|
+
# The token comes from a profile, which a slave may have sent: one that is not a token the gem
|
|
11
|
+
# issues, or a name that is not one the tools use, is refused. The cleanup only removes the
|
|
12
|
+
# token directories of this cache: tmp_path also holds the profiles, the SQLite blobs and the
|
|
13
|
+
# env overrides.
|
|
7
14
|
class FileCache
|
|
15
|
+
DIR_NAME = "mcp-cache"
|
|
16
|
+
NAME_FORMAT = /\A[a-z0-9_]+\z/
|
|
17
|
+
|
|
8
18
|
def self.base_dir
|
|
9
|
-
Profiler.configuration.tmp_path.to_s
|
|
19
|
+
Profiler.configuration.tmp_path.join(DIR_NAME).to_s
|
|
10
20
|
end
|
|
11
21
|
|
|
12
22
|
def self.save(token, name, content)
|
|
23
|
+
return nil unless Storage::Token.valid?(token) && name.is_a?(String) && NAME_FORMAT.match?(name)
|
|
24
|
+
|
|
13
25
|
cleanup if rand < 0.05
|
|
14
26
|
|
|
15
|
-
dir =
|
|
16
|
-
|
|
17
|
-
path = File.join(dir, name)
|
|
18
|
-
File.write(path, content)
|
|
19
|
-
path
|
|
27
|
+
dir = Storage::PrivateFiles.tmp_dir(DIR_NAME, token)
|
|
28
|
+
Storage::PrivateFiles.write(dir.join(name), content)
|
|
20
29
|
rescue Errno::EACCES, Errno::EROFS
|
|
21
30
|
nil
|
|
22
31
|
end
|
|
@@ -25,7 +34,10 @@ module Profiler
|
|
|
25
34
|
bd = base_dir
|
|
26
35
|
return unless Dir.exist?(bd)
|
|
27
36
|
|
|
28
|
-
Dir.
|
|
37
|
+
Dir.children(bd).each do |entry|
|
|
38
|
+
next unless Storage::Token.valid?(entry)
|
|
39
|
+
|
|
40
|
+
dir = File.join(bd, entry)
|
|
29
41
|
FileUtils.rm_rf(dir) if File.directory?(dir) && (Time.now - File.mtime(dir)) > max_age
|
|
30
42
|
end
|
|
31
43
|
end
|
|
@@ -14,6 +14,8 @@ module Profiler
|
|
|
14
14
|
method: profile.method,
|
|
15
15
|
status: profile.status,
|
|
16
16
|
duration: profile.duration&.round(2),
|
|
17
|
+
allocated_objects: profile.allocated_objects,
|
|
18
|
+
# Deprecated, kept for the readers that expect it: allocated_objects times 40, in MB.
|
|
17
19
|
memory: profile.memory ? (profile.memory / 1024.0 / 1024.0).round(2) : nil,
|
|
18
20
|
timestamp: profile.started_at&.iso8601,
|
|
19
21
|
query_count: profile.collector_data("database")&.dig("total_queries") || 0,
|
|
@@ -26,7 +26,9 @@ module Profiler
|
|
|
26
26
|
Profiler.env_override_store.delete(key)
|
|
27
27
|
ENV.delete(key)
|
|
28
28
|
|
|
29
|
-
[{ type: "text", text: "Deleted #{key}. Override persisted
|
|
29
|
+
[{ type: "text", text: "Deleted #{key}. Override persisted: it will remain deleted across restarts until reset." }]
|
|
30
|
+
rescue Profiler::EnvOverrideStore::Error => e
|
|
31
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.message}; #{key} was left unchanged." }], error: true)
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
end
|
|
@@ -87,7 +87,7 @@ module Profiler
|
|
|
87
87
|
lines << "**Request:** #{profile.method} #{profile.path}"
|
|
88
88
|
lines << "**Status:** #{profile.status}"
|
|
89
89
|
lines << "**Duration:** #{profile.duration.round(2)} ms"
|
|
90
|
-
lines << "**
|
|
90
|
+
lines << "**Allocated objects:** #{profile.allocated_objects}" if profile.allocated_objects
|
|
91
91
|
lines << "**Time:** #{profile.started_at}"
|
|
92
92
|
if profile.gem_version
|
|
93
93
|
if profile.gem_version != Profiler::VERSION
|
|
@@ -184,6 +184,9 @@ module Profiler
|
|
|
184
184
|
req_body = req_data["request_body"]
|
|
185
185
|
if req_body && !req_body.empty?
|
|
186
186
|
lines << "## Request Body"
|
|
187
|
+
if req_data["request_body_truncated"]
|
|
188
|
+
lines << truncation_note(req_data["request_body_size"], req_data["request_body_size_is_minimum"])
|
|
189
|
+
end
|
|
187
190
|
formatted = BodyFormatter.format_body(
|
|
188
191
|
profile.token,
|
|
189
192
|
"request_body",
|
|
@@ -209,6 +212,10 @@ module Profiler
|
|
|
209
212
|
resp_body = profile.response_body
|
|
210
213
|
if resp_body && !resp_body.empty?
|
|
211
214
|
lines << "## Response Body"
|
|
215
|
+
response_data = profile.collector_data("request") || {}
|
|
216
|
+
if response_data["response_body_truncated"]
|
|
217
|
+
lines << truncation_note(response_data["response_body_size"], response_data["response_body_size_is_minimum"])
|
|
218
|
+
end
|
|
212
219
|
formatted = BodyFormatter.format_body(
|
|
213
220
|
profile.token,
|
|
214
221
|
"response_body",
|
|
@@ -222,6 +229,11 @@ module Profiler
|
|
|
222
229
|
lines
|
|
223
230
|
end
|
|
224
231
|
|
|
232
|
+
def self.truncation_note(size, minimum)
|
|
233
|
+
"_Truncated: the body was #{"at least " if minimum}#{size || "?"} bytes, only its beginning was kept " \
|
|
234
|
+
"(max_captured_body_bytes)._"
|
|
235
|
+
end
|
|
236
|
+
|
|
225
237
|
def self.section_curl(profile)
|
|
226
238
|
req_data = profile.collector_data("request")
|
|
227
239
|
lines = []
|
|
@@ -52,7 +52,7 @@ module Profiler
|
|
|
52
52
|
lines << "| File | #{test_data["test_file"]}:#{test_data["test_line"]} |"
|
|
53
53
|
lines << "| Duration | #{profile.duration&.round(2)}ms |"
|
|
54
54
|
lines << "| Assertions | #{test_data["assertions"] || "-"} |"
|
|
55
|
-
lines << "|
|
|
55
|
+
lines << "| Allocated objects | #{profile.allocated_objects || "-"} |"
|
|
56
56
|
lines << "| Time | #{profile.started_at&.strftime("%H:%M:%S")} |"
|
|
57
57
|
|
|
58
58
|
# Exception / skip
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "mcp"
|
|
3
4
|
require_relative "../slave_support"
|
|
4
5
|
require_relative "../../env_override_store"
|
|
5
6
|
require_relative "../../redaction"
|
|
@@ -61,6 +62,8 @@ module Profiler
|
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
[{ type: "text", text: text }]
|
|
65
|
+
rescue Profiler::EnvOverrideStore::Error => e
|
|
66
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.message}." }], error: true)
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
private
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "mcp"
|
|
3
4
|
require_relative "../slave_support"
|
|
4
5
|
|
|
5
6
|
module Profiler
|
|
@@ -22,6 +23,8 @@ module Profiler
|
|
|
22
23
|
Profiler.env_override_store.reset_all
|
|
23
24
|
|
|
24
25
|
[{ type: "text", text: "Reset #{count} environment variable#{"s" if count != 1} to original values." }]
|
|
26
|
+
rescue Profiler::EnvOverrideStore::Error => e
|
|
27
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.message}; nothing was changed." }], error: true)
|
|
25
28
|
end
|
|
26
29
|
end
|
|
27
30
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "mcp"
|
|
3
4
|
require_relative "../slave_support"
|
|
4
5
|
|
|
5
6
|
require "uri"
|
|
@@ -27,6 +28,8 @@ module Profiler
|
|
|
27
28
|
|
|
28
29
|
done = restored ? "original value restored" : "ENV left unchanged"
|
|
29
30
|
[{ type: "text", text: "Reset #{key}: override removed, #{done} in this process." }]
|
|
31
|
+
rescue Profiler::EnvOverrideStore::Error => e
|
|
32
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.message}; #{key} was left unchanged." }], error: true)
|
|
30
33
|
end
|
|
31
34
|
end
|
|
32
35
|
end
|
|
@@ -33,6 +33,8 @@ module Profiler
|
|
|
33
33
|
ENV[key] = value
|
|
34
34
|
|
|
35
35
|
[{ type: "text", text: "Set #{key}=#{value}" }]
|
|
36
|
+
rescue Profiler::EnvOverrideStore::Error => e
|
|
37
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.message}; #{key} was left unchanged." }], error: true)
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
end
|