rails-profiler 0.36.2 → 0.38.0
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 +26 -0
- data/app/assets/builds/profiler-toolbar.js +1 -1
- data/app/assets/builds/profiler.js +10 -10
- data/app/controllers/profiler/api/profiles_controller.rb +10 -0
- data/app/controllers/profiler/health_controller.rb +27 -0
- data/app/views/profiler/health/show.html.erb +26 -0
- data/config/routes.rb +1 -0
- data/lib/profiler/collectors/circuit_breaker.rb +102 -0
- data/lib/profiler/collectors/lifecycle.rb +30 -5
- data/lib/profiler/configuration.rb +19 -1
- data/lib/profiler/console_profiler.rb +1 -3
- data/lib/profiler/current_context.rb +10 -0
- data/lib/profiler/instrumentation/net_http_instrumentation.rb +20 -0
- data/lib/profiler/job_profiler.rb +1 -3
- data/lib/profiler/middleware/profiler_middleware.rb +24 -12
- data/lib/profiler/middleware/streamed_profile.rb +9 -6
- data/lib/profiler/models/profile.rb +7 -1
- data/lib/profiler/storage/base_store.rb +5 -0
- data/lib/profiler/storage/file_store.rb +25 -6
- data/lib/profiler/storage/memory_store.rb +15 -2
- data/lib/profiler/storage/redis_store.rb +23 -0
- data/lib/profiler/storage/sqlite_store.rb +20 -3
- data/lib/profiler/test_profiler.rb +1 -3
- data/lib/profiler/trace_context.rb +48 -0
- data/lib/profiler/version.rb +1 -1
- metadata +6 -2
|
@@ -9,6 +9,8 @@ module Profiler
|
|
|
9
9
|
# dashboard relies on the default: http profiles, as summaries.
|
|
10
10
|
if params[:parent_token].present?
|
|
11
11
|
render_children_page
|
|
12
|
+
elsif params[:trace_id].present?
|
|
13
|
+
render_trace_page
|
|
12
14
|
elsif all_types?
|
|
13
15
|
render_profile_page(type: profile_type_param, summary: false)
|
|
14
16
|
else
|
|
@@ -57,6 +59,14 @@ module Profiler
|
|
|
57
59
|
params[:type].to_s.match?(/\A[a-z_]{1,32}\z/) ? params[:type].to_s : nil
|
|
58
60
|
end
|
|
59
61
|
|
|
62
|
+
# The profiles of a trace, oldest last, read through the store's trace index.
|
|
63
|
+
def render_trace_page
|
|
64
|
+
limit, offset = page_params
|
|
65
|
+
members = Profiler.storage.find_by_trace(params[:trace_id].to_s).reverse
|
|
66
|
+
members = members.select { |p| p.profile_type == "http" } unless all_types?
|
|
67
|
+
render_page(members.drop(offset).first(limit + 1), limit, offset)
|
|
68
|
+
end
|
|
69
|
+
|
|
60
70
|
# The children of a page, oldest last, read through the store's parent index.
|
|
61
71
|
def render_children_page
|
|
62
72
|
limit, offset = page_params
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
# GET /_profiler/health: the state of the circuit of each collector (see
|
|
5
|
+
# Profiler::Collectors::CircuitBreaker), as a page or, with a .json suffix or an Accept
|
|
6
|
+
# header, as JSON. Always 200: an open circuit means a collector is left out of the profiles,
|
|
7
|
+
# not that the profiler or the application is down, and a 503 would trip any probe pointed at it.
|
|
8
|
+
class HealthController < ApplicationController
|
|
9
|
+
def show
|
|
10
|
+
@enabled = Profiler.configuration.collector_circuit_breaker_enabled ? true : false
|
|
11
|
+
@threshold = Profiler.configuration.collector_circuit_breaker_threshold
|
|
12
|
+
@collectors = Profiler.configuration.collectors.map do |collector_class|
|
|
13
|
+
Collectors::CircuitBreaker.status(collector_class).merge(name: collector_class.name.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
respond_to do |format|
|
|
17
|
+
format.html
|
|
18
|
+
format.json do
|
|
19
|
+
render json: {
|
|
20
|
+
circuit_breaker: { enabled: @enabled, threshold: @threshold },
|
|
21
|
+
collectors: @collectors
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div style="max-width: 900px; margin: 24px auto; padding: 0 16px; font-family: system-ui, sans-serif;">
|
|
2
|
+
<p><a href="<%= profiler.root_path %>">← Profiler</a></p>
|
|
3
|
+
<h1>Collector health</h1>
|
|
4
|
+
<p>
|
|
5
|
+
Circuit breaker: <strong><%= @enabled ? "enabled" : "disabled" %></strong>,
|
|
6
|
+
opens after <strong><%= @threshold %></strong> consecutive failures.
|
|
7
|
+
</p>
|
|
8
|
+
<table style="border-collapse: collapse; width: 100%;">
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
<th style="text-align: left; border-bottom: 1px solid #888; padding: 6px;">Collector</th>
|
|
12
|
+
<th style="text-align: left; border-bottom: 1px solid #888; padding: 6px;">State</th>
|
|
13
|
+
<th style="text-align: right; border-bottom: 1px solid #888; padding: 6px;">Consecutive failures</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% @collectors.each do |collector| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td style="padding: 6px;"><%= collector[:name] %></td>
|
|
20
|
+
<td style="padding: 6px; font-weight: bold; color: <%= { closed: "#2e7d32", open: "#c62828", half_open: "#ef6c00" }[collector[:state]] %>;"><%= collector[:state] %></td>
|
|
21
|
+
<td style="padding: 6px; text-align: right;"><%= collector[:failures] %></td>
|
|
22
|
+
</tr>
|
|
23
|
+
<% end %>
|
|
24
|
+
</tbody>
|
|
25
|
+
</table>
|
|
26
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Collectors
|
|
5
|
+
# One circuit per collector class, shared by the threads and requests of the process. A
|
|
6
|
+
# class that fails config.collector_circuit_breaker_threshold times in a row (a subscribe or
|
|
7
|
+
# a collect that raises) is no longer instantiated: its circuit is open. After RECOVERY_SECONDS
|
|
8
|
+
# a single request tries it again (half open): a success closes the circuit, a failure
|
|
9
|
+
# opens it again for another RECOVERY_SECONDS. With the option off, nothing is counted and
|
|
10
|
+
# nothing is blocked.
|
|
11
|
+
module CircuitBreaker
|
|
12
|
+
RECOVERY_SECONDS = 60
|
|
13
|
+
|
|
14
|
+
# state: :closed, :open or :half_open. +since+ is when the circuit opened, or when the
|
|
15
|
+
# trial of a half open one was granted.
|
|
16
|
+
Circuit = Struct.new(:failures, :state, :since)
|
|
17
|
+
|
|
18
|
+
LOCK = Mutex.new
|
|
19
|
+
CIRCUITS = {}
|
|
20
|
+
private_constant :LOCK, :CIRCUITS
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def enabled?
|
|
25
|
+
Profiler.configuration.collector_circuit_breaker_enabled ? true : false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Whether a collector of this class is to be created for the next profile. An open circuit
|
|
29
|
+
# whose delay has passed answers true once, and becomes half open; a half open one answers
|
|
30
|
+
# false while its trial is under way, unless that trial never reported for RECOVERY_SECONDS.
|
|
31
|
+
def allow?(collector_class)
|
|
32
|
+
return true unless enabled?
|
|
33
|
+
|
|
34
|
+
LOCK.synchronize do
|
|
35
|
+
circuit = CIRCUITS[collector_class]
|
|
36
|
+
return true unless circuit
|
|
37
|
+
|
|
38
|
+
case circuit.state
|
|
39
|
+
when :closed then true
|
|
40
|
+
else
|
|
41
|
+
return false if now - circuit.since < RECOVERY_SECONDS
|
|
42
|
+
|
|
43
|
+
circuit.state = :half_open
|
|
44
|
+
circuit.since = now
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def record_failure(collector_class)
|
|
51
|
+
return unless enabled?
|
|
52
|
+
|
|
53
|
+
LOCK.synchronize do
|
|
54
|
+
circuit = (CIRCUITS[collector_class] ||= Circuit.new(0, :closed, nil))
|
|
55
|
+
circuit.failures += 1
|
|
56
|
+
if circuit.state == :half_open || circuit.failures >= Profiler.configuration.collector_circuit_breaker_threshold
|
|
57
|
+
circuit.state = :open
|
|
58
|
+
circuit.since = now
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def record_success(collector_class)
|
|
64
|
+
return unless enabled?
|
|
65
|
+
|
|
66
|
+
LOCK.synchronize do
|
|
67
|
+
circuit = CIRCUITS[collector_class]
|
|
68
|
+
return unless circuit
|
|
69
|
+
|
|
70
|
+
# A late success of a request that started before the circuit opened does not close it.
|
|
71
|
+
return if circuit.state == :open
|
|
72
|
+
|
|
73
|
+
CIRCUITS.delete(collector_class)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# { state:, failures: } for one class, without changing anything. An open circuit whose
|
|
78
|
+
# delay has passed is reported half open: the next request will try it.
|
|
79
|
+
def status(collector_class)
|
|
80
|
+
return { state: :closed, failures: 0 } unless enabled?
|
|
81
|
+
|
|
82
|
+
LOCK.synchronize do
|
|
83
|
+
circuit = CIRCUITS[collector_class]
|
|
84
|
+
return { state: :closed, failures: 0 } unless circuit
|
|
85
|
+
|
|
86
|
+
state = circuit.state
|
|
87
|
+
state = :half_open if state == :open && now - circuit.since >= RECOVERY_SECONDS
|
|
88
|
+
{ state: state, failures: circuit.failures }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def reset!
|
|
93
|
+
LOCK.synchronize { CIRCUITS.clear }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Monotonic seconds; a spec replaces it to move time.
|
|
97
|
+
def now
|
|
98
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "circuit_breaker"
|
|
4
|
+
|
|
3
5
|
module Profiler
|
|
4
6
|
module Collectors
|
|
5
7
|
# Subscribes and releases the collectors of one profile, for every entry point that runs
|
|
@@ -7,14 +9,26 @@ module Profiler
|
|
|
7
9
|
module Lifecycle
|
|
8
10
|
module_function
|
|
9
11
|
|
|
10
|
-
# Subscribes each collector in turn.
|
|
11
|
-
#
|
|
12
|
+
# Subscribes each collector in turn. A collector whose subscribe raises is released and
|
|
13
|
+
# taken out of +collectors+ (the array is changed in place, so that the caller only collects
|
|
14
|
+
# the ones that subscribed); the others keep profiling. Returns false, with every collector
|
|
15
|
+
# released, only when none could subscribe: the caller then runs the work unprofiled.
|
|
12
16
|
def subscribe_all(collectors, label)
|
|
13
|
-
|
|
17
|
+
return true if collectors.empty?
|
|
18
|
+
|
|
19
|
+
collectors.reject! { |collector| !subscribe_one(collector, label) }
|
|
20
|
+
!collectors.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Runs the block that collects from one collector. A failure is logged and counted against
|
|
24
|
+
# the collector's class (CircuitBreaker), a success clears that count; the others carry on.
|
|
25
|
+
def guard(collector, label)
|
|
26
|
+
yield
|
|
27
|
+
CircuitBreaker.record_success(collector.class)
|
|
14
28
|
true
|
|
15
29
|
rescue => e
|
|
16
|
-
Profiler.log_error("#{label}: collector
|
|
17
|
-
|
|
30
|
+
Profiler.log_error("#{label}: collector #{collector.class} failed", e)
|
|
31
|
+
CircuitBreaker.record_failure(collector.class)
|
|
18
32
|
false
|
|
19
33
|
end
|
|
20
34
|
|
|
@@ -26,6 +40,17 @@ module Profiler
|
|
|
26
40
|
Profiler.log_error("collector #{collector.class} release failed", e)
|
|
27
41
|
end
|
|
28
42
|
end
|
|
43
|
+
|
|
44
|
+
def subscribe_one(collector, label)
|
|
45
|
+
collector.subscribe if collector.respond_to?(:subscribe)
|
|
46
|
+
true
|
|
47
|
+
rescue => e
|
|
48
|
+
Profiler.log_error("#{label}: collector #{collector.class} subscribe failed", e)
|
|
49
|
+
CircuitBreaker.record_failure(collector.class)
|
|
50
|
+
release_all([collector])
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
private_class_method :subscribe_one
|
|
29
54
|
end
|
|
30
55
|
end
|
|
31
56
|
end
|
|
@@ -57,7 +57,8 @@ module Profiler
|
|
|
57
57
|
:name, :master_url, :self_url,
|
|
58
58
|
:cluster_heartbeat_interval, :cluster_offline_threshold,
|
|
59
59
|
:cluster_master, :cluster_secret, :cluster_require_secret,
|
|
60
|
-
:cluster_allow_insecure_http, :logger
|
|
60
|
+
:cluster_allow_insecure_http, :logger,
|
|
61
|
+
:collector_circuit_breaker_enabled
|
|
61
62
|
|
|
62
63
|
attr_reader :authorize_block, :enabled, :track_tests, :max_profiles
|
|
63
64
|
|
|
@@ -156,6 +157,23 @@ module Profiler
|
|
|
156
157
|
# Where the profiler's own errors go (Profiler.log): Rails.logger when nil, or $stderr when
|
|
157
158
|
# there is none.
|
|
158
159
|
@logger = nil
|
|
160
|
+
# A collector that fails this many times in a row is no longer run (see
|
|
161
|
+
# Profiler::Collectors::CircuitBreaker); false never stops one.
|
|
162
|
+
@collector_circuit_breaker_enabled = true
|
|
163
|
+
@collector_circuit_breaker_threshold = 3
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
attr_reader :collector_circuit_breaker_threshold
|
|
167
|
+
|
|
168
|
+
# A positive Integer. Anything else is left out with a warning, the previous value kept: the
|
|
169
|
+
# gem is loaded in production too, where a mistake here must not stop the application booting.
|
|
170
|
+
def collector_circuit_breaker_threshold=(value)
|
|
171
|
+
if value.is_a?(Integer) && value.positive?
|
|
172
|
+
@collector_circuit_breaker_threshold = value
|
|
173
|
+
else
|
|
174
|
+
Profiler.log_warn("config.collector_circuit_breaker_threshold: #{value.inspect} is not a positive " \
|
|
175
|
+
"Integer; keeping #{@collector_circuit_breaker_threshold}")
|
|
176
|
+
end
|
|
159
177
|
end
|
|
160
178
|
|
|
161
179
|
# Always a Pathname, whether set from a String or left to its default.
|
|
@@ -72,11 +72,9 @@ module Profiler
|
|
|
72
72
|
profile.finish(console_status == "completed" ? 200 : 500)
|
|
73
73
|
|
|
74
74
|
collectors.each do |collector|
|
|
75
|
-
|
|
75
|
+
Collectors::Lifecycle.guard(collector, "ConsoleProfiler") do
|
|
76
76
|
collector.collect if collector.respond_to?(:collect)
|
|
77
77
|
profile.add_collector_metadata(collector)
|
|
78
|
-
rescue => e
|
|
79
|
-
Profiler.log_error("ConsoleProfiler: collector #{collector.class} failed", e)
|
|
80
78
|
end
|
|
81
79
|
end
|
|
82
80
|
|
|
@@ -10,8 +10,18 @@ module Profiler
|
|
|
10
10
|
Thread.current[:profiler_token] = value
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# The trace id of the profile in progress, for the outgoing calls to carry.
|
|
14
|
+
def self.trace_id
|
|
15
|
+
Thread.current[:profiler_trace_id]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.trace_id=(value)
|
|
19
|
+
Thread.current[:profiler_trace_id] = value
|
|
20
|
+
end
|
|
21
|
+
|
|
13
22
|
def self.clear
|
|
14
23
|
Thread.current[:profiler_token] = nil
|
|
24
|
+
Thread.current[:profiler_trace_id] = nil
|
|
15
25
|
end
|
|
16
26
|
end
|
|
17
27
|
end
|
|
@@ -8,12 +8,18 @@ require "securerandom"
|
|
|
8
8
|
require_relative "../redaction"
|
|
9
9
|
require_relative "../body_content"
|
|
10
10
|
require_relative "../configuration"
|
|
11
|
+
require_relative "../current_context"
|
|
12
|
+
require_relative "../trace_context"
|
|
11
13
|
|
|
12
14
|
module Profiler
|
|
13
15
|
module Instrumentation
|
|
14
16
|
module NetHttpInstrumentation
|
|
15
17
|
module RequestPatch
|
|
16
18
|
def request(req, body = nil, &block)
|
|
19
|
+
# Before the guards below: the profiler's own calls (the master's requests to its
|
|
20
|
+
# slaves) carry the trace too, so that the slave's profile joins the request's trace.
|
|
21
|
+
NetHttpInstrumentation.inject_trace_context(req, address.to_s, port)
|
|
22
|
+
|
|
17
23
|
collector = Thread.current[:profiler_http_collector]
|
|
18
24
|
return super unless collector
|
|
19
25
|
# Re-entrancy guard: Net::HTTP#request calls itself recursively when
|
|
@@ -128,6 +134,20 @@ module Profiler
|
|
|
128
134
|
@installed = true
|
|
129
135
|
end
|
|
130
136
|
|
|
137
|
+
# Adds a traceparent to a call made while a profiled request runs on this thread: the trace
|
|
138
|
+
# id of that request and a span id of its own. A traceparent the application set is kept,
|
|
139
|
+
# and a host excluded from the instrumentation gets none.
|
|
140
|
+
def self.inject_trace_context(req, host, port)
|
|
141
|
+
trace_id = CurrentContext.trace_id
|
|
142
|
+
return unless trace_id
|
|
143
|
+
return if req[TraceContext::HEADER]
|
|
144
|
+
return if skip_host?(host, port)
|
|
145
|
+
|
|
146
|
+
req[TraceContext::HEADER] = TraceContext.format(trace_id, TraceContext.new_span_id)
|
|
147
|
+
rescue => e
|
|
148
|
+
Profiler.log_error("NetHttpInstrumentation: could not add the traceparent header", e)
|
|
149
|
+
end
|
|
150
|
+
|
|
131
151
|
# The hosts of http_skip_hosts, and the cluster's master (host and port): a slave's calls to
|
|
132
152
|
# it are the profiler's, whichever code makes them.
|
|
133
153
|
def self.skip_host?(host, port = nil)
|
|
@@ -102,11 +102,9 @@ module Profiler
|
|
|
102
102
|
profile.finish(job_status == "completed" ? 200 : 500)
|
|
103
103
|
|
|
104
104
|
collectors.each do |collector|
|
|
105
|
-
|
|
105
|
+
Collectors::Lifecycle.guard(collector, "JobProfiler") do
|
|
106
106
|
collector.collect if collector.respond_to?(:collect)
|
|
107
107
|
profile.add_collector_metadata(collector)
|
|
108
|
-
rescue => e
|
|
109
|
-
Profiler.log_error("JobProfiler: collector #{collector.class} failed", e)
|
|
110
108
|
end
|
|
111
109
|
end
|
|
112
110
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "rack"
|
|
4
4
|
require_relative "../models/profile"
|
|
5
5
|
require_relative "../current_context"
|
|
6
|
+
require_relative "../trace_context"
|
|
6
7
|
require_relative "../collectors/lifecycle"
|
|
7
8
|
require_relative "toolbar_injector"
|
|
8
9
|
require_relative "capturing_body"
|
|
@@ -31,6 +32,7 @@ module Profiler
|
|
|
31
32
|
profile = Models::Profile.new(build_request(env), params: false)
|
|
32
33
|
profile.gem_version = Profiler.version
|
|
33
34
|
Profiler::CurrentContext.token = profile.token
|
|
35
|
+
assign_trace(env, profile)
|
|
34
36
|
|
|
35
37
|
# Capture request body before app processes it
|
|
36
38
|
request_body = read_request_body(env)
|
|
@@ -76,6 +78,16 @@ module Profiler
|
|
|
76
78
|
|
|
77
79
|
private
|
|
78
80
|
|
|
81
|
+
# Continues the trace of a valid incoming traceparent (its parent-id becomes this profile's
|
|
82
|
+
# parent_span_id), or starts a new one. Either way the profile gets a span of its own.
|
|
83
|
+
def assign_trace(env, profile)
|
|
84
|
+
incoming = TraceContext.parse(env["HTTP_TRACEPARENT"])
|
|
85
|
+
profile.trace_id = incoming ? incoming.trace_id : TraceContext.new_trace_id
|
|
86
|
+
profile.parent_span_id = incoming&.parent_id
|
|
87
|
+
profile.span_id = TraceContext.new_span_id
|
|
88
|
+
Profiler::CurrentContext.trace_id = profile.trace_id
|
|
89
|
+
end
|
|
90
|
+
|
|
79
91
|
# Read after the application has run, so after the routing: the route parameters are there,
|
|
80
92
|
# and the params carry the encoding the route declared, as the controller saw them.
|
|
81
93
|
def capture_params(env, profile)
|
|
@@ -153,10 +165,10 @@ module Profiler
|
|
|
153
165
|
# A Rack 3 streaming body (call without each) writes to the socket itself: nothing to relay.
|
|
154
166
|
unless body.respond_to?(:each)
|
|
155
167
|
kept.each do |collector|
|
|
156
|
-
collector
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
Collectors::Lifecycle.guard(collector, "ProfilerMiddleware") do
|
|
169
|
+
collector.collect if collector.respond_to?(:collect)
|
|
170
|
+
profile.refresh_collector_metadata(collector)
|
|
171
|
+
end
|
|
160
172
|
end
|
|
161
173
|
record_response(env, profile, request_body, status, profiled_headers, "", nil, false)
|
|
162
174
|
# Saved before the headers leave: a profile that is not there gets no token.
|
|
@@ -199,10 +211,10 @@ module Profiler
|
|
|
199
211
|
next unless collector.is_a?(Collectors::RequestCollector) ||
|
|
200
212
|
(error && collector.respond_to?(:capture) && !collect_from_any_thread?(collector))
|
|
201
213
|
|
|
202
|
-
collector
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
214
|
+
Collectors::Lifecycle.guard(collector, "ProfilerMiddleware") do
|
|
215
|
+
collector.collect
|
|
216
|
+
profile.refresh_collector_metadata(collector)
|
|
217
|
+
end
|
|
206
218
|
end
|
|
207
219
|
|
|
208
220
|
Profiler.save_profile(profile, from: "ProfilerMiddleware")
|
|
@@ -261,11 +273,9 @@ module Profiler
|
|
|
261
273
|
|
|
262
274
|
def collect_all(profile, collectors)
|
|
263
275
|
collectors.each do |collector|
|
|
264
|
-
|
|
276
|
+
Collectors::Lifecycle.guard(collector, "ProfilerMiddleware") do
|
|
265
277
|
collector.collect if collector.respond_to?(:collect)
|
|
266
278
|
profile.add_collector_metadata(collector)
|
|
267
|
-
rescue => e
|
|
268
|
-
Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
|
|
269
279
|
end
|
|
270
280
|
end
|
|
271
281
|
end
|
|
@@ -303,7 +313,9 @@ module Profiler
|
|
|
303
313
|
# Builds the collectors without subscribing them: Lifecycle.subscribe_all does, so that
|
|
304
314
|
# a failure part way leaves every collector reachable for release.
|
|
305
315
|
def create_collectors(profile)
|
|
306
|
-
Profiler.configuration.collectors
|
|
316
|
+
Profiler.configuration.collectors
|
|
317
|
+
.select { |collector_class| Collectors::CircuitBreaker.allow?(collector_class) }
|
|
318
|
+
.map { |collector_class| collector_class.new(profile) }
|
|
307
319
|
end
|
|
308
320
|
|
|
309
321
|
def html_response?(headers)
|
|
@@ -82,10 +82,10 @@ module Profiler
|
|
|
82
82
|
@released = true
|
|
83
83
|
@profile.collectors_released_after_seconds = after_seconds if after_seconds
|
|
84
84
|
@collectors.each do |collector|
|
|
85
|
-
collector
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
Collectors::Lifecycle.guard(collector, "ProfilerMiddleware") do
|
|
86
|
+
collector.collect if collector.respond_to?(:collect)
|
|
87
|
+
@profile.refresh_collector_metadata(collector)
|
|
88
|
+
end
|
|
89
89
|
end
|
|
90
90
|
Collectors::Lifecycle.release_all(@collectors)
|
|
91
91
|
end
|
|
@@ -114,7 +114,9 @@ module Profiler
|
|
|
114
114
|
# once the collectors are released.
|
|
115
115
|
def enter
|
|
116
116
|
token = CurrentContext.token
|
|
117
|
+
trace_id = CurrentContext.trace_id
|
|
117
118
|
CurrentContext.token = @profile.token
|
|
119
|
+
CurrentContext.trace_id = @profile.trace_id
|
|
118
120
|
lent = @mutex.synchronize do
|
|
119
121
|
next [] if @released
|
|
120
122
|
|
|
@@ -122,13 +124,14 @@ module Profiler
|
|
|
122
124
|
[collector, collector.lend_thread_slots] if collector.respond_to?(:lend_thread_slots)
|
|
123
125
|
end
|
|
124
126
|
end
|
|
125
|
-
[token, lent]
|
|
127
|
+
[token, lent, trace_id]
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
def leave(state)
|
|
129
|
-
token, lent = state
|
|
131
|
+
token, lent, trace_id = state
|
|
130
132
|
lent.reverse_each { |collector, previous| collector.return_thread_slots(previous) }
|
|
131
133
|
CurrentContext.token = token
|
|
134
|
+
CurrentContext.trace_id = trace_id
|
|
132
135
|
end
|
|
133
136
|
end
|
|
134
137
|
end
|
|
@@ -21,7 +21,7 @@ module Profiler
|
|
|
21
21
|
:request_body_size, :request_body_truncated, :request_body_size_is_minimum,
|
|
22
22
|
:response_body_size, :response_body_truncated, :response_body_size_is_minimum,
|
|
23
23
|
:collectors_released_after_seconds, :params_truncated,
|
|
24
|
-
:gem_version
|
|
24
|
+
:gem_version, :trace_id, :span_id, :parent_span_id
|
|
25
25
|
|
|
26
26
|
# +params+: false leaves the params to capture_params, for a caller that builds the profile
|
|
27
27
|
# before the router has run.
|
|
@@ -167,6 +167,9 @@ module Profiler
|
|
|
167
167
|
collectors_data: @collectors_data,
|
|
168
168
|
tabs: @collectors_metadata,
|
|
169
169
|
parent_token: @parent_token,
|
|
170
|
+
trace_id: @trace_id,
|
|
171
|
+
span_id: @span_id,
|
|
172
|
+
parent_span_id: @parent_span_id,
|
|
170
173
|
is_ajax: @is_ajax
|
|
171
174
|
}
|
|
172
175
|
end
|
|
@@ -217,6 +220,9 @@ module Profiler
|
|
|
217
220
|
profile.is_ajax = data[:is_ajax] || false
|
|
218
221
|
profile.profile_type = data[:profile_type] || "http"
|
|
219
222
|
profile.gem_version = data[:gem_version]
|
|
223
|
+
profile.trace_id = data[:trace_id]
|
|
224
|
+
profile.span_id = data[:span_id]
|
|
225
|
+
profile.parent_span_id = data[:parent_span_id]
|
|
220
226
|
|
|
221
227
|
# Convert collectors_data keys to strings recursively for consistency
|
|
222
228
|
profile.collectors_data = (data[:collectors_data] || {}).transform_keys(&:to_s).transform_values do |value|
|
|
@@ -45,6 +45,11 @@ module Profiler
|
|
|
45
45
|
raise NotImplementedError, "#{self.class} must implement #find_by_parent"
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# The profiles sharing a trace id, oldest first.
|
|
49
|
+
def find_by_trace(trace_id)
|
|
50
|
+
raise NotImplementedError, "#{self.class} must implement #find_by_trace"
|
|
51
|
+
end
|
|
52
|
+
|
|
48
53
|
def delete(token)
|
|
49
54
|
raise NotImplementedError, "#{self.class} must implement #delete"
|
|
50
55
|
end
|
|
@@ -41,7 +41,7 @@ module Profiler
|
|
|
41
41
|
# Past a cap, the oldest profiles are evicted down to this share of it.
|
|
42
42
|
LOW_WATER = 0.8
|
|
43
43
|
|
|
44
|
-
Entry = Struct.new(:token, :at, :type, :parent, :bytes, :summary, :sequence)
|
|
44
|
+
Entry = Struct.new(:token, :at, :type, :parent, :bytes, :summary, :sequence, :trace)
|
|
45
45
|
|
|
46
46
|
def initialize(options = {})
|
|
47
47
|
super()
|
|
@@ -138,6 +138,16 @@ module Profiler
|
|
|
138
138
|
children.filter_map { |entry| load(entry.token) }.sort_by(&:started_at)
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
def find_by_trace(trace_id)
|
|
142
|
+
return [] unless Token.valid?(trace_id)
|
|
143
|
+
|
|
144
|
+
members = @mutex.synchronize do
|
|
145
|
+
refresh
|
|
146
|
+
(@traces[trace_id] || {}).keys.filter_map { |token| @entries[token] }
|
|
147
|
+
end
|
|
148
|
+
members.filter_map { |entry| load(entry.token) }.sort_by(&:started_at)
|
|
149
|
+
end
|
|
150
|
+
|
|
141
151
|
def delete(token)
|
|
142
152
|
return unless Token.valid?(token)
|
|
143
153
|
|
|
@@ -173,13 +183,14 @@ module Profiler
|
|
|
173
183
|
def put_record(token, data, bytes, started_at: nil)
|
|
174
184
|
started_at = started_at&.to_f || (data[:started_at] ? Time.parse(data[:started_at]).to_f : Time.now.to_f)
|
|
175
185
|
{ "op" => "put", "token" => token, "at" => started_at, "type" => (data[:profile_type] || "http").to_s,
|
|
176
|
-
"parent" => data[:parent_token], "bytes" => bytes, "summary" => Summary.build(data) }
|
|
186
|
+
"parent" => data[:parent_token], "trace" => data[:trace_id], "bytes" => bytes, "summary" => Summary.build(data) }
|
|
177
187
|
end
|
|
178
188
|
|
|
179
189
|
def forget_index
|
|
180
190
|
@entries = {}
|
|
181
191
|
@sequence = 0
|
|
182
192
|
@children = {}
|
|
193
|
+
@traces = {}
|
|
183
194
|
@bytes = 0
|
|
184
195
|
@records = 0
|
|
185
196
|
@offset = 0
|
|
@@ -243,8 +254,9 @@ module Profiler
|
|
|
243
254
|
when "put"
|
|
244
255
|
remove_entry(token)
|
|
245
256
|
parent = Token.valid?(record["parent"]) ? record["parent"] : nil
|
|
257
|
+
trace = Token.valid?(record["trace"]) ? record["trace"] : nil
|
|
246
258
|
add_entry(Entry.new(token, record["at"].to_f, record["type"], parent, record["bytes"].to_i,
|
|
247
|
-
record["summary"].is_a?(Hash) ? record["summary"] : nil))
|
|
259
|
+
record["summary"].is_a?(Hash) ? record["summary"] : nil, nil, trace))
|
|
248
260
|
when "del"
|
|
249
261
|
remove_entry(token)
|
|
250
262
|
end
|
|
@@ -255,6 +267,7 @@ module Profiler
|
|
|
255
267
|
@entries[entry.token] = entry
|
|
256
268
|
@bytes += entry.bytes
|
|
257
269
|
(@children[entry.parent] ||= {})[entry.token] = true if entry.parent
|
|
270
|
+
(@traces[entry.trace] ||= {})[entry.token] = true if entry.trace
|
|
258
271
|
@sorted = nil
|
|
259
272
|
end
|
|
260
273
|
|
|
@@ -267,6 +280,10 @@ module Profiler
|
|
|
267
280
|
siblings.delete(token)
|
|
268
281
|
@children.delete(entry.parent) if siblings.empty?
|
|
269
282
|
end
|
|
283
|
+
if entry.trace && (members = @traces[entry.trace])
|
|
284
|
+
members.delete(token)
|
|
285
|
+
@traces.delete(entry.trace) if members.empty?
|
|
286
|
+
end
|
|
270
287
|
@sorted = nil
|
|
271
288
|
end
|
|
272
289
|
|
|
@@ -344,7 +361,8 @@ module Profiler
|
|
|
344
361
|
def entry_from_file(token)
|
|
345
362
|
json = PrivateFiles.open_for_reading(profile_file_path(token), &:read)
|
|
346
363
|
record = put_record(token, Models::Profile.from_json(json).to_h, json.bytesize)
|
|
347
|
-
|
|
364
|
+
trace = Token.valid?(record["trace"]) ? record["trace"] : nil
|
|
365
|
+
Entry.new(token, record["at"], record["type"], record["parent"], record["bytes"], record["summary"], nil, trace)
|
|
348
366
|
rescue StandardError => e
|
|
349
367
|
# Unreadable: kept in the count, so that it is evicted in turn; listed by no one.
|
|
350
368
|
Profiler.log_error_once(:file_store_entry, "FileStore: could not read profile #{token} to index it", e)
|
|
@@ -353,7 +371,7 @@ module Profiler
|
|
|
353
371
|
rescue SystemCallError
|
|
354
372
|
nil
|
|
355
373
|
end
|
|
356
|
-
Entry.new(token, stat ? stat.mtime.to_f : 0.0, nil, nil, stat ? stat.size : 0, nil)
|
|
374
|
+
Entry.new(token, stat ? stat.mtime.to_f : 0.0, nil, nil, stat ? stat.size : 0, nil, nil, nil)
|
|
357
375
|
end
|
|
358
376
|
|
|
359
377
|
def modified_at(token)
|
|
@@ -390,7 +408,8 @@ module Profiler
|
|
|
390
408
|
"compacted_at" => Time.now.to_f)]
|
|
391
409
|
@entries.each_value do |entry|
|
|
392
410
|
lines << JSON.generate("op" => "put", "token" => entry.token, "at" => entry.at, "type" => entry.type,
|
|
393
|
-
"parent" => entry.parent, "
|
|
411
|
+
"parent" => entry.parent, "trace" => entry.trace, "bytes" => entry.bytes,
|
|
412
|
+
"summary" => entry.summary)
|
|
394
413
|
end
|
|
395
414
|
data = "#{lines.join("\n")}\n"
|
|
396
415
|
PrivateFiles.write(@index_path, data)
|