closeyourit-ruby 0.6.1 → 0.8.0
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 +141 -3
- data/lib/closeyourit/background_worker.rb +1 -0
- data/lib/closeyourit/client.rb +26 -4
- data/lib/closeyourit/configuration.rb +68 -3
- data/lib/closeyourit/events/job_metric_event.rb +44 -0
- data/lib/closeyourit/log_buffer.rb +6 -0
- data/lib/closeyourit/rails/active_job_extension.rb +60 -4
- data/lib/closeyourit/rails/log_broadcast.rb +9 -2
- data/lib/closeyourit/rails/net_http_patch.rb +50 -0
- data/lib/closeyourit/rails/railtie.rb +29 -3
- data/lib/closeyourit/rails/request_context.rb +23 -3
- data/lib/closeyourit/scope.rb +11 -1
- data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +58 -0
- data/lib/closeyourit/stats.rb +11 -4
- data/lib/closeyourit/subscribers/job_performance.rb +123 -0
- data/lib/closeyourit/subscribers/slow_query.rb +21 -0
- data/lib/closeyourit/trace_context.rb +109 -0
- data/lib/closeyourit/transport.rb +17 -0
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +146 -5
- metadata +19 -1
data/lib/closeyourit-ruby.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "logger"
|
|
|
5
5
|
require_relative "closeyourit/version"
|
|
6
6
|
require_relative "closeyourit/configuration"
|
|
7
7
|
require_relative "closeyourit/breadcrumb"
|
|
8
|
+
require_relative "closeyourit/trace_context"
|
|
8
9
|
require_relative "closeyourit/scope"
|
|
9
10
|
require_relative "closeyourit/scrubber"
|
|
10
11
|
require_relative "closeyourit/stats"
|
|
@@ -17,12 +18,14 @@ require_relative "closeyourit/events/slow_query_event"
|
|
|
17
18
|
require_relative "closeyourit/events/slow_method_event"
|
|
18
19
|
require_relative "closeyourit/events/log_event"
|
|
19
20
|
require_relative "closeyourit/events/performance_issue_event"
|
|
21
|
+
require_relative "closeyourit/events/job_metric_event"
|
|
20
22
|
require_relative "closeyourit/performance/request_profile"
|
|
21
23
|
require_relative "closeyourit/performance/rollup"
|
|
22
24
|
require_relative "closeyourit/log_device"
|
|
23
25
|
require_relative "closeyourit/log_buffer"
|
|
24
26
|
require_relative "closeyourit/subscribers/slow_query"
|
|
25
27
|
require_relative "closeyourit/subscribers/request_performance"
|
|
28
|
+
require_relative "closeyourit/subscribers/job_performance"
|
|
26
29
|
require_relative "closeyourit/instrumenter"
|
|
27
30
|
require_relative "closeyourit/monitor"
|
|
28
31
|
require_relative "closeyourit/client"
|
|
@@ -34,6 +37,7 @@ require_relative "closeyourit/rails/net_http_patch"
|
|
|
34
37
|
require_relative "closeyourit/rails/active_job_extension"
|
|
35
38
|
require_relative "closeyourit/rails/error_subscriber"
|
|
36
39
|
require_relative "closeyourit/sidekiq/error_handler"
|
|
40
|
+
require_relative "closeyourit/sidekiq/job_metrics_middleware"
|
|
37
41
|
|
|
38
42
|
# CloseYourIt — client di telemetria (errori + statistiche di query/metodi lenti)
|
|
39
43
|
# che invia gli eventi all'endpoint di ingest di CloseYourIt.
|
|
@@ -46,12 +50,27 @@ module CloseYourIt
|
|
|
46
50
|
|
|
47
51
|
CAPTURED_FLAG = :@__closeyourit_captured
|
|
48
52
|
|
|
53
|
+
# Flag thread-local che segna "sono già dentro l'hook diagnostico": impedisce che una notifica
|
|
54
|
+
# emessa DENTRO l'hook (o da codice da esso invocato) rientri e riesegua l'hook → niente loop di
|
|
55
|
+
# auto-monitoraggio (CYRB-12). È per-thread perché le tappe girano su thread diversi (worker pool
|
|
56
|
+
# per send/timeout, thread chiamante per enqueue/drop).
|
|
57
|
+
DIAGNOSTIC_GUARD = :__closeyourit_in_diagnostic
|
|
58
|
+
|
|
49
59
|
class << self
|
|
50
60
|
# Configura il client. Senza token/endpoint → no-op.
|
|
61
|
+
#
|
|
62
|
+
# Una re-init SPEGNE prima il client e il log buffer della configurazione precedente (CYRB-10):
|
|
63
|
+
# azzerarli e basta lascerebbe orfani il thread del worker pool e il TimerTask del buffer, e gli
|
|
64
|
+
# eventi ancora in coda andrebbero persi o flushati fuori tempo dal timer orfano con la vecchia
|
|
65
|
+
# credenziale. Riusa la semantica di fine-vita di #shutdown (flush del buffer → drain del worker
|
|
66
|
+
# con timeout, CYRB-5) — così la coda precedente è svuotata in modo prevedibile con la sua config.
|
|
67
|
+
# Idempotente: alla prima init (o senza eventi catturati) @client/@log_buffer sono nil → no-op.
|
|
51
68
|
def init
|
|
69
|
+
shutdown
|
|
52
70
|
@configuration = Configuration.new
|
|
53
71
|
@client = nil
|
|
54
72
|
@log_buffer = nil
|
|
73
|
+
@shutdown_notified = false # nuova sessione: :shutdown potrà essere notificato di nuovo
|
|
55
74
|
yield(@configuration) if block_given?
|
|
56
75
|
@configuration.validate!
|
|
57
76
|
register_shutdown_flush
|
|
@@ -73,12 +92,13 @@ module CloseYourIt
|
|
|
73
92
|
# Cattura un'eccezione e la spedisce (fire-and-forget). No-op se disabilitato,
|
|
74
93
|
# se l'eccezione è esclusa o già catturata.
|
|
75
94
|
def capture_exception(exception, handled: false, level: "error", contexts: nil)
|
|
95
|
+
return nil if in_diagnostic?
|
|
76
96
|
return nil unless enabled?
|
|
77
97
|
return nil if ignored_exception?(exception)
|
|
78
98
|
return nil if exception_captured?(exception)
|
|
79
99
|
|
|
80
100
|
mark_captured(exception)
|
|
81
|
-
return
|
|
101
|
+
return record_drop(:sampled) unless sampled?
|
|
82
102
|
|
|
83
103
|
event = ErrorEvent.from_exception(
|
|
84
104
|
exception, configuration: configuration, handled: handled, level: level, contexts: contexts
|
|
@@ -88,6 +108,7 @@ module CloseYourIt
|
|
|
88
108
|
|
|
89
109
|
# Spedisce un evento già costruito (slow_query/slow_method).
|
|
90
110
|
def capture_event(event)
|
|
111
|
+
return nil if in_diagnostic?
|
|
91
112
|
return nil unless enabled?
|
|
92
113
|
|
|
93
114
|
client.capture_event(event)
|
|
@@ -96,8 +117,9 @@ module CloseYourIt
|
|
|
96
117
|
# Invia un messaggio diagnostico esplicito (non un'eccezione). Soggetto a sampling + scope.
|
|
97
118
|
# CloseYourIt.capture_message("cache miss storm", level: "warning")
|
|
98
119
|
def capture_message(message, level: "info")
|
|
120
|
+
return nil if in_diagnostic?
|
|
99
121
|
return nil unless enabled?
|
|
100
|
-
return
|
|
122
|
+
return record_drop(:sampled) unless sampled?
|
|
101
123
|
|
|
102
124
|
event = MessageEvent.new(message, level: level, configuration: configuration)
|
|
103
125
|
client.capture_event(event)
|
|
@@ -181,9 +203,10 @@ module CloseYourIt
|
|
|
181
203
|
# sorgente) e per impostare la sorgente solo via `.named` (child logger, parità dart/js — CYRB-8).
|
|
182
204
|
# Le app usano `CloseYourIt.log` / `CloseYourIt.logger`.
|
|
183
205
|
def emit_log(level, message, source: nil, attributes: {})
|
|
206
|
+
return nil if in_diagnostic?
|
|
184
207
|
return nil unless logs_enabled?
|
|
185
208
|
return nil if log_below_min_level?(level)
|
|
186
|
-
return
|
|
209
|
+
return record_drop(:sampled) unless logs_sampled?
|
|
187
210
|
|
|
188
211
|
event = LogEvent.new(message, level: level, attributes: attributes,
|
|
189
212
|
logger: source, configuration: configuration)
|
|
@@ -197,6 +220,37 @@ module CloseYourIt
|
|
|
197
220
|
logs_enabled?
|
|
198
221
|
end
|
|
199
222
|
|
|
223
|
+
# Vero se una riga del broadcast Rails.logger va scartata: nomina un'eccezione già presente in
|
|
224
|
+
# `excluded_exceptions`, oppure combacia con `excluded_log_patterns`.
|
|
225
|
+
#
|
|
226
|
+
# Serve perché un'eccezione esclusa dal canale ERRORI rientrava da quello dei LOG: Rails la
|
|
227
|
+
# registra con `logger.error`, il broadcast inoltrava la riga senza guardarla, e il rumore che
|
|
228
|
+
# `excluded_exceptions` aveva appena scartato ricompariva come log-entry. Il 2026-07-30 erano
|
|
229
|
+
# 48.000 voci su 49.985 nello stream, quasi tutte `ActionController::RoutingError` da favicon
|
|
230
|
+
# mancanti e scansioni di bot — che è nella lista di default dalla prima riga (CYRB-17).
|
|
231
|
+
#
|
|
232
|
+
# `ignored_exception?` non è applicabile: qui la classe arriva come TESTO dentro il messaggio, non
|
|
233
|
+
# come oggetto con `ancestors` da confrontare. Da cui il match per sottostringa sui matcher String.
|
|
234
|
+
#
|
|
235
|
+
# Vale SOLO per il mirror automatico di Rails.logger: un `CloseYourIt.log` scritto di proposito
|
|
236
|
+
# dallo sviluppatore non si silenzia mai (chi lo scrive ha già deciso che vuole quella riga).
|
|
237
|
+
def ignored_log_message?(text)
|
|
238
|
+
text = text.to_s
|
|
239
|
+
return false if text.empty?
|
|
240
|
+
|
|
241
|
+
config = configuration
|
|
242
|
+
named = config.excluded_exceptions.any? do |matcher|
|
|
243
|
+
if matcher.is_a?(Regexp)
|
|
244
|
+
matcher.match?(text)
|
|
245
|
+
else
|
|
246
|
+
# Un matcher vuoto combacerebbe con qualunque riga: mai silenziare tutto per una lista sporca.
|
|
247
|
+
!matcher.empty? && text.include?(matcher)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
named || config.excluded_log_patterns.any? { |pattern| pattern.match?(text) }
|
|
252
|
+
end
|
|
253
|
+
|
|
200
254
|
# Forza l'invio dei log bufferizzati (chiamato anche allo shutdown del processo).
|
|
201
255
|
def flush_logs
|
|
202
256
|
@log_buffer&.flush
|
|
@@ -213,25 +267,112 @@ module CloseYourIt
|
|
|
213
267
|
# Ordine critico: prima il buffer (accoda l'ultimo batch nel worker), poi il worker (lo drena).
|
|
214
268
|
@log_buffer&.shutdown
|
|
215
269
|
@client&.shutdown
|
|
270
|
+
# Riepilogo di fine-vita: l'app riceve lo snapshot dei contatori senza log rumorosi. Emesso una
|
|
271
|
+
# sola volta per sessione (uno shutdown esplicito seguito dall'at_exit non deve duplicarlo; il
|
|
272
|
+
# flag è azzerato a ogni init). Lo snapshot è best-effort: eventuali invii ancora in volo oltre il
|
|
273
|
+
# breve timeout di drain possono non esservi riflessi — il drain non blocca l'uscita (CYRB-5).
|
|
274
|
+
unless @shutdown_notified
|
|
275
|
+
@shutdown_notified = true
|
|
276
|
+
notify_diagnostic(:shutdown, stats: stats.to_h)
|
|
277
|
+
end
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Ripristina le risorse di invio in un processo figlio dopo un fork. Chiamalo dai worker hook dei
|
|
282
|
+
# server che forkano (Puma `on_worker_boot`, Sidekiq/Unicorn `after_fork`) per ricreare SUBITO worker
|
|
283
|
+
# pool e log buffer nel figlio, invece di attendere la rilevazione lazy al primo evento. Opzionale:
|
|
284
|
+
# la gemma rileva comunque il cambio PID da sé (vedi #ensure_current_process!). Idempotente e sicuro
|
|
285
|
+
# anche se client/buffer non sono ancora stati materializzati (→ no-op, verranno creati lazy).
|
|
286
|
+
def after_fork
|
|
287
|
+
discard_inherited_client!
|
|
288
|
+
@pid = Process.pid
|
|
216
289
|
nil
|
|
217
290
|
end
|
|
218
291
|
|
|
219
|
-
# Contatori diagnostici del client (accodati/scartati/spediti/falliti).
|
|
220
|
-
# CloseYourIt.stats.to_h # => { enqueued: …, dropped: …, sent: …, failed: … }
|
|
292
|
+
# Contatori diagnostici del client (accodati/scartati/spediti/falliti/timeout).
|
|
293
|
+
# CloseYourIt.stats.to_h # => { enqueued: …, dropped: …, sent: …, failed: …, timeout: … }
|
|
221
294
|
def stats
|
|
222
295
|
@stats ||= Stats.new
|
|
223
296
|
end
|
|
224
297
|
|
|
298
|
+
# Notifica una tappa del ciclo di vita di un evento all'hook `on_diagnostic` (se configurato).
|
|
299
|
+
# `event` è uno tra :enqueue, :send, :drop, :timeout, :shutdown; `details` un Hash privo di dati
|
|
300
|
+
# sensibili (es. `{ reason: :queue_full }`, `{ status: 429 }`). Chiamato da Client/Transport/
|
|
301
|
+
# BackgroundWorker/LogBuffer. Garanzie (CYRB-12):
|
|
302
|
+
# * NON invia telemetria: tocca solo l'hook dell'app e i contatori in-memory;
|
|
303
|
+
# * NON innesca loop di auto-monitoraggio: durante l'hook il guard è alzato, e finché è alzato
|
|
304
|
+
# sono soppressi SIA i `notify_diagnostic` annidati SIA le API di telemetria (`capture_*`/log,
|
|
305
|
+
# vedi #in_diagnostic?). Poiché l'accodamento della telemetria è sincrono nel thread dell'hook
|
|
306
|
+
# (solo l'invio HTTP è async), sopprimere l'accodamento chiude il loop anche cross-thread;
|
|
307
|
+
# * NON solleva: un hook difettoso è isolato (logga su internal_logger) e mai propagato nell'app.
|
|
308
|
+
# L'hook osserva sempre la configurazione CORRENTE: una notifica in volo che completa dopo una
|
|
309
|
+
# re-init raggiunge l'hook nuovo (best-effort, coerente col modello fire-and-forget).
|
|
310
|
+
def notify_diagnostic(event, **details)
|
|
311
|
+
hook = configuration.on_diagnostic
|
|
312
|
+
return nil if hook.nil?
|
|
313
|
+
return nil if Thread.current[DIAGNOSTIC_GUARD]
|
|
314
|
+
|
|
315
|
+
Thread.current[DIAGNOSTIC_GUARD] = true
|
|
316
|
+
begin
|
|
317
|
+
hook.call(event, details)
|
|
318
|
+
rescue StandardError => e
|
|
319
|
+
internal_logger.error("CloseYourIt diagnostic hook: #{e.class}: #{e.message}")
|
|
320
|
+
ensure
|
|
321
|
+
Thread.current[DIAGNOSTIC_GUARD] = false
|
|
322
|
+
end
|
|
323
|
+
nil
|
|
324
|
+
end
|
|
325
|
+
|
|
225
326
|
private
|
|
226
327
|
|
|
328
|
+
# Vero se il thread corrente sta eseguendo l'hook diagnostico: le API di telemetria diventano no-op
|
|
329
|
+
# per impedire che un hook (mal scritto) generi altra telemetria e inneschi auto-monitoraggio.
|
|
330
|
+
def in_diagnostic?
|
|
331
|
+
Thread.current[DIAGNOSTIC_GUARD] == true
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Registra uno scarto: incrementa il contatore aggregato `dropped` e notifica `:drop` con il motivo
|
|
335
|
+
# (queue_full/before_send/sampled/error). Un unico punto tiene allineati snapshot e hook.
|
|
336
|
+
def record_drop(reason, **details)
|
|
337
|
+
stats.increment(:dropped)
|
|
338
|
+
notify_diagnostic(:drop, reason: reason, **details)
|
|
339
|
+
nil
|
|
340
|
+
end
|
|
341
|
+
|
|
227
342
|
def client
|
|
343
|
+
ensure_current_process!
|
|
228
344
|
@client ||= Client.new(configuration)
|
|
229
345
|
end
|
|
230
346
|
|
|
231
347
|
def log_buffer
|
|
348
|
+
ensure_current_process!
|
|
232
349
|
@log_buffer ||= LogBuffer.new(client: client, configuration: configuration)
|
|
233
350
|
end
|
|
234
351
|
|
|
352
|
+
# Rileva un fork confrontando il PID del processo in cui @client/@log_buffer sono stati materializzati
|
|
353
|
+
# con quello corrente. In un figlio forkato i due oggetti sono ereditati dal padre, ma i loro thread
|
|
354
|
+
# — il worker pool di Client e il TimerTask di LogBuffer — vivono solo nel padre (il fork copia il
|
|
355
|
+
# solo thread chiamante): gli eventi accodati non partirebbero mai, restando affidati a thread che
|
|
356
|
+
# esistono soltanto nel padre. Al cambio di PID abbandoniamo i riferimenti ereditati SENZA #shutdown
|
|
357
|
+
# (nessun join possibile sui thread del padre, e un flush del buffer rispedirebbe eventi del padre) →
|
|
358
|
+
# i getter li ricreano lazy sotto il processo corrente. Nel padre lo stato resta invariato.
|
|
359
|
+
def ensure_current_process!
|
|
360
|
+
pid = Process.pid
|
|
361
|
+
return if @pid == pid
|
|
362
|
+
|
|
363
|
+
discard_inherited_client! if @pid # non alla prima materializzazione (@pid nil): nulla da abbandonare
|
|
364
|
+
@pid = pid
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Sgancia i riferimenti a client e log buffer senza drenarli. Usato sia dalla rilevazione lazy del
|
|
368
|
+
# fork sia da #after_fork: nel figlio i thread sottostanti non esistono, quindi non c'è nulla da
|
|
369
|
+
# joinare e non si deve flushare (rispedirebbe gli eventi del padre). Il GC raccoglie i vecchi
|
|
370
|
+
# oggetti; i getter ne creano di nuovi al prossimo accesso.
|
|
371
|
+
def discard_inherited_client!
|
|
372
|
+
@client = nil
|
|
373
|
+
@log_buffer = nil
|
|
374
|
+
end
|
|
375
|
+
|
|
235
376
|
# I log seguono il master switch del client + il proprio flag dedicato.
|
|
236
377
|
def logs_enabled?
|
|
237
378
|
enabled? && configuration.logs_enabled
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: closeyourit-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessio Bussolari
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: logger
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.6'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.6'
|
|
26
40
|
description: Gemma client che cattura eccezioni e le statistiche di query e metodi
|
|
27
41
|
lenti e le invia, fire-and-forget, all'endpoint di ingest di CloseYourIt.
|
|
28
42
|
email:
|
|
@@ -41,6 +55,7 @@ files:
|
|
|
41
55
|
- lib/closeyourit/configuration.rb
|
|
42
56
|
- lib/closeyourit/event.rb
|
|
43
57
|
- lib/closeyourit/events/error_event.rb
|
|
58
|
+
- lib/closeyourit/events/job_metric_event.rb
|
|
44
59
|
- lib/closeyourit/events/log_event.rb
|
|
45
60
|
- lib/closeyourit/events/message_event.rb
|
|
46
61
|
- lib/closeyourit/events/performance_issue_event.rb
|
|
@@ -65,9 +80,12 @@ files:
|
|
|
65
80
|
- lib/closeyourit/scope.rb
|
|
66
81
|
- lib/closeyourit/scrubber.rb
|
|
67
82
|
- lib/closeyourit/sidekiq/error_handler.rb
|
|
83
|
+
- lib/closeyourit/sidekiq/job_metrics_middleware.rb
|
|
68
84
|
- lib/closeyourit/stats.rb
|
|
85
|
+
- lib/closeyourit/subscribers/job_performance.rb
|
|
69
86
|
- lib/closeyourit/subscribers/request_performance.rb
|
|
70
87
|
- lib/closeyourit/subscribers/slow_query.rb
|
|
88
|
+
- lib/closeyourit/trace_context.rb
|
|
71
89
|
- lib/closeyourit/transport.rb
|
|
72
90
|
- lib/closeyourit/version.rb
|
|
73
91
|
homepage: https://github.com/bussolabs/closeyourit-ruby
|