closeyourit-ruby 0.9.4 → 0.10.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/README.md +6 -0
- data/lib/closeyourit/background_worker.rb +28 -9
- data/lib/closeyourit/breadcrumb.rb +2 -2
- data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
- data/lib/closeyourit/client.rb +42 -29
- data/lib/closeyourit/configuration.rb +105 -101
- data/lib/closeyourit/event.rb +6 -6
- data/lib/closeyourit/events/error_event.rb +16 -16
- data/lib/closeyourit/events/job_metric_event.rb +8 -8
- data/lib/closeyourit/events/log_event.rb +17 -17
- data/lib/closeyourit/events/message_event.rb +4 -4
- data/lib/closeyourit/events/performance_issue_event.rb +4 -4
- data/lib/closeyourit/events/slow_method_event.rb +6 -6
- data/lib/closeyourit/events/slow_query_event.rb +4 -4
- data/lib/closeyourit/instrumenter.rb +12 -4
- data/lib/closeyourit/line_cache.rb +4 -4
- data/lib/closeyourit/log_buffer.rb +10 -10
- data/lib/closeyourit/log_device.rb +18 -18
- data/lib/closeyourit/monitor.rb +2 -2
- data/lib/closeyourit/performance/request_profile.rb +5 -5
- data/lib/closeyourit/performance/rollup.rb +3 -3
- data/lib/closeyourit/rails/active_job_extension.rb +33 -31
- data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
- data/lib/closeyourit/rails/error_subscriber.rb +4 -4
- data/lib/closeyourit/rails/log_broadcast.rb +12 -12
- data/lib/closeyourit/rails/net_http_patch.rb +22 -22
- data/lib/closeyourit/rails/query_source.rb +3 -3
- data/lib/closeyourit/rails/railtie.rb +32 -52
- data/lib/closeyourit/rails/request_body.rb +7 -7
- data/lib/closeyourit/rails/request_context.rb +27 -27
- data/lib/closeyourit/scope.rb +29 -29
- data/lib/closeyourit/scrubber.rb +47 -48
- data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
- data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +10 -11
- data/lib/closeyourit/stats.rb +8 -8
- data/lib/closeyourit/subscribers/job_performance.rb +30 -19
- data/lib/closeyourit/subscribers/request_performance.rb +4 -4
- data/lib/closeyourit/subscribers/slow_query.rb +42 -20
- data/lib/closeyourit/trace_context.rb +22 -22
- data/lib/closeyourit/transport.rb +25 -20
- data/lib/closeyourit/usage_registry.rb +17 -16
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +125 -125
- metadata +1 -1
|
@@ -5,15 +5,15 @@ require_relative "../scope"
|
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
7
|
module Rails
|
|
8
|
-
# Prepended
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# Prepended to Net::HTTP: times every external call and pushes it into the Scope's RequestProfile,
|
|
9
|
+
# so the request window can detect slow external HTTP. Transparent (returns the original response)
|
|
10
|
+
# and defensive (no-op when telemetry is off; never raises because of profiling). Excludes calls to
|
|
11
|
+
# the CloseYourIt endpoint itself (no loop).
|
|
12
12
|
module NetHTTPPatch
|
|
13
13
|
def request(req, body = nil, &block)
|
|
14
14
|
config = CloseYourIt.configuration
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# W3C propagation: independent of profiling (it has its own opt-in), must happen BEFORE the
|
|
16
|
+
# round-trip because it adds headers to the outgoing request.
|
|
17
17
|
inject_trace_context(config, req)
|
|
18
18
|
return super unless config.detect_performance_issues && config.capture_external_http
|
|
19
19
|
|
|
@@ -28,12 +28,12 @@ module CloseYourIt
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# baggage
|
|
31
|
+
# Handles the W3C trace headers on the outgoing request (only with opt-in propagation ON).
|
|
32
|
+
# To an allowed destination it injects traceparent/tracestate; to any other it REMOVES them.
|
|
33
|
+
# Removal is the defense against leaks on cross-host redirects: if the same request object is
|
|
34
|
+
# reused to follow a redirect to a host not in the allowlist, the previous call's headers must not
|
|
35
|
+
# survive ("external hosts do not receive internal headers" — CYRB-15).
|
|
36
|
+
# baggage is never read nor emitted. Defensive: never raises because of propagation.
|
|
37
37
|
def inject_trace_context(config, req)
|
|
38
38
|
return unless config.propagate_trace_context
|
|
39
39
|
|
|
@@ -47,9 +47,9 @@ module CloseYourIt
|
|
|
47
47
|
nil
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
50
|
+
# The trace context to deliver to THIS destination, or nil if nothing must be propagated: missing
|
|
51
|
+
# host, the CloseYourIt endpoint itself (no self-propagation), destination outside the allowlist,
|
|
52
|
+
# or a scope without context.
|
|
53
53
|
def deliverable_context(config)
|
|
54
54
|
host = address
|
|
55
55
|
return nil if host.nil? || own_endpoint?(config, host)
|
|
@@ -58,16 +58,16 @@ module CloseYourIt
|
|
|
58
58
|
CloseYourIt::Scope.current.trace_context
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
# Regexp = pattern (
|
|
61
|
+
# Is the destination allowed to receive the trace context? String = exact host (case-insensitive),
|
|
62
|
+
# Regexp = pattern (subdomains/families). Empty list → always false (no destination).
|
|
63
63
|
def destination_allowed?(config, host)
|
|
64
64
|
config.trace_propagation_allowlist.any? do |pattern|
|
|
65
65
|
pattern.is_a?(Regexp) ? pattern.match?(host) : pattern.to_s.casecmp?(host)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
#
|
|
70
|
-
#
|
|
69
|
+
# Removes the W3C trace headers a previous call on the same request object may have left.
|
|
70
|
+
# Only our headers, nothing else; no-op if the request does not support them.
|
|
71
71
|
def strip_trace_headers(req)
|
|
72
72
|
return unless req.respond_to?(:delete)
|
|
73
73
|
|
|
@@ -83,7 +83,7 @@ module CloseYourIt
|
|
|
83
83
|
host: host, path: templatize_path(req), duration_ms: duration_ms
|
|
84
84
|
)
|
|
85
85
|
rescue StandardError
|
|
86
|
-
#
|
|
86
|
+
# Profiling must never disturb the host call.
|
|
87
87
|
nil
|
|
88
88
|
end
|
|
89
89
|
|
|
@@ -96,8 +96,8 @@ module CloseYourIt
|
|
|
96
96
|
false
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# Path
|
|
100
|
-
# signature).
|
|
99
|
+
# Path without query string, with uuids and runs of ≥3 digits → placeholders (same route = same
|
|
100
|
+
# signature). The ≥3 digit threshold keeps API versions like "/v1" and templatizes real ids
|
|
101
101
|
# ("/v1/charges/ch_12345" → "/v1/charges/ch_<n>", "/users/123" → "/users/<n>").
|
|
102
102
|
def templatize_path(req)
|
|
103
103
|
path = req.respond_to?(:path) ? req.path.to_s : ""
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
4
|
module Rails
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Application call site of a slow query (privacy-safe → always sent): first backtrace frame
|
|
6
|
+
# cleaned by Rails.backtrace_cleaner (removes gems/framework, keeps app code), without the
|
|
7
|
+
# ":in '...'" suffix. E.g. "app/models/order.rb:42".
|
|
8
8
|
module QuerySource
|
|
9
9
|
def self.from_caller(backtrace = caller)
|
|
10
10
|
frame = ::Rails.backtrace_cleaner.clean(backtrace).first
|
|
@@ -15,13 +15,13 @@ require_relative "../sidekiq/job_metrics_middleware"
|
|
|
15
15
|
|
|
16
16
|
module CloseYourIt
|
|
17
17
|
module Rails
|
|
18
|
-
#
|
|
19
|
-
#
|
|
18
|
+
# Hooks the client into Rails: exception-capturing Rack middleware +
|
|
19
|
+
# `sql.active_record` subscriber for slow queries.
|
|
20
20
|
class Railtie < ::Rails::Railtie
|
|
21
21
|
initializer "closeyourit.use_rack_middleware" do |app|
|
|
22
22
|
app.config.middleware.use CloseYourIt::Rails::CaptureExceptions
|
|
23
|
-
# RequestContext
|
|
24
|
-
#
|
|
23
|
+
# RequestContext must WRAP CaptureExceptions: the scope must already be populated
|
|
24
|
+
# when the exception bubbles up to CaptureExceptions.
|
|
25
25
|
app.config.middleware.insert_before(
|
|
26
26
|
CloseYourIt::Rails::CaptureExceptions,
|
|
27
27
|
CloseYourIt::Rails::RequestContext
|
|
@@ -33,43 +33,22 @@ module CloseYourIt
|
|
|
33
33
|
|
|
34
34
|
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
35
35
|
event = ActiveSupport::Notifications::Event.new(*args)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
sql: event.payload[:sql],
|
|
40
|
-
cached: event.payload.fetch(:cached, false),
|
|
41
|
-
connection: event.payload[:connection],
|
|
42
|
-
binds: event.payload[:binds],
|
|
43
|
-
type_casted_binds: event.payload[:type_casted_binds],
|
|
44
|
-
source: CloseYourIt::Rails::QuerySource.from_caller
|
|
45
|
-
)
|
|
46
|
-
subscriber.breadcrumb(
|
|
47
|
-
name: event.payload[:name],
|
|
48
|
-
sql: event.payload[:sql],
|
|
49
|
-
duration_ms: event.duration,
|
|
50
|
-
cached: event.payload.fetch(:cached, false)
|
|
51
|
-
)
|
|
52
|
-
# Accumula la query nel profilo per-richiesta (detection N+1 a fine richiesta).
|
|
53
|
-
subscriber.profile(
|
|
54
|
-
name: event.payload[:name],
|
|
55
|
-
sql: event.payload[:sql],
|
|
56
|
-
duration_ms: event.duration,
|
|
57
|
-
cached: event.payload.fetch(:cached, false),
|
|
58
|
-
source: CloseYourIt::Rails::QuerySource.from_caller
|
|
59
|
-
)
|
|
36
|
+
# Slow query, breadcrumb and per-request profile (N+1 detection). The call site is lazy:
|
|
37
|
+
# a backtrace for every query was the SDK's biggest cost on the host thread.
|
|
38
|
+
subscriber.notify(event.payload, event.duration, -> { CloseYourIt::Rails::QuerySource.from_caller })
|
|
60
39
|
end
|
|
61
40
|
end
|
|
62
41
|
|
|
63
|
-
#
|
|
64
|
-
# request,
|
|
42
|
+
# At the end of the request: turns the collected profile into performance_issue verdicts (N+1,
|
|
43
|
+
# slow request, slow external HTTP). The subscriber is a no-op when detect_performance_issues is OFF.
|
|
65
44
|
initializer "closeyourit.subscribe_request_performance" do
|
|
66
45
|
perf = CloseYourIt::Subscribers::RequestPerformance.new
|
|
67
46
|
|
|
68
47
|
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
|
|
69
48
|
event = ActiveSupport::Notifications::Event.new(*args)
|
|
70
49
|
payload = event.payload
|
|
71
|
-
# CYSK-29 —
|
|
72
|
-
#
|
|
50
|
+
# CYSK-29 — usage telemetry: the route is `Controller#action`, NEVER the URL. The registry does
|
|
51
|
+
# one lookup and one increment; the usage_enabled gate lives inside #record.
|
|
73
52
|
CloseYourIt.usage_registry.record("route", "#{payload[:controller]}##{payload[:action]}") if CloseYourIt.enabled?
|
|
74
53
|
perf.record(
|
|
75
54
|
route: "#{payload[:controller]}##{payload[:action]}",
|
|
@@ -78,24 +57,25 @@ module CloseYourIt
|
|
|
78
57
|
end
|
|
79
58
|
end
|
|
80
59
|
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# opt-
|
|
60
|
+
# Instruments Net::HTTP: detects slow external calls (only with detect_performance_issues) and
|
|
61
|
+
# propagates the W3C trace context (only with propagate_trace_context). Unconditional prepend —
|
|
62
|
+
# the two opt-ins are evaluated per call in the patch; with both OFF it is in effect a no-op (calls super).
|
|
84
63
|
initializer "closeyourit.instrument_net_http" do
|
|
85
64
|
require "net/http"
|
|
86
65
|
::Net::HTTP.prepend(CloseYourIt::Rails::NetHTTPPatch) unless ::Net::HTTP.ancestors.include?(CloseYourIt::Rails::NetHTTPPatch)
|
|
87
66
|
end
|
|
88
67
|
|
|
89
|
-
#
|
|
68
|
+
# Captures ActiveJob/Solid Queue errors (around_perform).
|
|
90
69
|
initializer "closeyourit.active_job" do
|
|
91
70
|
ActiveSupport.on_load(:active_job) do
|
|
92
71
|
include CloseYourIt::Rails::ActiveJobExtension
|
|
93
72
|
end
|
|
94
73
|
end
|
|
95
74
|
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
75
|
+
# Measures duration and queue wait of ActiveJob jobs via ActiveSupport notifications:
|
|
76
|
+
# `perform_start` gives the wait (now - enqueued_at) as soon as the job starts, `perform` gives
|
|
77
|
+
# the execution duration at the end. Beyond threshold → slow_job / job_queue_latency metrics.
|
|
78
|
+
# No-op when monitor_jobs is OFF.
|
|
99
79
|
initializer "closeyourit.subscribe_active_job_performance" do
|
|
100
80
|
jobs = CloseYourIt::Subscribers::JobPerformance.new
|
|
101
81
|
|
|
@@ -107,28 +87,28 @@ module CloseYourIt
|
|
|
107
87
|
ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
|
|
108
88
|
event = ActiveSupport::Notifications::Event.new(*args)
|
|
109
89
|
job = event.payload[:job]
|
|
110
|
-
# CYSK-29 —
|
|
90
|
+
# CYSK-29 — jobs also declare they ran: kind `job`, symbol = the class.
|
|
111
91
|
CloseYourIt.usage_registry.record("job", job.class.name) if job && CloseYourIt.enabled?
|
|
112
92
|
jobs.active_job_performed(job, event.duration) if job
|
|
113
93
|
end
|
|
114
94
|
end
|
|
115
95
|
|
|
116
|
-
#
|
|
96
|
+
# Captures HANDLED errors reported via Rails.error.report (Rails 7+).
|
|
117
97
|
initializer "closeyourit.error_reporter" do
|
|
118
98
|
if ::Rails.respond_to?(:error) && ::Rails.error.respond_to?(:subscribe)
|
|
119
99
|
::Rails.error.subscribe(CloseYourIt::Rails::ErrorSubscriber.new)
|
|
120
100
|
end
|
|
121
101
|
end
|
|
122
102
|
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
# (Started GET, Rendered, ... —
|
|
103
|
+
# Opt-in Rails.logger → CloseYourIt.log broadcast (config.capture_rails_logs, default OFF).
|
|
104
|
+
# Sends only app logs ≥ capture_rails_logs_min_level: a DEDICATED threshold (default :warn),
|
|
105
|
+
# separate from logs_min_level, so the broadcast does not flood the stream with framework info
|
|
106
|
+
# noise (Started GET, Rendered, ... — one line per request under high traffic, CYRB-7). Requires
|
|
127
107
|
# BroadcastLogger (Rails 7.1+).
|
|
128
|
-
# `after: :load_config_initializers`: config.capture_rails_logs
|
|
129
|
-
# config/initializers/closeyourit.rb (CloseYourIt.init),
|
|
130
|
-
#
|
|
131
|
-
#
|
|
108
|
+
# `after: :load_config_initializers`: config.capture_rails_logs is set in
|
|
109
|
+
# config/initializers/closeyourit.rb (CloseYourIt.init), which runs AFTER the railtie
|
|
110
|
+
# initializers. Without this `after:` the check would read the default (false) and the broadcast
|
|
111
|
+
# would never be attached → the app logs would never reach CloseYourIt.
|
|
132
112
|
initializer "closeyourit.capture_rails_logs", after: :load_config_initializers do
|
|
133
113
|
config = CloseYourIt.configuration
|
|
134
114
|
if config.capture_rails_logs && ::Rails.logger.respond_to?(:broadcast_to)
|
|
@@ -136,9 +116,9 @@ module CloseYourIt
|
|
|
136
116
|
end
|
|
137
117
|
end
|
|
138
118
|
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
# in JobPerformance#record).
|
|
119
|
+
# Captures Sidekiq job errors + measures duration/wait via server middleware (only when Sidekiq
|
|
120
|
+
# is present). The middleware is effectively a no-op when monitor_jobs is OFF (the decision
|
|
121
|
+
# lives in JobPerformance#record).
|
|
142
122
|
initializer "closeyourit.sidekiq" do
|
|
143
123
|
if defined?(::Sidekiq) && ::Sidekiq.respond_to?(:configure_server)
|
|
144
124
|
::Sidekiq.configure_server do |sidekiq_config|
|
|
@@ -6,11 +6,11 @@ require_relative "../scrubber"
|
|
|
6
6
|
|
|
7
7
|
module CloseYourIt
|
|
8
8
|
module Rails
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
9
|
+
# Extracts the request body params (`request.data`) at EVENT time — never eagerly on every
|
|
10
|
+
# request (zero overhead on the happy path). Prefers the params already parsed by Rails/Rack in
|
|
11
|
+
# env; falls back to re-reading rack.input (with rewind) only for JSON/form ≤ MAX_BODY_BYTES.
|
|
12
|
+
# Sanitized output (uploads → "[FILE: …]", objects → "[OBJECT: …]", truncated strings) and
|
|
13
|
+
# scrubbed with the same denylist as the rest of the client.
|
|
14
14
|
module RequestBody
|
|
15
15
|
MAX_BODY_BYTES = 65_536
|
|
16
16
|
MAX_DEPTH = 8
|
|
@@ -31,7 +31,7 @@ module CloseYourIt
|
|
|
31
31
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
|
-
# Params
|
|
34
|
+
# Params already parsed upstream (ActionDispatch or Rack): no body re-read.
|
|
35
35
|
def parsed_params(env)
|
|
36
36
|
env["action_dispatch.request.request_parameters"] || env["rack.request.form_hash"]
|
|
37
37
|
end
|
|
@@ -45,7 +45,7 @@ module CloseYourIt
|
|
|
45
45
|
value = JSON.parse(raw)
|
|
46
46
|
value.is_a?(Hash) ? value : { "_json" => value }
|
|
47
47
|
when FORM_TYPE
|
|
48
|
-
#
|
|
48
|
+
# Flat fallback (stdlib): the real path with nesting goes through the already parsed params.
|
|
49
49
|
URI.decode_www_form(raw).to_h
|
|
50
50
|
end
|
|
51
51
|
rescue JSON::ParserError, ArgumentError
|
|
@@ -5,16 +5,16 @@ require_relative "../trace_context"
|
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
7
|
module Rails
|
|
8
|
-
# Rack middleware:
|
|
9
|
-
#
|
|
10
|
-
# `CaptureExceptions` (insert_before)
|
|
11
|
-
# Rack
|
|
8
|
+
# Rack middleware: populates the Scope with the request's HTTP context (method/url/headers)
|
|
9
|
+
# so the error event captured downstream knows "on which page" it happened. It must wrap
|
|
10
|
+
# `CaptureExceptions` (insert_before) to be already populated when the exception bubbles up.
|
|
11
|
+
# Pure Rack (reads `env`, no Rails dependency) → testable in isolation.
|
|
12
12
|
class RequestContext
|
|
13
|
-
#
|
|
13
|
+
# Headers without the HTTP_ prefix in the Rack env.
|
|
14
14
|
RAW_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# Correlation cookie written by the browser SDK (session replay): opaque id of the
|
|
17
|
+
# replay session, carried by every same-origin request.
|
|
18
18
|
REPLAY_COOKIE = "cyi_replay"
|
|
19
19
|
|
|
20
20
|
def initialize(app)
|
|
@@ -23,19 +23,19 @@ module CloseYourIt
|
|
|
23
23
|
|
|
24
24
|
def call(env)
|
|
25
25
|
if enabled?
|
|
26
|
-
#
|
|
26
|
+
# W3C trace context (only with opt-in propagation): adopted from the incoming header or generated.
|
|
27
27
|
context = trace_context_for(env)
|
|
28
28
|
CloseYourIt::Scope.current.trace_context = context
|
|
29
|
-
# trace_id
|
|
30
|
-
#
|
|
31
|
-
#
|
|
29
|
+
# trace_id always (log↔error correlation), even with capture_request OFF. With a W3C trace
|
|
30
|
+
# context the events' trace_id IS the W3C trace-id → the error/metric lines up with the
|
|
31
|
+
# distributed trace propagated downstream (trace_id mapping, CYRB-15); otherwise request_id as before.
|
|
32
32
|
CloseYourIt::Scope.current.trace_id = context ? context.trace_id : trace_id_for(env)
|
|
33
|
-
#
|
|
34
|
-
# → contexts.replay.replay_id
|
|
33
|
+
# Server error ↔ session replay correlation: the id from the cookie lands on the scope
|
|
34
|
+
# → the event's contexts.replay.replay_id (same spot as the JS path).
|
|
35
35
|
CloseYourIt::Scope.current.replay_session_id = replay_session_id_for(env)
|
|
36
36
|
if CloseYourIt.configuration.capture_request
|
|
37
37
|
CloseYourIt::Scope.current.request = build_request(env)
|
|
38
|
-
#
|
|
38
|
+
# Reference to the env for LAZY body extraction (request.data) once the event is built.
|
|
39
39
|
CloseYourIt::Scope.current.rack_env = env
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -52,10 +52,10 @@ module CloseYourIt
|
|
|
52
52
|
false
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
55
|
+
# W3C trace context of the request, ONLY with opt-in propagation on (otherwise nil → historical
|
|
56
|
+
# behavior unchanged). Adopts a valid incoming traceparent/tracestate (distributed correlation),
|
|
57
|
+
# otherwise generates a root: so services that ORIGINATE traffic also start with a W3C trace-id
|
|
58
|
+
# that can be propagated downstream. A malformed traceparent is ignored → a root is generated.
|
|
59
59
|
def trace_context_for(env)
|
|
60
60
|
return nil unless CloseYourIt.configuration.propagate_trace_context
|
|
61
61
|
|
|
@@ -63,28 +63,28 @@ module CloseYourIt
|
|
|
63
63
|
CloseYourIt::TraceContext.generate
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
66
|
+
# Reuses the Rails/Rack request id when present (same correlation as the application logs),
|
|
67
|
+
# otherwise generates one.
|
|
68
68
|
def trace_id_for(env)
|
|
69
69
|
env["action_dispatch.request_id"] ||
|
|
70
70
|
env["HTTP_X_REQUEST_ID"].to_s.split(",").first&.strip.then { |id| id&.empty? ? nil : id } ||
|
|
71
71
|
SecureRandom.uuid
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
#
|
|
74
|
+
# Extracts `cyi_replay` from the request cookie (nil if absent). Pure Rack.
|
|
75
75
|
def replay_session_id_for(env)
|
|
76
76
|
cookie = env["HTTP_COOKIE"]
|
|
77
77
|
return nil if cookie.nil? || cookie.empty?
|
|
78
78
|
|
|
79
|
-
# `rack/utils`
|
|
80
|
-
#
|
|
81
|
-
#
|
|
79
|
+
# `rack/utils` loaded lazily: the middleware runs only inside a Rack app (where Rack is surely
|
|
80
|
+
# present), so `require "closeyourit-ruby"` does not force Rack on non-web apps (CLI/workers)
|
|
81
|
+
# and the gem stays installable without declaring `rack` as a dependency (CYRB-13).
|
|
82
82
|
require "rack/utils"
|
|
83
83
|
Rack::Utils.parse_cookies_header(cookie)[REPLAY_COOKIE].to_s.then { |id| id.empty? ? nil : id }
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
#
|
|
87
|
-
# Authorization/Cookie). query_string + IP
|
|
86
|
+
# Sentry `request` shape. URL without query string; headers only from the allowlist (never
|
|
87
|
+
# Authorization/Cookie). query_string + IP only with send_pii (opt-in).
|
|
88
88
|
def build_request(env)
|
|
89
89
|
request = {
|
|
90
90
|
"method" => env["REQUEST_METHOD"],
|
|
@@ -101,7 +101,7 @@ module CloseYourIt
|
|
|
101
101
|
|
|
102
102
|
request
|
|
103
103
|
rescue StandardError
|
|
104
|
-
#
|
|
104
|
+
# Telemetry must never disturb the host request.
|
|
105
105
|
nil
|
|
106
106
|
end
|
|
107
107
|
|
data/lib/closeyourit/scope.rb
CHANGED
|
@@ -4,29 +4,29 @@ require_relative "breadcrumb_buffer"
|
|
|
4
4
|
require_relative "performance/request_profile"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
#
|
|
8
|
-
# user/tags/extra/contexts/request.
|
|
9
|
-
# →
|
|
7
|
+
# Per-request (or per-job) context isolated per execution context (Fiber storage):
|
|
8
|
+
# user/tags/extra/contexts/request. Read by ErrorEvent#to_h on the calling thread (synchronous)
|
|
9
|
+
# → the send worker never sees it and the scope does not leak between requests.
|
|
10
10
|
class Scope
|
|
11
11
|
STORAGE_KEY = :__closeyourit_scope
|
|
12
12
|
|
|
13
13
|
class << self
|
|
14
|
-
# Scope
|
|
15
|
-
#
|
|
16
|
-
# `Thread.current` (thread-local
|
|
14
|
+
# Scope of the current execution context. Uses `ActiveSupport::IsolatedExecutionState` when
|
|
15
|
+
# present (honors isolation_level: thread on Puma, fiber on Falcon), otherwise
|
|
16
|
+
# `Thread.current` (pure thread-local, NOT inherited by child threads → no bleed).
|
|
17
17
|
def current
|
|
18
18
|
store[STORAGE_KEY] ||= new
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# Reinstalls a previously saved scope. Used by `after_discard` (ActiveJob 7.1+) to get back the
|
|
22
|
+
# scope enriched during `perform` — tags/contexts/breadcrumbs, queries included — after the
|
|
23
|
+
# end-of-`perform` reset detached it from storage (CYRB-19). Symmetric to `reset!`.
|
|
24
24
|
def current=(scope)
|
|
25
25
|
store[STORAGE_KEY] = scope
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
# thread
|
|
28
|
+
# Clears the current scope — called in `ensure` by middleware and jobs (on Puma the
|
|
29
|
+
# thread is reused: without a reset the scope would leak into the next request).
|
|
30
30
|
def reset!
|
|
31
31
|
store[STORAGE_KEY] = nil
|
|
32
32
|
end
|
|
@@ -73,8 +73,8 @@ module CloseYourIt
|
|
|
73
73
|
@breadcrumbs.add(breadcrumb)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
76
|
+
# Per-request performance profile (queries + external HTTP). Lazy: created on first access,
|
|
77
|
+
# reset by #clear at the end of the request. Subscribers::RequestPerformance computes the verdict.
|
|
78
78
|
def performance_profile
|
|
79
79
|
@performance_profile ||= Performance::RequestProfile.new
|
|
80
80
|
end
|
|
@@ -88,17 +88,17 @@ module CloseYourIt
|
|
|
88
88
|
@rack_env = nil
|
|
89
89
|
@trace_id = nil
|
|
90
90
|
@replay_session_id = nil
|
|
91
|
-
#
|
|
92
|
-
#
|
|
91
|
+
# W3C trace context of the request (CloseYourIt::TraceContext): populated only when opt-in
|
|
92
|
+
# propagation is on, consumed by the Net::HTTP patch for outgoing headers.
|
|
93
93
|
@trace_context = nil
|
|
94
94
|
@breadcrumbs = BreadcrumbBuffer.new(CloseYourIt.configuration.max_breadcrumbs)
|
|
95
95
|
@performance_profile = nil
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
98
|
+
# Non-empty subset in Sentry event shape (user/tags/extra/contexts/request), merged into the
|
|
99
|
+
# payload by ErrorEvent#to_h. tags/extra/contexts go through the Scrubber (recursive denylist by
|
|
100
|
+
# key): the backend does NOT scrub them again (Errors::Ingest::Normalize keeps them verbatim),
|
|
101
|
+
# so this is the only safety net against sensitive keys there — R2.
|
|
102
102
|
def to_event_hash
|
|
103
103
|
{
|
|
104
104
|
"user" => serialize_user,
|
|
@@ -112,26 +112,26 @@ module CloseYourIt
|
|
|
112
112
|
|
|
113
113
|
private
|
|
114
114
|
|
|
115
|
-
# contexts
|
|
116
|
-
#
|
|
117
|
-
#
|
|
115
|
+
# User contexts + `replay.replay_id` (session replay) when present on the scope: the backend
|
|
116
|
+
# reads contexts.replay.replay_id to link the server error to the video (same spot as the JS
|
|
117
|
+
# path). replay/replay_id are not sensitive keys → they survive the Scrubber.
|
|
118
118
|
def contexts_payload
|
|
119
119
|
merged = @contexts.dup
|
|
120
120
|
merged["replay"] = { "replay_id" => @replay_session_id } if @replay_session_id
|
|
121
121
|
scrub(presence(merged))
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
124
|
+
# Redacts the values of sensitive keys preserving the structure (e.g. contexts.runtime stays
|
|
125
|
+
# intact, only values under sensitive keys become [FILTERED]). Reuses the configuration's
|
|
126
|
+
# Scrubber, the same path as breadcrumb.data and log attributes.
|
|
127
127
|
def scrub(hash)
|
|
128
128
|
return hash if hash.nil?
|
|
129
129
|
|
|
130
130
|
Scrubber.new(CloseYourIt.configuration).filter_params(hash)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
# Request context + body params (`request.data`)
|
|
134
|
-
#
|
|
133
|
+
# Request context + body params (`request.data`) extracted LAZILY here — i.e. only when an
|
|
134
|
+
# event is actually built, never on the request's happy path.
|
|
135
135
|
def request_payload
|
|
136
136
|
return nil if @request.nil?
|
|
137
137
|
|
|
@@ -148,8 +148,8 @@ module CloseYourIt
|
|
|
148
148
|
nil
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
-
# `user.id`
|
|
152
|
-
#
|
|
151
|
+
# `user.id` always; email/ip_address/username only with `send_pii` (the backend strips them
|
|
152
|
+
# anyway — defense in depth).
|
|
153
153
|
def serialize_user
|
|
154
154
|
return nil if @user.empty?
|
|
155
155
|
return @user if CloseYourIt.configuration.send_pii
|