Module: CloseYourIt

Defined in:
lib/closeyourit-ruby.rb,
lib/closeyourit/event.rb,
lib/closeyourit/scope.rb,
lib/closeyourit/stats.rb,
lib/closeyourit/client.rb,
lib/closeyourit/monitor.rb,
lib/closeyourit/version.rb,
lib/closeyourit/scrubber.rb,
lib/closeyourit/transport.rb,
lib/closeyourit/breadcrumb.rb,
lib/closeyourit/line_cache.rb,
lib/closeyourit/log_buffer.rb,
lib/closeyourit/log_device.rb,
lib/closeyourit/instrumenter.rb,
lib/closeyourit/configuration.rb,
lib/closeyourit/rails/railtie.rb,
lib/closeyourit/events/log_event.rb,
lib/closeyourit/background_worker.rb,
lib/closeyourit/breadcrumb_buffer.rb,
lib/closeyourit/events/error_event.rb,
lib/closeyourit/performance/rollup.rb,
lib/closeyourit/rails/query_source.rb,
lib/closeyourit/rails/request_body.rb,
lib/closeyourit/rails/log_broadcast.rb,
lib/closeyourit/events/message_event.rb,
lib/closeyourit/rails/net_http_patch.rb,
lib/closeyourit/rails/request_context.rb,
lib/closeyourit/sidekiq/error_handler.rb,
lib/closeyourit/rails/error_subscriber.rb,
lib/closeyourit/subscribers/slow_query.rb,
lib/closeyourit/events/job_metric_event.rb,
lib/closeyourit/events/slow_query_event.rb,
lib/closeyourit/events/slow_method_event.rb,
lib/closeyourit/rails/capture_exceptions.rb,
lib/closeyourit/rails/active_job_extension.rb,
lib/closeyourit/performance/request_profile.rb,
lib/closeyourit/subscribers/job_performance.rb,
lib/closeyourit/events/performance_issue_event.rb,
lib/closeyourit/sidekiq/job_metrics_middleware.rb,
lib/closeyourit/subscribers/request_performance.rb

Overview

CloseYourIt — client di telemetria (errori + statistiche di query/metodi lenti) che invia gli eventi all'endpoint di ingest di CloseYourIt.

Entry point della gemma (file con trattino come sentry-ruby): require "closeyourit-ruby" carica il modulo CloseYourIt.

Defined Under Namespace

Modules: Instrumenter, LineCache, Monitor, Performance, Rails, Sidekiq, Subscribers Classes: BackgroundWorker, Breadcrumb, BreadcrumbBuffer, Client, Configuration, Error, ErrorEvent, Event, JobMetricEvent, LogBuffer, LogDevice, LogEvent, MessageEvent, PerformanceIssueEvent, Scope, Scrubber, SlowMethodEvent, SlowQueryEvent, Stats, Transport

Constant Summary collapse

CAPTURED_FLAG =
:@__closeyourit_captured
DIAGNOSTIC_GUARD =

Flag thread-local che segna "sono già dentro l'hook diagnostico": impedisce che una notifica emessa DENTRO l'hook (o da codice da esso invocato) rientri e riesegua l'hook → niente loop di auto-monitoraggio (CYRB-12). È per-thread perché le tappe girano su thread diversi (worker pool per send/timeout, thread chiamante per enqueue/drop).

:__closeyourit_in_diagnostic
VERSION =
"0.7.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.internal_loggerObject

Logger interno della gemma (warning/errori diagnostici su stdout). NON è il logging applicativo: per spedire log strutturati a CloseYourIt usa CloseYourIt.log / CloseYourIt.logger.



177
178
179
# File 'lib/closeyourit-ruby.rb', line 177

def internal_logger
  @internal_logger ||= default_internal_logger
end

Class Method Details

.add_breadcrumb(message: nil, category: nil, type: "default", level: "info", data: {}) ⇒ Object

Aggiunge una briciola di contesto (query, navigazione, evento custom) all'evento corrente. No-op se breadcrumbs disabilitati; data viene scrubato (denylist) prima di essere salvato.



166
167
168
169
170
171
172
173
# File 'lib/closeyourit-ruby.rb', line 166

def add_breadcrumb(message: nil, category: nil, type: "default", level: "info", data: {})
  return nil unless configuration.breadcrumbs_enabled

  scrubbed = data.nil? || data.empty? ? data : Scrubber.new(configuration).filter_params(data)
  Scope.current.add_breadcrumb(
    Breadcrumb.new(message: message, category: category, type: type, level: level, data: scrubbed)
  )
end

.after_forkObject

Ripristina le risorse di invio in un processo figlio dopo un fork. Chiamalo dai worker hook dei server che forkano (Puma on_worker_boot, Sidekiq/Unicorn after_fork) per ricreare SUBITO worker pool e log buffer nel figlio, invece di attendere la rilevazione lazy al primo evento. Opzionale: la gemma rileva comunque il cambio PID da sé (vedi #ensure_current_process!). Idempotente e sicuro anche se client/buffer non sono ancora stati materializzati (→ no-op, verranno creati lazy).



285
286
287
288
289
# File 'lib/closeyourit-ruby.rb', line 285

def after_fork
  discard_inherited_client!
  @pid = Process.pid
  nil
end

.capture_event(event) ⇒ Object

Spedisce un evento già costruito (slow_query/slow_method).



109
110
111
112
113
114
# File 'lib/closeyourit-ruby.rb', line 109

def capture_event(event)
  return nil if in_diagnostic?
  return nil unless enabled?

  client.capture_event(event)
end

.capture_exception(exception, handled: false, level: "error", contexts: nil) ⇒ Object

Cattura un'eccezione e la spedisce (fire-and-forget). No-op se disabilitato, se l'eccezione è esclusa o già catturata.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/closeyourit-ruby.rb', line 93

def capture_exception(exception, handled: false, level: "error", contexts: nil)
  return nil if in_diagnostic?
  return nil unless enabled?
  return nil if ignored_exception?(exception)
  return nil if exception_captured?(exception)

  mark_captured(exception)
  return record_drop(:sampled) unless sampled?

  event = ErrorEvent.from_exception(
    exception, configuration: configuration, handled: handled, level: level, contexts: contexts
  )
  client.capture_event(event)
end

.capture_message(message, level: "info") ⇒ Object

Invia un messaggio diagnostico esplicito (non un'eccezione). Soggetto a sampling + scope. CloseYourIt.capture_message("cache miss storm", level: "warning")



118
119
120
121
122
123
124
125
# File 'lib/closeyourit-ruby.rb', line 118

def capture_message(message, level: "info")
  return nil if in_diagnostic?
  return nil unless enabled?
  return record_drop(:sampled) unless sampled?

  event = MessageEvent.new(message, level: level, configuration: configuration)
  client.capture_event(event)
end

.clear_scopeObject



160
161
162
# File 'lib/closeyourit-ruby.rb', line 160

def clear_scope
  Scope.reset!
end

.configurationObject



79
80
81
# File 'lib/closeyourit-ruby.rb', line 79

def configuration
  @configuration ||= Configuration.new
end

.configure_scope {|Scope.current| ... } ⇒ Object

Yields:



156
157
158
# File 'lib/closeyourit-ruby.rb', line 156

def configure_scope
  yield(Scope.current) if block_given?
end

.configured?Boolean

Returns:

  • (Boolean)


83
84
85
# File 'lib/closeyourit-ruby.rb', line 83

def configured?
  !@configuration.nil?
end

.emit_log(level, message, source: nil, attributes: {}) ⇒ Object

Percorso a basso livello (gating + costruzione + buffer) con sorgente e attributes SEPARATI e non-ambigui: usato da LogDevice per trattare una chiave logger come attributo dati (mai come sorgente) e per impostare la sorgente solo via .named (child logger, parità dart/js — CYRB-8). Le app usano CloseYourIt.log / CloseYourIt.logger.



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/closeyourit-ruby.rb', line 204

def emit_log(level, message, source: nil, attributes: {})
  return nil if in_diagnostic?
  return nil unless logs_enabled?
  return nil if log_below_min_level?(level)
  return record_drop(:sampled) unless logs_sampled?

  event = LogEvent.new(message, level: level, attributes: attributes,
                                logger: source, configuration: configuration)
  log_buffer.add(event)
  nil
end

.enabled?Boolean

Returns:

  • (Boolean)


87
88
89
# File 'lib/closeyourit-ruby.rb', line 87

def enabled?
  configuration.enabled?
end

.flush_logsObject

Forza l'invio dei log bufferizzati (chiamato anche allo shutdown del processo).



254
255
256
257
# File 'lib/closeyourit-ruby.rb', line 254

def flush_logs
  @log_buffer&.flush
  nil
end

.ignored_log_message?(text) ⇒ Boolean

Vero se una riga del broadcast Rails.logger va scartata: nomina un'eccezione già presente in excluded_exceptions, oppure combacia con excluded_log_patterns.

Serve perché un'eccezione esclusa dal canale ERRORI rientrava da quello dei LOG: Rails la registra con logger.error, il broadcast inoltrava la riga senza guardarla, e il rumore che excluded_exceptions aveva appena scartato ricompariva come log-entry. Il 2026-07-30 erano 48.000 voci su 49.985 nello stream, quasi tutte ActionController::RoutingError da favicon mancanti e scansioni di bot — che è nella lista di default dalla prima riga (CYRB-17).

ignored_exception? non è applicabile: qui la classe arriva come TESTO dentro il messaggio, non come oggetto con ancestors da confrontare. Da cui il match per sottostringa sui matcher String.

Vale SOLO per il mirror automatico di Rails.logger: un CloseYourIt.log scritto di proposito dallo sviluppatore non si silenzia mai (chi lo scrive ha già deciso che vuole quella riga).

Returns:

  • (Boolean)


236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/closeyourit-ruby.rb', line 236

def ignored_log_message?(text)
  text = text.to_s
  return false if text.empty?

  config = configuration
  named = config.excluded_exceptions.any? do |matcher|
    if matcher.is_a?(Regexp)
      matcher.match?(text)
    else
      # Un matcher vuoto combacerebbe con qualunque riga: mai silenziare tutto per una lista sporca.
      !matcher.empty? && text.include?(matcher)
    end
  end

  named || config.excluded_log_patterns.any? { |pattern| pattern.match?(text) }
end

.init {|@configuration| ... } ⇒ Object

Configura il client. Senza token/endpoint → no-op.

Una re-init SPEGNE prima il client e il log buffer della configurazione precedente (CYRB-10): azzerarli e basta lascerebbe orfani il thread del worker pool e il TimerTask del buffer, e gli eventi ancora in coda andrebbero persi o flushati fuori tempo dal timer orfano con la vecchia credenziale. Riusa la semantica di fine-vita di #shutdown (flush del buffer → drain del worker con timeout, CYRB-5) — così la coda precedente è svuotata in modo prevedibile con la sua config. Idempotente: alla prima init (o senza eventi catturati) @client/@log_buffer sono nil → no-op.

Yields:



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/closeyourit-ruby.rb', line 67

def init
  shutdown
  @configuration = Configuration.new
  @client = nil
  @log_buffer = nil
  @shutdown_notified = false # nuova sessione: :shutdown potrà essere notificato di nuovo
  yield(@configuration) if block_given?
  @configuration.validate!
  register_shutdown_flush
  @configuration
end

.log(level, message, logger: nil, **attributes) ⇒ Object

API esplicita: costruisce e bufferizza una voce di log strutturata (batch verso /logs, fire-and-forget). Il level è normalizzato ai livelli canonici (:warnwarning, downcase; ignoto→info). Il keyword logger: = nome della sorgente del log; ogni altra keyword è un attributo dati. I log sotto logs_min_level sono scartati con la stessa mappa numerica di dart/js (regola standardize, not adapt — CYRB-6): niente costruzione né invio. CloseYourIt.log(:info, "ordine creato", order_id: 1) CloseYourIt.log(:warn, "retry", logger: "payments", attempt: 3)



196
197
198
# File 'lib/closeyourit-ruby.rb', line 196

def log(level, message, logger: nil, **attributes)
  emit_log(level, message, source: logger, attributes: attributes)
end

.loggerObject

Logger applicativo Logger-compatibile: inoltra ogni messaggio a CloseYourIt.log (→ ingest /logs). CloseYourIt.logger.info("ordine creato", order_id: 1)



185
186
187
# File 'lib/closeyourit-ruby.rb', line 185

def logger
  @app_logger ||= LogDevice.new
end

.logs_active?Boolean

Vero se i log sono attivi (master switch + flag): usato da LogDevice per NON valutare i block costosi (logger.debug { dump }) quando il logging è spento.

Returns:

  • (Boolean)


218
219
220
# File 'lib/closeyourit-ruby.rb', line 218

def logs_active?
  logs_enabled?
end

.measure(label, &block) ⇒ Object

Cronometra un blocco e invia un slow_method se supera la soglia. CloseYourIt.measure("checkout.total") { ... }



129
130
131
# File 'lib/closeyourit-ruby.rb', line 129

def measure(label, &block)
  Instrumenter.measure(label, &block)
end

.notify_diagnostic(event, **details) ⇒ Object

Notifica una tappa del ciclo di vita di un evento all'hook on_diagnostic (se configurato). event è uno tra :enqueue, :send, :drop, :timeout, :shutdown; details un Hash privo di dati sensibili (es. { reason: :queue_full }, { status: 429 }). Chiamato da Client/Transport/ BackgroundWorker/LogBuffer. Garanzie (CYRB-12):

* NON invia telemetria: tocca solo l'hook dell'app e i contatori in-memory;
* NON innesca loop di auto-monitoraggio: durante l'hook il guard è alzato, e finché è alzato
sono soppressi SIA i `notify_diagnostic` annidati SIA le API di telemetria (`capture_*`/log,
vedi #in_diagnostic?). Poiché l'accodamento della telemetria è sincrono nel thread dell'hook
(solo l'invio HTTP è async), sopprimere l'accodamento chiude il loop anche cross-thread;
* NON solleva: un hook difettoso è isolato (logga su internal_logger) e mai propagato nell'app.

L'hook osserva sempre la configurazione CORRENTE: una notifica in volo che completa dopo una re-init raggiunge l'hook nuovo (best-effort, coerente col modello fire-and-forget).



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/closeyourit-ruby.rb', line 309

def notify_diagnostic(event, **details)
  hook = configuration.on_diagnostic
  return nil if hook.nil?
  return nil if Thread.current[DIAGNOSTIC_GUARD]

  Thread.current[DIAGNOSTIC_GUARD] = true
  begin
    hook.call(event, details)
  rescue StandardError => e
    internal_logger.error("CloseYourIt diagnostic hook: #{e.class}: #{e.message}")
  ensure
    Thread.current[DIAGNOSTIC_GUARD] = false
  end
  nil
end

.set_context(key, attributes) ⇒ Object



148
149
150
# File 'lib/closeyourit-ruby.rb', line 148

def set_context(key, attributes)
  Scope.current.set_context(key, attributes)
end

.set_extra(key, value) ⇒ Object



152
153
154
# File 'lib/closeyourit-ruby.rb', line 152

def set_extra(key, value)
  Scope.current.set_extra(key, value)
end

.set_tag(key, value) ⇒ Object



140
141
142
# File 'lib/closeyourit-ruby.rb', line 140

def set_tag(key, value)
  Scope.current.set_tag(key, value)
end

.set_tags(attributes) ⇒ Object



144
145
146
# File 'lib/closeyourit-ruby.rb', line 144

def set_tags(attributes)
  Scope.current.set_tags(attributes)
end

.set_user(attributes) ⇒ Object

--- Scope per-richiesta/job (user/tags/extra/contexts) --- Arricchiscono l'evento corrente; resettati a fine richiesta/job da middleware e estensioni.



136
137
138
# File 'lib/closeyourit-ruby.rb', line 136

def set_user(attributes)
  Scope.current.set_user(attributes)
end

.shutdownObject

Flush di fine-vita: svuota i log bufferizzati E drena il worker asincrono, attendendo (fino a un timeout breve) che gli invii in volo verso l'ingest completino. Solo flushare il buffer li accoderebbe nel worker che verrebbe poi ucciso alla terminazione → log persi (CYRB-5). Drena TUTTO il worker (log + errori + metriche fire-and-forget), non solo i log. Registrato su at_exit da CloseYourIt.init; con config.trap_signals viene raggiunto anche su SIGTERM. Idempotente: richiamarlo è sicuro (buffer già vuoto, worker già fermo → no-op).



265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/closeyourit-ruby.rb', line 265

def shutdown
  # Ordine critico: prima il buffer (accoda l'ultimo batch nel worker), poi il worker (lo drena).
  @log_buffer&.shutdown
  @client&.shutdown
  # Riepilogo di fine-vita: l'app riceve lo snapshot dei contatori senza log rumorosi. Emesso una
  # sola volta per sessione (uno shutdown esplicito seguito dall'at_exit non deve duplicarlo; il
  # flag è azzerato a ogni init). Lo snapshot è best-effort: eventuali invii ancora in volo oltre il
  # breve timeout di drain possono non esservi riflessi — il drain non blocca l'uscita (CYRB-5).
  unless @shutdown_notified
    @shutdown_notified = true
    notify_diagnostic(:shutdown, stats: stats.to_h)
  end
  nil
end

.statsObject

Contatori diagnostici del client (accodati/scartati/spediti/falliti/timeout). CloseYourIt.stats.to_h # => { enqueued: …, dropped: …, sent: …, failed: …, timeout: … }



293
294
295
# File 'lib/closeyourit-ruby.rb', line 293

def stats
  @stats ||= Stats.new
end