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
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
|
data/README.md
CHANGED
|
@@ -79,6 +79,7 @@ end
|
|
|
79
79
|
| `before_send` | `nil` | `->(payload) { ... }` — scrub finale; ritorna payload o `nil` per scartare |
|
|
80
80
|
| `sample_rate` | `1.0` | Frazione di errori/messaggi inviata (`1.0` tutto, `0.0` niente) |
|
|
81
81
|
| `async_threads` | `cpu/2` | Thread di invio; `0` = sincrono (test) |
|
|
82
|
+
| `shutdown_timeout` | `15` | Secondi di attesa, alla chiusura, perché gli invii in volo completino; scaduto il tempo gli eventi residui sono contati come `dropped` (vedi [Flush di fine-vita](#flush-di-fine-vita)) |
|
|
82
83
|
| `trap_signals` | `false` | Intercetta `SIGTERM` per garantire il flush di fine-vita (deploy/Kamal); opt-in perché sovrascrive un eventuale handler `TERM` dell'app ospite (vedi [Flush di fine-vita](#flush-di-fine-vita)) |
|
|
83
84
|
| `slow_query_threshold_ms` | `100` | Soglia query lente |
|
|
84
85
|
| `excluded_query_patterns` | `solid_queue_`, `solid_cache_`, `solid_cable_` | Query da NON misurare come rallentamento — **Regexp** (pattern) o **String** (testo letterale). Vale solo per la misura: breadcrumb e profiling N+1 vedono tutto |
|
|
@@ -227,6 +228,11 @@ buffer), così i log sotto-batch di rake/CLI non vanno persi all'uscita. `SIGTER
|
|
|
227
228
|
termina il processo **senza** eseguire gli `at_exit`: imposta `trap_signals = true` per intercettarlo e
|
|
228
229
|
convertirlo in un exit pulito, oppure chiama `CloseYourIt.shutdown` esplicitamente prima di terminare.
|
|
229
230
|
|
|
231
|
+
Il drain attende fino a `shutdown_timeout` secondi (default `15`, il tetto di durata di una singola
|
|
232
|
+
POST verso l'ingest). Gli eventi ancora in coda quando il tempo scade sono contati come `dropped`
|
|
233
|
+
nello snapshot diagnostico di fine-vita: nessuna perdita silenziosa. Abbassa il valore se l'uscita
|
|
234
|
+
del processo deve essere più rapida della consegna.
|
|
235
|
+
|
|
230
236
|
#### Server che forkano (Puma cluster, Sidekiq, Unicorn)
|
|
231
237
|
|
|
232
238
|
I server che precaricano l'app (`preload_app!`) inizializzano la gemma nel processo **master**, poi
|
|
@@ -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,34 +13,53 @@ 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
|
-
|
|
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
|
+
def shutdown(timeout = Transport::MAX_REQUEST_SECONDS)
|
|
36
40
|
return unless @executor.respond_to?(:shutdown)
|
|
37
41
|
|
|
38
42
|
@executor.shutdown
|
|
39
|
-
@executor.wait_for_termination(timeout)
|
|
43
|
+
terminated = @executor.wait_for_termination(timeout)
|
|
44
|
+
report_abandoned(timeout) unless terminated
|
|
45
|
+
terminated
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
private
|
|
43
49
|
|
|
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
|
+
def report_abandoned(timeout)
|
|
53
|
+
abandoned = @executor.respond_to?(:queue_length) ? @executor.queue_length.to_i : 0
|
|
54
|
+
CloseYourIt.internal_logger.warn(
|
|
55
|
+
"CloseYourIt background worker: drain expired after #{timeout}s, #{abandoned} events abandoned"
|
|
56
|
+
)
|
|
57
|
+
abandoned.times do
|
|
58
|
+
CloseYourIt.stats.increment(:dropped)
|
|
59
|
+
CloseYourIt.notify_diagnostic(:drop, reason: :shutdown_timeout)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
44
63
|
def build_executor(threads, max_queue)
|
|
45
64
|
return Concurrent::ImmediateExecutor.new if threads <= 0
|
|
46
65
|
|
|
@@ -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,35 +36,25 @@ 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
|
|
|
57
|
-
payloads = events.
|
|
58
|
-
if @configuration.before_send
|
|
59
|
-
kept = payloads.filter_map { |payload| @configuration.before_send.call(payload) }
|
|
60
|
-
# I log che before_send porta a nil sono scarti voluti: contabilizzali come gli errori/metriche
|
|
61
|
-
# (parità con #capture_event), altrimenti sparirebbero silenziosamente dai contatori (CYRB-12).
|
|
62
|
-
(payloads.size - kept.size).times do
|
|
63
|
-
CloseYourIt.stats.increment(:dropped)
|
|
64
|
-
CloseYourIt.notify_diagnostic(:drop, reason: :before_send)
|
|
65
|
-
end
|
|
66
|
-
payloads = kept
|
|
67
|
-
end
|
|
57
|
+
payloads = events.filter_map { |event| build_log_payload(event) }
|
|
68
58
|
return nil if payloads.empty?
|
|
69
59
|
|
|
70
60
|
path = events.first.ingest_path(@configuration.project_id)
|
|
@@ -78,8 +68,8 @@ module CloseYourIt
|
|
|
78
68
|
payloads
|
|
79
69
|
end
|
|
80
70
|
|
|
81
|
-
# CYSK-29 —
|
|
82
|
-
#
|
|
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.
|
|
83
73
|
def flush_usage(symbols:, truncated:, window_started_at:, window_ended_at:)
|
|
84
74
|
payload = {
|
|
85
75
|
environment: @configuration.environment.to_s,
|
|
@@ -95,7 +85,30 @@ module CloseYourIt
|
|
|
95
85
|
end
|
|
96
86
|
|
|
97
87
|
def shutdown
|
|
98
|
-
@worker.shutdown
|
|
88
|
+
@worker.shutdown(@configuration.shutdown_timeout)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
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
|
+
def build_log_payload(event)
|
|
98
|
+
payload = event.to_h
|
|
99
|
+
payload = @configuration.before_send.call(payload) if @configuration.before_send
|
|
100
|
+
return payload unless payload.nil?
|
|
101
|
+
|
|
102
|
+
# Intentional before_send drop: counted as in #capture_event, otherwise it would silently
|
|
103
|
+
# disappear from the counters (CYRB-12).
|
|
104
|
+
CloseYourIt.stats.increment(:dropped)
|
|
105
|
+
CloseYourIt.notify_diagnostic(:drop, reason: :before_send)
|
|
106
|
+
nil
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
CloseYourIt.internal_logger.error("CloseYourIt client: log dropped — #{e.class}: #{e.message}")
|
|
109
|
+
CloseYourIt.stats.increment(:dropped)
|
|
110
|
+
CloseYourIt.notify_diagnostic(:drop, reason: :error, error: e.class.name)
|
|
111
|
+
nil
|
|
99
112
|
end
|
|
100
113
|
end
|
|
101
114
|
end
|