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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/lib/closeyourit/background_worker.rb +28 -9
  4. data/lib/closeyourit/breadcrumb.rb +2 -2
  5. data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
  6. data/lib/closeyourit/client.rb +42 -29
  7. data/lib/closeyourit/configuration.rb +105 -101
  8. data/lib/closeyourit/event.rb +6 -6
  9. data/lib/closeyourit/events/error_event.rb +16 -16
  10. data/lib/closeyourit/events/job_metric_event.rb +8 -8
  11. data/lib/closeyourit/events/log_event.rb +17 -17
  12. data/lib/closeyourit/events/message_event.rb +4 -4
  13. data/lib/closeyourit/events/performance_issue_event.rb +4 -4
  14. data/lib/closeyourit/events/slow_method_event.rb +6 -6
  15. data/lib/closeyourit/events/slow_query_event.rb +4 -4
  16. data/lib/closeyourit/instrumenter.rb +12 -4
  17. data/lib/closeyourit/line_cache.rb +4 -4
  18. data/lib/closeyourit/log_buffer.rb +10 -10
  19. data/lib/closeyourit/log_device.rb +18 -18
  20. data/lib/closeyourit/monitor.rb +2 -2
  21. data/lib/closeyourit/performance/request_profile.rb +5 -5
  22. data/lib/closeyourit/performance/rollup.rb +3 -3
  23. data/lib/closeyourit/rails/active_job_extension.rb +33 -31
  24. data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
  25. data/lib/closeyourit/rails/error_subscriber.rb +4 -4
  26. data/lib/closeyourit/rails/log_broadcast.rb +12 -12
  27. data/lib/closeyourit/rails/net_http_patch.rb +22 -22
  28. data/lib/closeyourit/rails/query_source.rb +3 -3
  29. data/lib/closeyourit/rails/railtie.rb +32 -52
  30. data/lib/closeyourit/rails/request_body.rb +7 -7
  31. data/lib/closeyourit/rails/request_context.rb +27 -27
  32. data/lib/closeyourit/scope.rb +29 -29
  33. data/lib/closeyourit/scrubber.rb +47 -48
  34. data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
  35. data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +10 -11
  36. data/lib/closeyourit/stats.rb +8 -8
  37. data/lib/closeyourit/subscribers/job_performance.rb +30 -19
  38. data/lib/closeyourit/subscribers/request_performance.rb +4 -4
  39. data/lib/closeyourit/subscribers/slow_query.rb +42 -20
  40. data/lib/closeyourit/trace_context.rb +22 -22
  41. data/lib/closeyourit/transport.rb +25 -20
  42. data/lib/closeyourit/usage_registry.rb +17 -16
  43. data/lib/closeyourit/version.rb +1 -1
  44. data/lib/closeyourit-ruby.rb +125 -125
  45. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1d5e126aa96c17501b6830b9c7763979c239cd4bf4b0e0d43685b0652025378
4
- data.tar.gz: c2d52c988da1288a5cea2986261481ec87fd67db8f3fb0921e2161c90653022e
3
+ metadata.gz: 866fd0b6c34af299e0e538ddb181b1f6b3fd961da1e638226a81ad8b8ea15c0b
4
+ data.tar.gz: eaf0d4530e9015f7353790aeffe104f04a2f80d46c4c7622ad7dad3302bd75fe
5
5
  SHA512:
6
- metadata.gz: e9b3c6b9d352ad92055e710951157956c6fa3a75c8cbcee6bf6f9dbdbd3eb829ac6c990f54a38a72ef141c2e8d5a71eb714905355cbeee7be94a3957d6f6b22e
7
- data.tar.gz: 6bc37b84e2d6a19e6690df0c40732df11fcf32069f1c9942f2d2ddbf6bb76cedb29a94e86c6fd4269002c9f0f9a4c352c53e1c0b41da9a32f5588503ad645b45
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
- # Esegue l'invio fire-and-forget. Con `threads == 0` esegue sincrono (test/dev);
7
- # altrimenti usa una thread-pool con coda bounded e `fallback_policy: :discard`
8
- # (se la coda è piena l'evento si perde, mai backpressure sulla request).
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
- # Ritorna true se l'evento è stato accettato (o eseguito sincrono), false se scartato
17
- # perché la coda era piena (`fallback_policy: :discard`). Mai backpressure sulla request.
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
- # Mai propagare: la telemetria non deve poter crashare l'app ospite.
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: coda piena, evento scartato")
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
- def shutdown(timeout = 1)
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
- # Singola briciola di contesto (query, navigazione, evento custom) precedente a un errore.
7
- # Forma evento Sentry (`breadcrumbs.values[]`). Il `data` è già scrubato a monte (module API).
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
- # Ring buffer limitato di breadcrumb. Vive nello Scope (un buffer per execution-context),
5
- # scritto solo dal thread proprietario → niente mutex. Oltre `max_size` droppa il più vecchio.
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
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- # Compone Transport + BackgroundWorker: applica `before_send` e dispatcha
5
- # l'invio in modo fire-and-forget.
4
+ # Combines Transport + BackgroundWorker: applies `before_send` and dispatches
5
+ # the send fire-and-forget.
6
6
  class Client
7
- # Tetto di log per singola richiesta a /logs. Il backend rifiuta un batch oltre questo limite
8
- # (413 R413-LOG-002) scartando l'INTERA richiesta — e il buffer è già stato drenato → log persi.
9
- # Deve restare ≤ del limite server (LOGS_MAX_BATCH backend = 1000). Vedi #flush_logs.
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 ha scartato l'evento (ritorna nil): scarto voluto, reso visibile (CYRB-12).
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
- # La telemetria non deve MAI propagare nel path dell'app ospite: capture_event è invocato dal
40
- # subscriber sql.active_record, che gira nel thread della query. to_h (scrubber su bind
41
- # malformato) e before_send sono valutati qui in modo sincrono → se sollevano, assorbiamo,
42
- # logghiamo e scartiamo l'evento invece di disturbare la query ospite. Vedi CYRB-2.
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
- # Invia un batch di log come ARRAY a /logs (l'endpoint accetta singolo o array). before_send è
50
- # applicato a ciascun payload; quelli scartati (nil) non vengono inviati. I payload oltre
51
- # LOGS_MAX_BATCH sono spezzati in più POST sequenziali (un chunk = un POST), così un flush grande
52
- # non viene rigettato in blocco dal backend e perso — vedi R3 / LOGS_MAX_BATCH. Un flush entro il
53
- # limite resta un singolo POST.
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.map(&:to_h)
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 — il flush della telemetria d'uso: UNA POST per finestra, payload privo di dati utente
82
- # per costruzione (route = Controller#action). Fire-and-forget via worker, come tutto il resto.
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