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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 866fd0b6c34af299e0e538ddb181b1f6b3fd961da1e638226a81ad8b8ea15c0b
|
|
4
|
+
data.tar.gz: eaf0d4530e9015f7353790aeffe104f04a2f80d46c4c7622ad7dad3302bd75fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4193d36f1123b89bdf11998ebcd5ea445b0b84c325ccae654d434d2d94158d6dd9c920b0e879167f0fba5cbaa7615f42a3d2cd11f3f61d8ade9f3eaee18917f
|
|
7
|
+
data.tar.gz: 7a4d366fbf67b5720d878e2e1febbcda5500c6063d8f654e450c10b68789964a7c8b15390e23d4516d9220ea6cb2ffbde619900ec2871fe7b8b20fd23c967b8b
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
require "concurrent"
|
|
4
4
|
|
|
5
5
|
module CloseYourIt
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# (
|
|
6
|
+
# Runs the send fire-and-forget. With `threads == 0` it runs synchronously (test/dev);
|
|
7
|
+
# otherwise it uses a thread pool with a bounded queue and `fallback_policy: :discard`
|
|
8
|
+
# (when the queue is full the event is lost, never backpressure on the request).
|
|
9
9
|
class BackgroundWorker
|
|
10
10
|
attr_reader :executor
|
|
11
11
|
|
|
@@ -13,29 +13,29 @@ module CloseYourIt
|
|
|
13
13
|
@executor = build_executor(threads.to_i, max_queue)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# Returns true if the event was accepted (or run synchronously), false if dropped
|
|
17
|
+
# because the queue was full (`fallback_policy: :discard`). Never backpressure on the request.
|
|
18
18
|
def perform(&block)
|
|
19
19
|
accepted = @executor.post do
|
|
20
20
|
block.call
|
|
21
21
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
22
|
-
#
|
|
22
|
+
# Never propagate: telemetry must not be able to crash the host app.
|
|
23
23
|
CloseYourIt.internal_logger.error("CloseYourIt background worker: #{e.class}: #{e.message}")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
unless accepted
|
|
27
27
|
CloseYourIt.stats.increment(:dropped)
|
|
28
|
-
CloseYourIt.internal_logger.warn("CloseYourIt background worker:
|
|
28
|
+
CloseYourIt.internal_logger.warn("CloseYourIt background worker: queue full, event dropped")
|
|
29
29
|
CloseYourIt.notify_diagnostic(:drop, reason: :queue_full)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
accepted
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
#
|
|
36
|
-
# (Transport::MAX_REQUEST_SECONDS):
|
|
37
|
-
#
|
|
38
|
-
#
|
|
35
|
+
# Drains the queue waiting up to `timeout` seconds. The default is the duration cap of ONE POST
|
|
36
|
+
# (Transport::MAX_REQUEST_SECONDS): waiting less abandons still healthy sends, which is what
|
|
37
|
+
# happened with the old fixed one second (CYRB-24). If the drain expires, the tasks left in the
|
|
38
|
+
# queue are lost: count them as `dropped`, otherwise the end-of-life snapshot reports zero losses.
|
|
39
39
|
def shutdown(timeout = Transport::MAX_REQUEST_SECONDS)
|
|
40
40
|
return unless @executor.respond_to?(:shutdown)
|
|
41
41
|
|
|
@@ -47,12 +47,12 @@ module CloseYourIt
|
|
|
47
47
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
50
|
+
# Tasks queued and never run when the drain expired. `queue_length` does not exist on the
|
|
51
|
+
# ImmediateExecutor (synchronous, no queue) → zero left over.
|
|
52
52
|
def report_abandoned(timeout)
|
|
53
53
|
abandoned = @executor.respond_to?(:queue_length) ? @executor.queue_length.to_i : 0
|
|
54
54
|
CloseYourIt.internal_logger.warn(
|
|
55
|
-
"CloseYourIt background worker: drain
|
|
55
|
+
"CloseYourIt background worker: drain expired after #{timeout}s, #{abandoned} events abandoned"
|
|
56
56
|
)
|
|
57
57
|
abandoned.times do
|
|
58
58
|
CloseYourIt.stats.increment(:dropped)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
5
|
module CloseYourIt
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# A single context breadcrumb (query, navigation, custom event) preceding an error.
|
|
7
|
+
# Sentry event shape (`breadcrumbs.values[]`). `data` is already scrubbed upstream (module API).
|
|
8
8
|
class Breadcrumb
|
|
9
9
|
def initialize(message: nil, category: nil, type: "default", level: "info", data: {}, timestamp: nil)
|
|
10
10
|
@timestamp = timestamp || Time.now.utc.iso8601
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Bounded breadcrumb ring buffer. It lives in the Scope (one buffer per execution context),
|
|
5
|
+
# written only by the owning thread → no mutex. Beyond `max_size` it drops the oldest.
|
|
6
6
|
class BreadcrumbBuffer
|
|
7
7
|
def initialize(max_size)
|
|
8
8
|
@max_size = max_size.to_i
|
data/lib/closeyourit/client.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Combines Transport + BackgroundWorker: applies `before_send` and dispatches
|
|
5
|
+
# the send fire-and-forget.
|
|
6
6
|
class Client
|
|
7
|
-
#
|
|
8
|
-
# (413 R413-LOG-002)
|
|
9
|
-
#
|
|
7
|
+
# Log cap per single request to /logs. The backend rejects a batch beyond this limit
|
|
8
|
+
# (413 R413-LOG-002) dropping the WHOLE request — and the buffer has already been drained → lost
|
|
9
|
+
# logs. Must stay ≤ the server limit (backend LOGS_MAX_BATCH = 1000). See #flush_logs.
|
|
10
10
|
LOGS_MAX_BATCH = 1000
|
|
11
11
|
|
|
12
12
|
def initialize(configuration)
|
|
@@ -22,7 +22,7 @@ module CloseYourIt
|
|
|
22
22
|
payload = event.to_h
|
|
23
23
|
payload = @configuration.before_send.call(payload) if @configuration.before_send
|
|
24
24
|
if payload.nil?
|
|
25
|
-
# before_send
|
|
25
|
+
# before_send dropped the event (returned nil): an intentional drop, made visible (CYRB-12).
|
|
26
26
|
CloseYourIt.stats.increment(:dropped)
|
|
27
27
|
CloseYourIt.notify_diagnostic(:drop, reason: :before_send)
|
|
28
28
|
return nil
|
|
@@ -36,21 +36,21 @@ module CloseYourIt
|
|
|
36
36
|
end
|
|
37
37
|
payload
|
|
38
38
|
rescue StandardError => e
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
39
|
+
# Telemetry must NEVER propagate into the host app's path: capture_event is called by the
|
|
40
|
+
# sql.active_record subscriber, which runs on the query's thread. to_h (scrubber on a malformed
|
|
41
|
+
# bind) and before_send are evaluated here synchronously → if they raise, we absorb, log and
|
|
42
|
+
# drop the event instead of disturbing the host query. See CYRB-2.
|
|
43
43
|
CloseYourIt.internal_logger.error("CloseYourIt client: #{e.class}: #{e.message}")
|
|
44
44
|
CloseYourIt.stats.increment(:dropped)
|
|
45
45
|
CloseYourIt.notify_diagnostic(:drop, reason: :error, error: e.class.name)
|
|
46
46
|
nil
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# LOGS_MAX_BATCH
|
|
52
|
-
#
|
|
53
|
-
#
|
|
49
|
+
# Sends a batch of logs as an ARRAY to /logs (the endpoint accepts a single item or an array).
|
|
50
|
+
# before_send is applied to each payload; dropped ones (nil) are not sent. Payloads beyond
|
|
51
|
+
# LOGS_MAX_BATCH are split into several sequential POSTs (one chunk = one POST), so a large flush
|
|
52
|
+
# is not rejected as a whole by the backend and lost — see R3 / LOGS_MAX_BATCH. A flush within
|
|
53
|
+
# the limit stays a single POST.
|
|
54
54
|
def flush_logs(events)
|
|
55
55
|
return nil if events.nil? || events.empty?
|
|
56
56
|
|
|
@@ -68,8 +68,8 @@ module CloseYourIt
|
|
|
68
68
|
payloads
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
# CYSK-29 —
|
|
72
|
-
#
|
|
71
|
+
# CYSK-29 — the usage telemetry flush: ONE POST per window, a payload free of user data by
|
|
72
|
+
# construction (route = Controller#action). Fire-and-forget via the worker, like everything else.
|
|
73
73
|
def flush_usage(symbols:, truncated:, window_started_at:, window_ended_at:)
|
|
74
74
|
payload = {
|
|
75
75
|
environment: @configuration.environment.to_s,
|
|
@@ -90,22 +90,22 @@ module CloseYourIt
|
|
|
90
90
|
|
|
91
91
|
private
|
|
92
92
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
93
|
+
# Builds the payload of ONE log entry. `to_h` (scrubber on a non-UTF-8 byte) and `before_send`
|
|
94
|
+
# may raise: here the buffer has already been drained, so a single broken entry evaluated together
|
|
95
|
+
# with the others would take the whole batch of healthy entries with it. Each entry is isolated
|
|
96
|
+
# and dropped on its own, like the JS client does (CYRB-24). `nil` = entry not to send, already counted.
|
|
97
97
|
def build_log_payload(event)
|
|
98
98
|
payload = event.to_h
|
|
99
99
|
payload = @configuration.before_send.call(payload) if @configuration.before_send
|
|
100
100
|
return payload unless payload.nil?
|
|
101
101
|
|
|
102
|
-
#
|
|
103
|
-
#
|
|
102
|
+
# Intentional before_send drop: counted as in #capture_event, otherwise it would silently
|
|
103
|
+
# disappear from the counters (CYRB-12).
|
|
104
104
|
CloseYourIt.stats.increment(:dropped)
|
|
105
105
|
CloseYourIt.notify_diagnostic(:drop, reason: :before_send)
|
|
106
106
|
nil
|
|
107
107
|
rescue StandardError => e
|
|
108
|
-
CloseYourIt.internal_logger.error("CloseYourIt client: log
|
|
108
|
+
CloseYourIt.internal_logger.error("CloseYourIt client: log dropped — #{e.class}: #{e.message}")
|
|
109
109
|
CloseYourIt.stats.increment(:dropped)
|
|
110
110
|
CloseYourIt.notify_diagnostic(:drop, reason: :error, error: e.class.name)
|
|
111
111
|
nil
|
|
@@ -4,22 +4,22 @@ require "uri"
|
|
|
4
4
|
require "concurrent"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Holds all client options. Built by `CloseYourIt.init { |c| ... }`.
|
|
8
|
+
# Without `endpoint_url`/`token`/`project_id` (or with `http://` in production) the client is a no-op.
|
|
9
9
|
class Configuration
|
|
10
10
|
DEFAULT_EXCLUDED_EXCEPTIONS = %w[
|
|
11
11
|
ActionController::RoutingError
|
|
12
12
|
ActiveRecord::RecordNotFound
|
|
13
13
|
].freeze
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# HTTP headers captured in the request context (never Authorization/Cookie → no PII/secrets).
|
|
16
16
|
DEFAULT_REQUEST_HEADER_ALLOWLIST = %w[Accept Content-Type User-Agent Referer].freeze
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
18
|
+
# Solid stack service tables: they are framework infrastructure, not application code. A slow
|
|
19
|
+
# query on them cannot be fixed by reading your own repo — it only says the database is under
|
|
20
|
+
# contention, which the app's queries already show. Excluded by DEFAULT from slowdown measurement
|
|
21
|
+
# because otherwise they flood it: on closeyourit-rails on 2026-07-30 they were about 2,740
|
|
22
|
+
# samples out of 4,400 (62%), all between 250 and 655 ms (CYRB-18).
|
|
23
23
|
DEFAULT_EXCLUDED_QUERY_PATTERNS = [
|
|
24
24
|
/\bsolid_queue_/,
|
|
25
25
|
/\bsolid_cache_/,
|
|
@@ -57,140 +57,140 @@ module CloseYourIt
|
|
|
57
57
|
@excluded_exceptions = DEFAULT_EXCLUDED_EXCEPTIONS.dup
|
|
58
58
|
@before_send = nil
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# (
|
|
63
|
-
#
|
|
60
|
+
# Opt-in diagnostic hook: `->(event, details) { ... }` called at every stage of an event's
|
|
61
|
+
# lifecycle (:enqueue, :send, :drop, :timeout, :shutdown) with details free of sensitive data
|
|
62
|
+
# (e.g. `{ reason: :queue_full }`, `{ status: 429 }`). Local and non-recursive: it sends no
|
|
63
|
+
# telemetry and cannot trigger self-monitoring loops (see CloseYourIt.notify_diagnostic, CYRB-12).
|
|
64
64
|
@on_diagnostic = nil
|
|
65
65
|
|
|
66
66
|
@async_threads = default_threads
|
|
67
67
|
@background_worker_max_queue = 30
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
68
|
+
# How long to wait, on process exit, for in-flight sends to complete. Default: the duration cap
|
|
69
|
+
# of ONE POST, so the last batch is not abandoned halfway (CYRB-24).
|
|
70
|
+
# Lower it if process exit must be faster than delivery.
|
|
71
71
|
@shutdown_timeout = Transport::MAX_REQUEST_SECONDS
|
|
72
72
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
73
|
+
# Intercepts SIGTERM to guarantee the end-of-life flush (deploy/Kamal): by default SIGTERM ends
|
|
74
|
+
# the process WITHOUT running at_exit. OPT-IN because it overrides any TERM handler of the host
|
|
75
|
+
# app. See CloseYourIt.shutdown / register_shutdown_flush (CYRB-5).
|
|
76
76
|
@trap_signals = false
|
|
77
77
|
|
|
78
78
|
@slow_query_threshold_ms = 100
|
|
79
79
|
@slow_method_threshold_ms = 200
|
|
80
|
-
#
|
|
81
|
-
#
|
|
80
|
+
# Queries NOT to measure as slowdowns (match on the SQL text). Default: the Solid stack service
|
|
81
|
+
# tables. Whoever really wants to measure them clears the list.
|
|
82
82
|
@excluded_query_patterns = DEFAULT_EXCLUDED_QUERY_PATTERNS.dup
|
|
83
83
|
|
|
84
84
|
@send_pii = false
|
|
85
85
|
@obfuscate_sql = true
|
|
86
86
|
@send_server_name = true
|
|
87
87
|
|
|
88
|
-
#
|
|
88
|
+
# HTTP request context (method/url/header allowlist). Body/query/IP only with send_pii.
|
|
89
89
|
@capture_request = true
|
|
90
90
|
@request_header_allowlist = DEFAULT_REQUEST_HEADER_ALLOWLIST.dup
|
|
91
91
|
|
|
92
|
-
#
|
|
92
|
+
# Source lines around each stacktrace frame (pre/context/post). 0 = disabled.
|
|
93
93
|
@context_lines = 3
|
|
94
94
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
95
|
+
# Request body params in the event (`request.data`), extracted LAZILY only when the error
|
|
96
|
+
# happens, sanitized and scrubbed (denylist + filter_parameters). The backend scrubs again
|
|
97
|
+
# defensively. Uploads → placeholder, 64 KB cap.
|
|
98
98
|
@capture_request_body = true
|
|
99
99
|
|
|
100
|
-
# Breadcrumbs:
|
|
100
|
+
# Breadcrumbs: history (obfuscated queries, custom events) attached to the error.
|
|
101
101
|
@breadcrumbs_enabled = true
|
|
102
102
|
@max_breadcrumbs = 100
|
|
103
103
|
|
|
104
|
-
#
|
|
104
|
+
# Probabilistic sampling of errors/messages (1.0 = send everything, 0.0 = nothing).
|
|
105
105
|
@sample_rate = 1.0
|
|
106
106
|
|
|
107
|
-
#
|
|
107
|
+
# Captures handled errors (Rails.error.report) and ActiveJob/Sidekiq errors (otherwise lost).
|
|
108
108
|
@capture_handled_errors = true
|
|
109
109
|
@report_active_job_errors = true
|
|
110
110
|
|
|
111
|
-
#
|
|
111
|
+
# Captures parameter values — opt-in, default OFF (privacy). Binds/arguments may contain PII.
|
|
112
112
|
@capture_query_bindings = false
|
|
113
113
|
@capture_method_arguments = false
|
|
114
114
|
|
|
115
|
-
#
|
|
115
|
+
# Structured logs (CloseYourIt.log / .logger). Dedicated master switch + sampling + batching.
|
|
116
116
|
@logs_enabled = true
|
|
117
117
|
@logs_sample_rate = 1.0
|
|
118
118
|
@logs_batch_size = 50
|
|
119
119
|
@logs_flush_interval = 5
|
|
120
120
|
|
|
121
|
-
# CYSK-29 —
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
121
|
+
# CYSK-29 — usage telemetry: which routes/jobs/keys actually run. The payload carries no user
|
|
122
|
+
# data by construction (route = Controller#action, never the URL), so the default is ON:
|
|
123
|
+
# thirty days of optional collection taught us that optional means never.
|
|
124
|
+
# The registry is emptied on every flush: the cap limits DISTINCT symbols per window.
|
|
125
125
|
@usage_enabled = true
|
|
126
126
|
@usage_flush_interval = 300
|
|
127
127
|
@usage_max_symbols = 2000
|
|
128
|
-
#
|
|
128
|
+
# Opt-in Rails.logger broadcast → CloseYourIt.log (default OFF; sends only ≥ threshold).
|
|
129
129
|
@capture_rails_logs = false
|
|
130
130
|
@logs_min_level = :info
|
|
131
|
-
#
|
|
132
|
-
# CloseYourIt.log/.logger,
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
131
|
+
# DEDICATED threshold of the Rails.logger broadcast, separate from logs_min_level (which governs
|
|
132
|
+
# CloseYourIt.log/.logger, where the dev chooses what to log). Default :warn — conservative:
|
|
133
|
+
# without it, in high-traffic production EVERY framework info line (Started GET, Rendered, ...)
|
|
134
|
+
# would flood the stream with tens of thousands of log entries/min (CYRB-7). Whoever also wants
|
|
135
|
+
# the framework info lines lowers it explicitly (e.g. :info).
|
|
136
136
|
@capture_rails_logs_min_level = :warn
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
137
|
+
# Rails.logger broadcast noise that is NOT an exception, so excluded_exceptions cannot cover it:
|
|
138
|
+
# repeated framework or gem lines. Regexp on the message text, empty by default. Applies ONLY
|
|
139
|
+
# to the automatic broadcast, never to an explicit CloseYourIt.log.
|
|
140
140
|
@excluded_log_patterns = []
|
|
141
141
|
|
|
142
|
-
# Performance issue detection (
|
|
143
|
-
# OPT-IN, default OFF:
|
|
144
|
-
#
|
|
142
|
+
# Performance issue detection (aggregate verdicts: N+1, slow request, slow external HTTP).
|
|
143
|
+
# OPT-IN, default OFF: it profiles EVERY query of the request → non-negligible overhead, enable it
|
|
144
|
+
# deliberately per app. Thresholds are conservative (little noise). See Performance::Rollup.
|
|
145
145
|
@detect_performance_issues = false
|
|
146
|
-
@n_plus_one_threshold = 10 #
|
|
147
|
-
@query_count_threshold = 100 #
|
|
148
|
-
@query_time_threshold_ms = 500 #
|
|
149
|
-
@slow_request_threshold_ms = 1000 #
|
|
150
|
-
@slow_external_threshold_ms = 1000 #
|
|
151
|
-
@capture_external_http = true #
|
|
152
|
-
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
146
|
+
@n_plus_one_threshold = 10 # same fingerprint+call-site run > N times in one request
|
|
147
|
+
@query_count_threshold = 100 # too many total queries in one request
|
|
148
|
+
@query_time_threshold_ms = 500 # total DB time per request beyond which = high_query_count
|
|
149
|
+
@slow_request_threshold_ms = 1000 # total request duration
|
|
150
|
+
@slow_external_threshold_ms = 1000 # single external HTTP call
|
|
151
|
+
@capture_external_http = true # instruments Net::HTTP (only with detect_performance_issues)
|
|
152
|
+
|
|
153
|
+
# Background job metrics: execution duration and queue wait (queue latency) for ActiveJob and
|
|
154
|
+
# Sidekiq. ON by default (unlike detect_performance_issues): visibility of slow/late/retried jobs
|
|
155
|
+
# must not require manual instrumentation in every app (CYRB-14), and the overhead is low — one
|
|
156
|
+
# notification per job, not profiling of every query. Noise is kept down by the thresholds
|
|
157
|
+
# (normal jobs under threshold = no metric) and by sampling. The label is the job class name;
|
|
158
|
+
# arguments are NEVER sent.
|
|
159
159
|
@monitor_jobs = true
|
|
160
|
-
@slow_job_threshold_ms = 5000 #
|
|
161
|
-
@job_queue_latency_threshold_ms = 60_000 #
|
|
162
|
-
@jobs_sample_rate = 1.0 #
|
|
163
|
-
|
|
164
|
-
#
|
|
165
|
-
# Net::HTTP. OPT-IN, default OFF:
|
|
166
|
-
# per
|
|
167
|
-
#
|
|
168
|
-
# (
|
|
169
|
-
# CloseYourIt →
|
|
160
|
+
@slow_job_threshold_ms = 5000 # perform duration beyond which = slow_job
|
|
161
|
+
@job_queue_latency_threshold_ms = 60_000 # enqueue→execution wait beyond which = job_queue_latency
|
|
162
|
+
@jobs_sample_rate = 1.0 # fraction of over-threshold candidates actually sent
|
|
163
|
+
|
|
164
|
+
# W3C trace context propagation (traceparent/tracestate) to external services called via
|
|
165
|
+
# Net::HTTP. OPT-IN, default OFF: injecting outgoing headers crosses a trust boundary and must be
|
|
166
|
+
# decided per app. It is limited PER DESTINATION by the allowlist (exact hosts, case-insensitive,
|
|
167
|
+
# or Regexp for subdomains); empty list = no destination. Never to unlisted hosts, never as
|
|
168
|
+
# `baggage` (which may carry PII). Inbound, a valid traceparent becomes the trace_id of the
|
|
169
|
+
# CloseYourIt events → the request's errors/metrics correlate with the distributed trace (CYRB-15).
|
|
170
170
|
@propagate_trace_context = false
|
|
171
171
|
@trace_propagation_allowlist = []
|
|
172
172
|
|
|
173
|
-
#
|
|
174
|
-
#
|
|
173
|
+
# Project root: base for the relative frame filename (cross-SDK culprit). Lazy: auto-detected
|
|
174
|
+
# from Rails.root or Dir.pwd on first access when not set explicitly.
|
|
175
175
|
@project_root = nil
|
|
176
176
|
|
|
177
177
|
@filter_parameters = []
|
|
178
178
|
@scrub_message_patterns = []
|
|
179
179
|
end
|
|
180
180
|
|
|
181
|
-
#
|
|
181
|
+
# Classes/strings → name (String); Regexps stay Regexps (pattern match on name/message).
|
|
182
182
|
def excluded_exceptions=(list)
|
|
183
183
|
@excluded_exceptions = Array(list).map { |item| item.is_a?(Regexp) ? item : item.to_s }
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
#
|
|
187
|
-
#
|
|
186
|
+
# Rails.logger broadcast patterns to drop. Strings become Regexps (literal match on the text):
|
|
187
|
+
# whoever writes `config.excluded_log_patterns = ["Rendered layout"]` means exactly that.
|
|
188
188
|
def excluded_log_patterns=(list)
|
|
189
189
|
@excluded_log_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
-
#
|
|
193
|
-
# (
|
|
192
|
+
# Queries not to measure. Same normalization as excluded_log_patterns: String = literal text
|
|
193
|
+
# (a table name is written that way, not as a pattern), Regexp = pattern.
|
|
194
194
|
def excluded_query_patterns=(list)
|
|
195
195
|
@excluded_query_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
|
|
196
196
|
end
|
|
@@ -199,8 +199,8 @@ module CloseYourIt
|
|
|
199
199
|
@filter_parameters = Array(list)
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
-
#
|
|
203
|
-
#
|
|
202
|
+
# Destinations allowed to receive the W3C trace headers. String = exact host (case-insensitive
|
|
203
|
+
# match), Regexp = pattern (for subdomains/host families). Empty list = none.
|
|
204
204
|
def trace_propagation_allowlist=(list)
|
|
205
205
|
@trace_propagation_allowlist = Array(list)
|
|
206
206
|
end
|
|
@@ -213,8 +213,8 @@ module CloseYourIt
|
|
|
213
213
|
environment.to_s == "production"
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
#
|
|
217
|
-
#
|
|
216
|
+
# The client sends only with complete credentials (endpoint + token + project_id) and a secure
|
|
217
|
+
# transport (http:// allowed outside production).
|
|
218
218
|
def enabled?
|
|
219
219
|
return false if blank?(endpoint_url) || blank?(token) || blank?(project_id)
|
|
220
220
|
return false if insecure_endpoint? && production?
|
|
@@ -222,8 +222,8 @@ module CloseYourIt
|
|
|
222
222
|
true
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
-
#
|
|
226
|
-
#
|
|
225
|
+
# Logs configuration warnings (e.g. http:// endpoint, malformed project_id/endpoint).
|
|
226
|
+
# Never raises: consistent with the client's no-op philosophy. Called by `CloseYourIt.init`.
|
|
227
227
|
def validate!
|
|
228
228
|
CloseYourIt.internal_logger.warn(insecure_endpoint_message) if insecure_endpoint?
|
|
229
229
|
CloseYourIt.internal_logger.warn(malformed_project_id_message) if malformed_project_id?
|
|
@@ -231,22 +231,22 @@ module CloseYourIt
|
|
|
231
231
|
self
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
-
#
|
|
234
|
+
# Effective release: the one set, otherwise auto-detected (deploy/CI ENV or git).
|
|
235
235
|
def release
|
|
236
236
|
return @release unless @release.nil?
|
|
237
237
|
|
|
238
238
|
@release = detect_release
|
|
239
239
|
end
|
|
240
240
|
|
|
241
|
-
#
|
|
242
|
-
#
|
|
241
|
+
# Effective project root: the one set, otherwise auto-detected (Rails.root or Dir.pwd).
|
|
242
|
+
# Used to make `frame.filename` relative (culprit comparable across SDKs — CYRB-4).
|
|
243
243
|
def project_root
|
|
244
244
|
return @project_root unless @project_root.nil?
|
|
245
245
|
|
|
246
246
|
@project_root = detect_project_root
|
|
247
247
|
end
|
|
248
248
|
|
|
249
|
-
# Rails.root
|
|
249
|
+
# Rails.root when the app runs under Rails, otherwise the working directory. Never raises.
|
|
250
250
|
def detect_project_root
|
|
251
251
|
return ::Rails.root.to_s if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
|
252
252
|
|
|
@@ -255,12 +255,12 @@ module CloseYourIt
|
|
|
255
255
|
Dir.pwd
|
|
256
256
|
end
|
|
257
257
|
|
|
258
|
-
#
|
|
259
|
-
#
|
|
258
|
+
# A semver tag (optional `v`) is preferred to the short SHA as release: it converges with what the
|
|
259
|
+
# CI records (it tags), while the short SHA creates duplicate releases on the backend (CYRB-9).
|
|
260
260
|
SEMVER_TAG = /\Av?\d+\.\d+\.\d+([-+.].+)?\z/
|
|
261
261
|
|
|
262
|
-
#
|
|
263
|
-
#
|
|
262
|
+
# Release auto-detection: first a semver tag (APP_GIT_TAG/GIT_TAG), then the short SHA from the
|
|
263
|
+
# deploy/CI env or from git. Never raises.
|
|
264
264
|
def detect_release
|
|
265
265
|
detect_tag ||
|
|
266
266
|
ENV["KAMAL_VERSION"] ||
|
|
@@ -275,8 +275,8 @@ module CloseYourIt
|
|
|
275
275
|
|
|
276
276
|
private
|
|
277
277
|
|
|
278
|
-
# `.git`
|
|
279
|
-
#
|
|
278
|
+
# `.git` is a directory in a normal checkout and a file in a worktree → File.directory? is false
|
|
279
|
+
# in worktrees, so tests do not spawn git subprocesses (deterministic).
|
|
280
280
|
def git_revision
|
|
281
281
|
return nil unless File.directory?(".git")
|
|
282
282
|
|
|
@@ -286,9 +286,9 @@ module CloseYourIt
|
|
|
286
286
|
nil
|
|
287
287
|
end
|
|
288
288
|
|
|
289
|
-
#
|
|
290
|
-
# (v
|
|
291
|
-
#
|
|
289
|
+
# Semver tag from APP_GIT_TAG then GIT_TAG: accepts only a non-blank value shaped like semver
|
|
290
|
+
# (optional v + MAJOR.MINOR.PATCH + optional suffix). Rejects `unknown`, empty, branch names,
|
|
291
|
+
# etc. → nil, so detect_release falls back to the SHA chain.
|
|
292
292
|
def detect_tag
|
|
293
293
|
tag = ENV["APP_GIT_TAG"] || ENV["GIT_TAG"]
|
|
294
294
|
return nil if blank?(tag)
|
|
@@ -303,17 +303,17 @@ module CloseYourIt
|
|
|
303
303
|
!uri.nil? && uri.scheme != "https"
|
|
304
304
|
end
|
|
305
305
|
|
|
306
|
-
#
|
|
307
|
-
#
|
|
306
|
+
# Warns when project_id is set but does not look like a UUID (the typical mistake is pasting a
|
|
307
|
+
# slug/name instead of the id). It does not block: the server is the authority on validity.
|
|
308
308
|
def malformed_project_id?
|
|
309
309
|
!blank?(project_id) && !UUID_FORMAT.match?(project_id.to_s)
|
|
310
310
|
end
|
|
311
311
|
|
|
312
312
|
def malformed_project_id_message
|
|
313
|
-
"CloseYourIt: project_id (#{project_id})
|
|
313
|
+
"CloseYourIt: project_id (#{project_id}) is not shaped like a UUID — check that you copied the right id."
|
|
314
314
|
end
|
|
315
315
|
|
|
316
|
-
#
|
|
316
|
+
# Warns when endpoint_url is set but not parsable or has no host.
|
|
317
317
|
def malformed_endpoint?
|
|
318
318
|
return false if blank?(endpoint_url)
|
|
319
319
|
|
|
@@ -322,12 +322,12 @@ module CloseYourIt
|
|
|
322
322
|
end
|
|
323
323
|
|
|
324
324
|
def malformed_endpoint_message
|
|
325
|
-
"CloseYourIt: endpoint_url (#{endpoint_url})
|
|
325
|
+
"CloseYourIt: endpoint_url (#{endpoint_url}) is not a valid URL (missing host)."
|
|
326
326
|
end
|
|
327
327
|
|
|
328
328
|
def insecure_endpoint_message
|
|
329
|
-
tail = production? ? "
|
|
330
|
-
"CloseYourIt: endpoint_url
|
|
329
|
+
tail = production? ? "Telemetry DISABLED in production." : "Allowed only in development."
|
|
330
|
+
"CloseYourIt: endpoint_url uses insecure http:// (#{endpoint_url}) — the token would travel in clear text. #{tail}"
|
|
331
331
|
end
|
|
332
332
|
|
|
333
333
|
def parsed_endpoint
|