closeyourit-ruby 0.10.0 → 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/lib/closeyourit/background_worker.rb +14 -14
- data/lib/closeyourit/breadcrumb.rb +2 -2
- data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
- data/lib/closeyourit/client.rb +24 -24
- data/lib/closeyourit/configuration.rb +102 -102
- 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 +6 -6
- 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 +22 -22
- 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
|
@@ -2,29 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
4
|
module Rails
|
|
5
|
-
#
|
|
6
|
-
# (
|
|
7
|
-
#
|
|
5
|
+
# Included in ActiveJob::Base (via the railtie's `on_load(:active_job)`): captures job errors
|
|
6
|
+
# (otherwise lost) with the job context. The logic lives in `.monitor`/`.report_discarded` so it
|
|
7
|
+
# is testable without ActiveSupport/ActiveJob.
|
|
8
8
|
#
|
|
9
|
-
# CYRB-19: `around_perform`
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
9
|
+
# CYRB-19: `around_perform` runs INSIDE `perform_now`, while `retry_on`/`discard_on` (rescue_from)
|
|
10
|
+
# are evaluated OUTSIDE, after the callbacks. Capturing in around_perform therefore means reporting
|
|
11
|
+
# EVERY attempt — even those that will be retried successfully — because the reporter sees the
|
|
12
|
+
# error before the retry can silence it, and every attempt raises a new instance (the internal,
|
|
13
|
+
# per-instance dedup does not kick in). On ActiveJob 7.1+ we delegate reporting to `after_discard`,
|
|
14
|
+
# which Rails calls on final failure (retry_on exhausted or unhandled exception), never on the
|
|
15
|
+
# attempts `retry_on` retries → a single occurrence per job.
|
|
16
16
|
#
|
|
17
|
-
#
|
|
18
|
-
# - `after_discard`
|
|
19
|
-
#
|
|
20
|
-
# -
|
|
21
|
-
# `after_discard`
|
|
22
|
-
#
|
|
23
|
-
#
|
|
17
|
+
# Known limits (by ActiveJob's design, not the client's):
|
|
18
|
+
# - `after_discard` does NOT fire for errors caught by a custom `rescue_from`: they are handled by
|
|
19
|
+
# the app, so we no longer report them as "unhandled" (we used to, improperly).
|
|
20
|
+
# - ADAPTER-level retries (e.g. Sidekiq) WITHOUT `retry_on` re-raise the unhandled error:
|
|
21
|
+
# `after_discard` fires on every execution, so per-attempt dedup does not apply there
|
|
22
|
+
# (behavior unchanged from before).
|
|
23
|
+
# On versions without `after_discard` we keep the legacy fallback (capture in around_perform).
|
|
24
24
|
module ActiveJobExtension
|
|
25
|
-
# Scope
|
|
26
|
-
# (
|
|
27
|
-
#
|
|
25
|
+
# Scope enriched during `perform`, handed over to `after_discard`. Bound to the job INSTANCE
|
|
26
|
+
# (every retry creates a new, deserialized one) → it lives exactly as long as needed, no bleed
|
|
27
|
+
# between jobs or threads; the end-of-`perform` reset only detaches it from storage, it does not
|
|
28
|
+
# mutate the object.
|
|
28
29
|
STASHED_SCOPE_IVAR = :@__closeyourit_stashed_scope
|
|
29
30
|
|
|
30
31
|
def self.included(base)
|
|
@@ -39,9 +40,10 @@ module CloseYourIt
|
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
43
|
+
# Runs the job enriching the scope with tags/context; resets the scope at the end of the job (no
|
|
44
|
+
# bleed between jobs on the same thread). On ActiveJob 7.1+ it does NOT capture the error
|
|
45
|
+
# (`after_discard` does, only on final failure): here it hands the scope to the job and re-raises.
|
|
46
|
+
# On the legacy fallback it captures.
|
|
45
47
|
def self.monitor(job)
|
|
46
48
|
return yield unless CloseYourIt.configuration.report_active_job_errors
|
|
47
49
|
|
|
@@ -60,10 +62,10 @@ module CloseYourIt
|
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
65
|
+
# `after_discard` hook (ActiveJob 7.1+): the job has failed for good, so we report the error ONLY
|
|
66
|
+
# once (handled:false). We restore the scope enriched during `perform` (handed over by `monitor`)
|
|
67
|
+
# so the report keeps the breadcrumbs/tags/contexts collected in the job; `apply_job_scope`
|
|
68
|
+
# refreshes the standard fields with the final `executions` without losing the custom ones.
|
|
67
69
|
def self.report_discarded(job, exception)
|
|
68
70
|
return unless CloseYourIt.configuration.report_active_job_errors
|
|
69
71
|
|
|
@@ -78,8 +80,8 @@ module CloseYourIt
|
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
|
|
81
|
-
# true
|
|
82
|
-
#
|
|
83
|
+
# true when ActiveJob exposes `after_discard` (7.1+): reporting is delegated there, so `monitor`
|
|
84
|
+
# does not capture intermediate attempts. false → legacy fallback (capture in around_perform).
|
|
83
85
|
def self.report_on_discard?(job)
|
|
84
86
|
job.class.respond_to?(:after_discard)
|
|
85
87
|
end
|
|
@@ -93,8 +95,8 @@ module CloseYourIt
|
|
|
93
95
|
context["executions"] = job.executions if job.respond_to?(:executions)
|
|
94
96
|
CloseYourIt.set_context("active_job", context) unless context.empty?
|
|
95
97
|
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
+
# A stable trace_id per execution (job_id is unique per ActiveJob run): logs and error of the
|
|
99
|
+
# same job share the trace_id → log↔error correlation outside the request cycle too.
|
|
98
100
|
CloseYourIt::Scope.current.trace_id = job.job_id if job.respond_to?(:job_id)
|
|
99
101
|
end
|
|
100
102
|
end
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
4
|
module Rails
|
|
5
|
-
# Rack middleware:
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Rack middleware: captures unhandled exceptions, sends them to CloseYourIt
|
|
6
|
+
# and **re-raises** them (the app keeps handling them as before). Pure Rack, no
|
|
7
|
+
# Rails dependency → testable in isolation.
|
|
8
8
|
class CaptureExceptions
|
|
9
9
|
def initialize(app)
|
|
10
10
|
@app = app
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
4
|
module Rails
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# middleware
|
|
5
|
+
# `ActiveSupport::ErrorReporter` subscriber (Rails 7+): captures HANDLED errors reported via
|
|
6
|
+
# `Rails.error.report`/`Rails.error.handle`. Unhandled ones already go through the Rack
|
|
7
|
+
# middleware → dedup (ivar on the instance) avoids double sending.
|
|
8
8
|
class ErrorSubscriber
|
|
9
9
|
SEVERITY_TO_LEVEL = { error: "error", warning: "warning", info: "info" }.freeze
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# Noisy internal sources not to forward (avoids loops/duplicates).
|
|
12
12
|
IGNORED_SOURCES = %w[closeyourit].freeze
|
|
13
13
|
|
|
14
14
|
def report(error, handled:, severity:, context:, source: nil)
|
|
@@ -4,26 +4,26 @@ require "logger"
|
|
|
4
4
|
|
|
5
5
|
module CloseYourIt
|
|
6
6
|
module Rails
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# CloseYourIt.log → ingest /logs.
|
|
10
|
-
# (:warn)
|
|
11
|
-
# device (
|
|
7
|
+
# Logger-compatible sink attached to Rails.logger (opt-in broadcast, `config.capture_rails_logs`):
|
|
8
|
+
# every app log ≥ threshold (`capture_rails_logs_min_level`, default :warn) is forwarded to
|
|
9
|
+
# CloseYourIt.log → ingest /logs. Below threshold: no-op (nothing sent). The default is
|
|
10
|
+
# conservative (:warn) so the stream is not flooded with framework info noise (CYRB-7). It writes
|
|
11
|
+
# to no device (it only forwards).
|
|
12
12
|
class LogBroadcast < ::Logger
|
|
13
13
|
SEVERITY_LEVELS = { 0 => "debug", 1 => "info", 2 => "warning", 3 => "error", 4 => "fatal", 5 => "fatal" }.freeze
|
|
14
14
|
LEVEL_BY_SYMBOL = { debug: 0, info: 1, warn: 2, warning: 2, error: 3, fatal: 4 }.freeze
|
|
15
15
|
|
|
16
16
|
def initialize(min_level = :warn)
|
|
17
|
-
super(nil) #
|
|
17
|
+
super(nil) # no device: it only forwards
|
|
18
18
|
self.level = LEVEL_BY_SYMBOL.fetch(min_level.to_sym, 2)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
# CloseYourIt.log.
|
|
23
|
-
# `ActionController::RoutingError`
|
|
24
|
-
#
|
|
25
|
-
# (CYRB-17).
|
|
26
|
-
#
|
|
21
|
+
# Overrides ::Logger's single entry point: filters by threshold and by exclusions, then forwards
|
|
22
|
+
# to CloseYourIt.log. The second filter exists because the threshold alone cannot tell noise
|
|
23
|
+
# apart: an `ActionController::RoutingError` from a missing favicon is a `logger.error` like any
|
|
24
|
+
# other, and without checking the TEXT it came back through the log channel after being excluded
|
|
25
|
+
# from the errors one (CYRB-17). Gating lives here and not in `CloseYourIt.log`: that method also
|
|
26
|
+
# serves the dev who writes a log on purpose, and their lines are never silenced.
|
|
27
27
|
def add(severity, message = nil, progname = nil)
|
|
28
28
|
severity ||= ::Logger::UNKNOWN
|
|
29
29
|
return true if severity < level
|
|
@@ -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
|