rails-profiler 0.31.4 → 0.31.5
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 +73 -0
- data/app/assets/builds/profiler.js +9 -1
- data/app/controllers/profiler/profiles_controller.rb +2 -2
- 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/file_cache.rb +2 -1
- data/lib/profiler/mcp/server.rb +1 -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 +60 -2
- 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/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
|
@@ -19,7 +19,7 @@ module Profiler
|
|
|
19
19
|
:response_body, :response_body_encoding,
|
|
20
20
|
:request_body_size, :request_body_truncated, :request_body_size_is_minimum,
|
|
21
21
|
:response_body_size, :response_body_truncated, :response_body_size_is_minimum,
|
|
22
|
-
:collectors_released_after_seconds,
|
|
22
|
+
:collectors_released_after_seconds, :params_truncated,
|
|
23
23
|
:gem_version
|
|
24
24
|
|
|
25
25
|
def initialize(request = nil)
|
|
@@ -34,7 +34,8 @@ module Profiler
|
|
|
34
34
|
if request
|
|
35
35
|
@path = request.path
|
|
36
36
|
@method = request.request_method
|
|
37
|
-
@params = sanitize_params(request.params)
|
|
37
|
+
@params, @params_truncated = cap_params(sanitize_params(request.params),
|
|
38
|
+
Profiler.configuration.max_captured_body_bytes)
|
|
38
39
|
@headers = extract_headers(request.env)
|
|
39
40
|
end
|
|
40
41
|
end
|
|
@@ -133,6 +134,7 @@ module Profiler
|
|
|
133
134
|
started_at: @started_at&.iso8601,
|
|
134
135
|
finished_at: @finished_at&.iso8601,
|
|
135
136
|
params: @params,
|
|
137
|
+
params_truncated: @params_truncated,
|
|
136
138
|
headers: @headers,
|
|
137
139
|
response_headers: @response_headers,
|
|
138
140
|
request_body: req_body,
|
|
@@ -178,6 +180,7 @@ module Profiler
|
|
|
178
180
|
profile.started_at = data[:started_at] ? Time.parse(data[:started_at]) : nil
|
|
179
181
|
profile.finished_at = data[:finished_at] ? Time.parse(data[:finished_at]) : nil
|
|
180
182
|
profile.params = data[:params]
|
|
183
|
+
profile.params_truncated = data[:params_truncated]
|
|
181
184
|
profile.headers = data[:headers]
|
|
182
185
|
profile.response_headers = data[:response_headers]
|
|
183
186
|
profile.request_body = data[:request_body]
|
|
@@ -266,6 +269,61 @@ module Profiler
|
|
|
266
269
|
|
|
267
270
|
LEGACY_FILTERED_PARAMS = %w[password password_confirmation token secret].freeze
|
|
268
271
|
|
|
272
|
+
# The key added to params cut at max_captured_body_bytes, so that the cut shows wherever
|
|
273
|
+
# the params are read.
|
|
274
|
+
PARAMS_TRUNCATED_KEY = "[profiler]"
|
|
275
|
+
|
|
276
|
+
# A copy of +params+ whose JSON stays within about +limit+ bytes: long strings are cut, and
|
|
277
|
+
# the entries past the limit left out. The application's own params are never touched.
|
|
278
|
+
# Returns the params and whether they were cut.
|
|
279
|
+
def cap_params(params, limit)
|
|
280
|
+
return [params, false] if limit.nil? || !params.is_a?(Hash)
|
|
281
|
+
|
|
282
|
+
budget = [limit]
|
|
283
|
+
cut = [false]
|
|
284
|
+
capped = cap_value(params, budget, cut)
|
|
285
|
+
return [capped, false] unless cut[0]
|
|
286
|
+
|
|
287
|
+
capped[PARAMS_TRUNCATED_KEY] = "params truncated at #{limit} bytes (max_captured_body_bytes)"
|
|
288
|
+
[capped, true]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def cap_value(value, budget, cut)
|
|
292
|
+
case value
|
|
293
|
+
when Hash
|
|
294
|
+
value.each_with_object({}) do |(key, item), out|
|
|
295
|
+
cost = key.to_s.bytesize + 4
|
|
296
|
+
if cost >= budget[0]
|
|
297
|
+
cut[0] = true
|
|
298
|
+
break out
|
|
299
|
+
end
|
|
300
|
+
budget[0] -= cost
|
|
301
|
+
out[key] = cap_value(item, budget, cut)
|
|
302
|
+
end
|
|
303
|
+
when Array
|
|
304
|
+
value.each_with_object([]) do |item, out|
|
|
305
|
+
if budget[0] <= 1
|
|
306
|
+
cut[0] = true
|
|
307
|
+
break out
|
|
308
|
+
end
|
|
309
|
+
budget[0] -= 1
|
|
310
|
+
out << cap_value(item, budget, cut)
|
|
311
|
+
end
|
|
312
|
+
when String
|
|
313
|
+
cost = value.bytesize + 2
|
|
314
|
+
if cost > budget[0]
|
|
315
|
+
cut[0] = true
|
|
316
|
+
value = Redaction.cut_bytes(value, [budget[0] - 2, 0].max).scrub("")
|
|
317
|
+
cost = value.bytesize + 2
|
|
318
|
+
end
|
|
319
|
+
budget[0] -= cost
|
|
320
|
+
value
|
|
321
|
+
else
|
|
322
|
+
budget[0] -= value.to_s.bytesize
|
|
323
|
+
value
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
269
327
|
def sanitize_params(params)
|
|
270
328
|
return {} unless params
|
|
271
329
|
|
|
@@ -36,7 +36,11 @@ module Profiler
|
|
|
36
36
|
candidates.find { |route| route[:verb].split("|").include?(verb) } ||
|
|
37
37
|
candidates.find { |route| route[:verb] == "ANY" } ||
|
|
38
38
|
candidates.first
|
|
39
|
-
rescue StandardError
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
# A path no route matches (a 404) has no route to show.
|
|
41
|
+
unless defined?(ActionController::RoutingError) && e.is_a?(ActionController::RoutingError)
|
|
42
|
+
Profiler.log_error_once(:snapshot_route, "could not find the route of a profile", e)
|
|
43
|
+
end
|
|
40
44
|
nil
|
|
41
45
|
end
|
|
42
46
|
|
|
@@ -61,7 +65,7 @@ module Profiler
|
|
|
61
65
|
end
|
|
62
66
|
profile
|
|
63
67
|
rescue StandardError => e
|
|
64
|
-
|
|
68
|
+
Profiler.log_error("could not add the routes and ENV to profile #{profile.token}", e)
|
|
65
69
|
profile
|
|
66
70
|
end
|
|
67
71
|
|
|
@@ -84,7 +88,8 @@ module Profiler
|
|
|
84
88
|
@routes = build_snapshot(fingerprint, list) unless @routes && @routes[0] == fingerprint
|
|
85
89
|
@routes
|
|
86
90
|
end
|
|
87
|
-
rescue StandardError
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
Profiler.log_error_once(:snapshot_routes, "could not read the application's routes", e)
|
|
88
93
|
[nil, [].freeze, {}.freeze]
|
|
89
94
|
end
|
|
90
95
|
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Profiler
|
|
|
23
23
|
if config.respond_to?("#{key}=")
|
|
24
24
|
config.public_send("#{key}=", value)
|
|
25
25
|
else
|
|
26
|
-
|
|
26
|
+
Profiler.log_warn("config.profiler.#{key} is not a profiler option, ignored")
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
data/lib/profiler/redaction.rb
CHANGED
|
@@ -518,9 +518,7 @@ module Profiler
|
|
|
518
518
|
return if reported.include?(error.class)
|
|
519
519
|
|
|
520
520
|
reported << error.class
|
|
521
|
-
|
|
522
|
-
logger = ::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
523
|
-
logger ? logger.warn(message) : Kernel.warn(message)
|
|
521
|
+
Profiler.log_warn("Redaction: #{error.class} while #{during}; the value was masked instead.")
|
|
524
522
|
rescue StandardError
|
|
525
523
|
nil
|
|
526
524
|
end
|
|
@@ -29,7 +29,8 @@ module Profiler
|
|
|
29
29
|
# The version of the last save of +token+, 0 when none is known or Redis cannot be read.
|
|
30
30
|
def version(token)
|
|
31
31
|
redis.get(key(token)).to_i
|
|
32
|
-
rescue StandardError
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
Profiler.log_error_once(:redis_event_bus_version, "RedisEventBus: could not read the version of a profile", e)
|
|
33
34
|
0
|
|
34
35
|
end
|
|
35
36
|
|
|
@@ -34,7 +34,8 @@ module Profiler
|
|
|
34
34
|
|
|
35
35
|
def exists?(token)
|
|
36
36
|
!load(token).nil?
|
|
37
|
-
rescue
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
Profiler.log_error_once(:store_exists, "#{self.class.name.split("::").last}: could not look up a profile", e)
|
|
38
39
|
false
|
|
39
40
|
end
|
|
40
41
|
|
|
@@ -54,8 +55,9 @@ module Profiler
|
|
|
54
55
|
|
|
55
56
|
def broadcast_event(token)
|
|
56
57
|
Profiler::SSE.current.broadcast(token)
|
|
57
|
-
rescue StandardError
|
|
58
|
+
rescue StandardError => e
|
|
58
59
|
# Never let a broadcast failure prevent profile persistence
|
|
60
|
+
Profiler.log_error_once(:store_broadcast, "#{self.class.name.split("::").last}: could not announce a saved profile", e)
|
|
59
61
|
end
|
|
60
62
|
end
|
|
61
63
|
end
|
|
@@ -100,7 +100,7 @@ module Profiler
|
|
|
100
100
|
json_data = File.read(file_path)
|
|
101
101
|
Models::Profile.from_json(json_data)
|
|
102
102
|
rescue StandardError => e
|
|
103
|
-
|
|
103
|
+
Profiler.log_error("FileStore: could not load profile #{token}", e)
|
|
104
104
|
nil
|
|
105
105
|
end
|
|
106
106
|
|
|
@@ -345,9 +345,14 @@ module Profiler
|
|
|
345
345
|
json = PrivateFiles.open_for_reading(profile_file_path(token), &:read)
|
|
346
346
|
record = put_record(token, Models::Profile.from_json(json).to_h, json.bytesize)
|
|
347
347
|
Entry.new(token, record["at"], record["type"], record["parent"], record["bytes"], record["summary"])
|
|
348
|
-
rescue StandardError
|
|
348
|
+
rescue StandardError => e
|
|
349
349
|
# Unreadable: kept in the count, so that it is evicted in turn; listed by no one.
|
|
350
|
-
|
|
350
|
+
Profiler.log_error_once(:file_store_entry, "FileStore: could not read profile #{token} to index it", e)
|
|
351
|
+
stat = begin
|
|
352
|
+
File.lstat(profile_file_path(token))
|
|
353
|
+
rescue SystemCallError
|
|
354
|
+
nil
|
|
355
|
+
end
|
|
351
356
|
Entry.new(token, stat ? stat.mtime.to_f : 0.0, nil, nil, stat ? stat.size : 0, nil)
|
|
352
357
|
end
|
|
353
358
|
|
|
@@ -125,9 +125,9 @@ module Profiler
|
|
|
125
125
|
first = @warn_mutex.synchronize { @warned[key] ? false : (@warned[key] = true) }
|
|
126
126
|
return unless first
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
Profiler.log_warn("tmp_path #{key} belongs to another user or is writable by group or others " \
|
|
129
|
+
"(mode #{format("%o", stat.mode & 0o777)}): other local users could place files there. Set " \
|
|
130
|
+
"config.tmp_path to a directory of your own, or run chmod 700 on it.")
|
|
131
131
|
rescue SystemCallError
|
|
132
132
|
nil
|
|
133
133
|
end
|
|
@@ -64,7 +64,7 @@ module Profiler
|
|
|
64
64
|
|
|
65
65
|
Models::Profile.from_json(json_data)
|
|
66
66
|
rescue => e
|
|
67
|
-
|
|
67
|
+
Profiler.log_error("RedisStore: could not load profile #{token}", e)
|
|
68
68
|
nil
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -270,7 +270,7 @@ module Profiler
|
|
|
270
270
|
data = JSON.parse(json, symbolize_names: true)
|
|
271
271
|
[token, data[:started_at] ? Time.parse(data[:started_at]).to_f : 0.0, Summary.build(data)]
|
|
272
272
|
rescue StandardError => e
|
|
273
|
-
|
|
273
|
+
Profiler.log_error("RedisStore: could not index profile #{token}", e)
|
|
274
274
|
nil
|
|
275
275
|
end
|
|
276
276
|
@redis.pipelined do |pipe|
|
|
@@ -102,7 +102,7 @@ module Profiler
|
|
|
102
102
|
|
|
103
103
|
row_to_profile(row)
|
|
104
104
|
rescue => e
|
|
105
|
-
|
|
105
|
+
Profiler.log_error("SqliteStore: could not load profile #{token}", e)
|
|
106
106
|
nil
|
|
107
107
|
end
|
|
108
108
|
|
|
@@ -191,7 +191,7 @@ module Profiler
|
|
|
191
191
|
|
|
192
192
|
Summary.to_profile(JSON.parse(row["summary"]))
|
|
193
193
|
rescue JSON::ParserError => e
|
|
194
|
-
|
|
194
|
+
Profiler.log_error("SqliteStore: could not read the summary of #{row["token"]}", e)
|
|
195
195
|
nil
|
|
196
196
|
end
|
|
197
197
|
|
|
@@ -308,7 +308,7 @@ module Profiler
|
|
|
308
308
|
collectors_data: collectors_data
|
|
309
309
|
)
|
|
310
310
|
rescue => e
|
|
311
|
-
|
|
311
|
+
Profiler.log_error("SqliteStore: could not deserialize profile #{row["token"]}", e)
|
|
312
312
|
nil
|
|
313
313
|
end
|
|
314
314
|
|
|
@@ -21,8 +21,7 @@ module Profiler
|
|
|
21
21
|
key = [Process.pid, error.class.name, error.message]
|
|
22
22
|
first = @mutex.synchronize { @said[key] ? false : (@said[key] = true) }
|
|
23
23
|
if first
|
|
24
|
-
|
|
25
|
-
"#{error.class}: #{error.message}"
|
|
24
|
+
Profiler.log_warn("storage unavailable, profiles are not saved until it is fixed", error)
|
|
26
25
|
end
|
|
27
26
|
new(error)
|
|
28
27
|
end
|
|
@@ -114,7 +114,7 @@ module Profiler
|
|
|
114
114
|
collector.collect if collector.respond_to?(:collect)
|
|
115
115
|
profile.add_collector_metadata(collector)
|
|
116
116
|
rescue => e
|
|
117
|
-
|
|
117
|
+
Profiler.log_error("TestProfiler: collector #{collector.class} failed", e)
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
|
|
@@ -51,7 +51,7 @@ module Profiler
|
|
|
51
51
|
root = project_root
|
|
52
52
|
allowed = if Profiler.configuration.test_runner_allow_undiscovered_files
|
|
53
53
|
warn_once(:undiscovered,
|
|
54
|
-
"
|
|
54
|
+
"config.test_runner_allow_undiscovered_files is enabled: the test runner " \
|
|
55
55
|
"runs any file under the Rails root, not only the discovered tests.")
|
|
56
56
|
nil
|
|
57
57
|
else
|
|
@@ -101,11 +101,7 @@ module Profiler
|
|
|
101
101
|
@warned[key] = true
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
Rails.logger.warn(message)
|
|
106
|
-
else
|
|
107
|
-
warn(message)
|
|
108
|
-
end
|
|
104
|
+
Profiler.log_warn(message)
|
|
109
105
|
end
|
|
110
106
|
|
|
111
107
|
def self.reset_warnings!
|
|
@@ -227,7 +223,7 @@ module Profiler
|
|
|
227
223
|
|
|
228
224
|
unless left_out.empty?
|
|
229
225
|
warn_once([:env, left_out.sort],
|
|
230
|
-
"
|
|
226
|
+
"Environment overrides not passed to the test runner, because they make it " \
|
|
231
227
|
"load other code: #{left_out.sort.join(", ")}.")
|
|
232
228
|
end
|
|
233
229
|
|
data/lib/profiler/version.rb
CHANGED
data/lib/profiler.rb
CHANGED
|
@@ -10,6 +10,10 @@ require_relative "profiler/allocation_counter"
|
|
|
10
10
|
module Profiler
|
|
11
11
|
class Error < StandardError; end
|
|
12
12
|
|
|
13
|
+
# The longest error message, in bytes, a log line carries: a parser error can quote what it
|
|
14
|
+
# was parsing.
|
|
15
|
+
LOG_ERROR_MESSAGE_LIMIT = 1000
|
|
16
|
+
|
|
13
17
|
class << self
|
|
14
18
|
attr_writer :configuration
|
|
15
19
|
attr_accessor :function_profiling_enabled
|
|
@@ -43,11 +47,75 @@ module Profiler
|
|
|
43
47
|
|
|
44
48
|
# Saves a profile from a path of the application (a job, a console command, a test, an
|
|
45
49
|
# outbound HTTP call): a storage error loses the profile, never the application's work.
|
|
50
|
+
# True when saved; false when the error was logged instead, or when the store is unavailable
|
|
51
|
+
# (Storage::Unavailable drops the save, and has said why once already).
|
|
46
52
|
def save_profile(profile, from:)
|
|
47
|
-
storage
|
|
53
|
+
target = storage
|
|
54
|
+
return false if target.is_a?(Storage::Unavailable)
|
|
55
|
+
|
|
56
|
+
target.save(profile.token, profile)
|
|
57
|
+
true
|
|
48
58
|
rescue StandardError => e
|
|
49
|
-
|
|
59
|
+
log_error("#{from}: could not save profile #{profile.token}", e)
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The profiler's own messages, to config.logger when the application sets one (read on each
|
|
64
|
+
# message), else to +logger+ when given (Rails.logger at boot), else to Rails.logger, else to
|
|
65
|
+
# $stderr.
|
|
66
|
+
# Prefixed [Profiler], with the profiler's credentials masked. Never raises: a logger that
|
|
67
|
+
# fails sends the line to $stderr instead. While the line is written, the thread is marked
|
|
68
|
+
# (Thread.current[:profiler_logging]) so that the Logs tab of a profile being recorded does
|
|
69
|
+
# not take it for one of the application's lines.
|
|
70
|
+
def log(level, message, error = nil, backtrace: false, logger: nil)
|
|
71
|
+
line = log_line(message, error, backtrace)
|
|
72
|
+
logger = configured_logger || logger || current_logger
|
|
73
|
+
if logger
|
|
74
|
+
begin
|
|
75
|
+
return write_log(logger, level, line)
|
|
76
|
+
rescue StandardError
|
|
77
|
+
# Below, to $stderr: the message is not lost.
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
$stderr.write("#{line}\n")
|
|
50
81
|
nil
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def log_error(message, error = nil, backtrace: false)
|
|
87
|
+
log(:error, message, error, backtrace: backtrace)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def log_warn(message, error = nil, logger: nil)
|
|
91
|
+
log(:warn, message, error, logger: logger)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def log_info(message)
|
|
95
|
+
log(:info, message)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Like log_error, once per +site+ and error class: for the paths called again and again (a
|
|
99
|
+
# store read on every poll), where the same failure would otherwise fill the log.
|
|
100
|
+
def log_error_once(site, message, error)
|
|
101
|
+
key = [site, error.class]
|
|
102
|
+
@logged_once_mutex.synchronize do
|
|
103
|
+
return nil if @logged_once.include?(key)
|
|
104
|
+
|
|
105
|
+
@logged_once << key
|
|
106
|
+
end
|
|
107
|
+
log_error(message, error)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Runs the block without recording its outgoing HTTP calls in the current profile: the
|
|
111
|
+
# profiler's own calls (the cluster's registration and heartbeats, the master's requests to
|
|
112
|
+
# its slaves) are not the application's.
|
|
113
|
+
def untracked_http
|
|
114
|
+
previous = Thread.current[:profiler_http_untracked]
|
|
115
|
+
Thread.current[:profiler_http_untracked] = true
|
|
116
|
+
yield
|
|
117
|
+
ensure
|
|
118
|
+
Thread.current[:profiler_http_untracked] = previous
|
|
51
119
|
end
|
|
52
120
|
|
|
53
121
|
def env_override_store
|
|
@@ -94,10 +162,62 @@ module Profiler
|
|
|
94
162
|
result
|
|
95
163
|
end
|
|
96
164
|
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def configured_logger
|
|
168
|
+
configuration.logger
|
|
169
|
+
rescue StandardError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def current_logger
|
|
174
|
+
configured = configuration.logger
|
|
175
|
+
return configured if configured
|
|
176
|
+
return nil unless defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
177
|
+
|
|
178
|
+
::Rails.logger
|
|
179
|
+
rescue StandardError
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def write_log(logger, level, line)
|
|
184
|
+
previous = Thread.current[:profiler_logging]
|
|
185
|
+
Thread.current[:profiler_logging] = true
|
|
186
|
+
logger.public_send(level, line)
|
|
187
|
+
ensure
|
|
188
|
+
Thread.current[:profiler_logging] = previous
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Masked whole, then cut: a cut first could leave the start of a credential, which the
|
|
192
|
+
# masking by value would no longer find.
|
|
193
|
+
def log_line(message, error, backtrace)
|
|
194
|
+
line = +"[Profiler] #{message}"
|
|
195
|
+
if error
|
|
196
|
+
line << ": #{error.class}"
|
|
197
|
+
text = error_text(error)
|
|
198
|
+
line << ": #{text}" unless text.empty?
|
|
199
|
+
line << "\n#{error.backtrace.join("\n")}" if backtrace && error.backtrace
|
|
200
|
+
end
|
|
201
|
+
Redaction.hide_credentials(line)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The error's message, masked, then cut at LOG_ERROR_MESSAGE_LIMIT bytes. A message that
|
|
205
|
+
# raises leaves the class alone.
|
|
206
|
+
def error_text(error)
|
|
207
|
+
text = Redaction.hide_credentials(error.message.to_s)
|
|
208
|
+
return text if text.bytesize <= LOG_ERROR_MESSAGE_LIMIT
|
|
209
|
+
|
|
210
|
+
"#{Redaction.cut_bytes(text, LOG_ERROR_MESSAGE_LIMIT).scrub("")}..."
|
|
211
|
+
rescue StandardError
|
|
212
|
+
""
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
public
|
|
216
|
+
|
|
97
217
|
# Dump a variable to the profiler
|
|
98
218
|
# Usage: Profiler.dump(variable, "optional label")
|
|
99
219
|
def dump(value, label = nil)
|
|
100
|
-
return unless enabled?
|
|
220
|
+
return value unless enabled?
|
|
101
221
|
|
|
102
222
|
# The slot exists only while a DumpCollector profiles this thread: outside of one, nobody
|
|
103
223
|
# would ever read the dump, and the thread would keep it for good.
|
|
@@ -122,6 +242,9 @@ module Profiler
|
|
|
122
242
|
end
|
|
123
243
|
end
|
|
124
244
|
|
|
245
|
+
@logged_once = Set.new
|
|
246
|
+
@logged_once_mutex = Mutex.new
|
|
247
|
+
|
|
125
248
|
self.function_profiling_enabled = true
|
|
126
249
|
self.function_profiling_max_frames = 2000
|
|
127
250
|
self.function_profiling_mode = "lite"
|