closeyourit-ruby
Client di telemetria per CloseYourIt. Una gemma che il tuo progetto Rails installa per inviare a CloseYourIt:
- Eccezioni → in formato evento Sentry, sul path Bearer
/api/v1/projects/:id/events. Finiscono nel error-tracker (raggruppate, triage, promote-to-ticket) come quelle di un SDK Sentry. - Query/metodi lenti → sul path
/api/v1/projects/:id/metrics(pipeline metriche dedicata, raggruppate per signature con aggregati di durata) — ciò che Sentry/GlitchTip non danno bene.
È il client primario di CloseYourIt: replica e migliora ciò che fa un SDK Sentry (request context, user/tag/contesto, breadcrumbs, errori nei background job, errori handled, sampling, messaggi) senza dipendere dagli SDK Sentry.
Caratteristiche:
- Contesto ricco sull'errore: request HTTP (method/url/header), user/tag/contesto custom, breadcrumbs (cronologia query offuscate prima del crash).
- Cattura automatica di: eccezioni non gestite (Rack), errori in ActiveJob/Sidekiq (oggi persi),
errori handled (
Rails.error.report), query/metodi lenti. capture_message, sampling (sample_rate), ignore per Regexp, release detection automatica.- Invio fire-and-forget: thread pool in background, non blocca la request, non crasha mai l'app.
- No-op se non configurata (sicura in sviluppo/test).
- Privacy-by-default: niente PII a meno di abilitarla esplicitamente.
Design e contratto dati in
PDR.md.
Installazione
Pubblicata su RubyGems:
# Gemfile del progetto da monitorare
gem "closeyourit-ruby"
# in sviluppo locale, dal checkout:
# gem "closeyourit-ruby", path: "../closeyourit-ruby"
Ottenere credenziali
In CloseYourIt, area Member → Project → tokens, crea un token: ottieni il Bearer secret (mostrato una volta) e l'UUID del progetto. Servono entrambi alla gemma.
Configurazione
# config/initializers/closeyourit.rb
CloseYourIt.init do |c|
c.endpoint_url = ENV["CLOSEYOURIT_ENDPOINT_URL"] # es. https://www.closeyour.it (host canonico)
c.token = ENV["CLOSEYOURIT_TOKEN"] # Bearer secret del Projects::Token
c.project_id = ENV["CLOSEYOURIT_PROJECT_ID"] # UUID del progetto su CloseYourIt
c.environment = Rails.env
end
Senza endpoint_url / token / project_id la gemma è no-op (nessun invio, nessun overhead). In
production un endpoint_url http:// viene rifiutato (no-op + warning): il token viaggerebbe in chiaro.
Endpoint: usa l'host canonico
https://www.closeyour.it. La gemma segue fino a 2 redirect su POST (preservando metodo e body), quindi anche l'apexhttps://closeyour.it(301 → www) funziona — ma puntare direttamente a www risparmia un hop per ogni evento.
Opzioni
| Opzione | Default | Descrizione |
|---|---|---|
endpoint_url |
ENV["CLOSEYOURIT_ENDPOINT_URL"] |
URL base (la gemma appende i path per-progetto) |
token |
ENV["CLOSEYOURIT_TOKEN"] |
Bearer secret del progetto |
project_id |
ENV["CLOSEYOURIT_PROJECT_ID"] |
UUID progetto (nel path di ingest) |
release |
ENV["CLOSEYOURIT_RELEASE"] |
Versione riportata negli errori (opzionale) |
environment |
Rails.env / RACK_ENV / "development" |
Ambiente riportato negli eventi |
excluded_exceptions |
RoutingError, RecordNotFound |
Eccezioni da NON inviare — String (nome classe) o Regexp (match su nome/messaggio) |
before_send |
nil |
->(payload) { ... } — scrub finale; ritorna payload o nil per scartare |
sample_rate |
1.0 |
Frazione di errori/messaggi inviata (1.0 tutto, 0.0 niente) |
async_threads |
cpu/2 |
Thread di invio; 0 = sincrono (test) |
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) |
slow_query_threshold_ms |
100 |
Soglia query lente |
slow_method_threshold_ms |
200 |
Soglia metodi lenti |
capture_request |
true |
Cattura il contesto HTTP della richiesta (method/url/header allowlist) |
request_header_allowlist |
Accept, Content-Type, User-Agent, Referer |
Header inviati (mai Authorization/Cookie) |
breadcrumbs_enabled |
true |
Cronologia (query offuscate + add_breadcrumb) allegata all'errore |
max_breadcrumbs |
100 |
Dimensione max del ring buffer breadcrumbs |
capture_handled_errors |
true |
Cattura gli errori riportati via Rails.error.report |
report_active_job_errors |
true |
Cattura gli errori dei job ActiveJob/Solid Queue/Sidekiq |
send_pii |
false |
Master switch PII |
obfuscate_sql |
true |
Maschera i literal nello SQL |
filter_parameters |
[] |
Chiavi extra da redarre (mergiate con quelle di Rails) |
scrub_message_patterns |
[] |
Regexp da redarre da messaggi d'eccezione e da message dei log |
logs_enabled |
true |
Master switch dello stream di log strutturato |
logs_min_level |
:info |
Soglia minima: i log sotto (es. debug) non vengono inviati |
logs_sample_rate |
1.0 |
Frazione di log inviata (1.0 tutto, 0.0 niente) |
logs_batch_size |
50 |
Log accumulati nel buffer prima di un flush a batch verso /logs |
logs_flush_interval |
5 |
Secondi tra i flush periodici del buffer log |
capture_rails_logs |
false |
Inoltra anche Rails.logger allo stream — attenzione al volume (soglia capture_rails_logs_min_level) |
capture_rails_logs_min_level |
:warn |
Soglia dedicata del broadcast Rails.logger (distinta da logs_min_level): default conservativo per non inondare lo stream col rumore info del framework |
detect_performance_issues |
false |
Master switch dei verdetti performance (N+1, query count, request/HTTP esterne lente). Opt-in: profila ogni query → overhead (vedi Performance issues) |
n_plus_one_threshold |
10 |
Ripetizioni dello stesso [fingerprint, call-site] in una richiesta oltre cui = n_plus_one |
query_count_threshold |
100 |
Query totali in una richiesta oltre cui = high_query_count |
query_time_threshold_ms |
500 |
Tempo DB totale per richiesta (ms) oltre cui = high_query_count |
slow_request_threshold_ms |
1000 |
Durata totale della richiesta (ms) oltre cui = slow_request |
slow_external_threshold_ms |
1000 |
Durata di una singola HTTP esterna (ms) oltre cui = slow_external_http |
capture_external_http |
true |
Strumenta Net::HTTP per rilevare le HTTP esterne (effettivo solo con detect_performance_issues) |
Cosa cattura
Eccezioni (automatico, con Rails) → error-tracker
Il Railtie inserisce un Rack middleware che cattura le eccezioni non gestite, le invia come evento
Sentry (exception.values[], level, event_id, stacktrace) e le ri-solleva (l'app continua a
gestirle come prima). Cattura manuale ovunque:
begin
rischioso!
rescue => e
CloseYourIt.capture_exception(e)
raise
end
Request context (automatico, con Rails)
Un Rack middleware allega a ogni evento il contesto HTTP della richiesta: method, url (senza
query string) e gli header dell'allowlist (request_header_allowlist, mai Authorization/Cookie).
Query string e IP solo con send_pii. Lo scope è resettato a fine richiesta (niente bleed tra request).
Background job + errori handled (automatico, con Rails)
- ActiveJob / Solid Queue / Sidekiq: gli errori dei job (prima persi) vengono catturati con tag
job.class/job.queuee contesto del job, poi ri-sollevati. Rails.error.report(ActiveSupport ErrorReporter): gli errori handled vengono inviati conmechanism.handled = truee illevelmappato dalla severity. La dedup garantisce un solo invio anche se la stessa eccezione passa da più punti (Rack + job + reporter).
Log strutturati (manuale/automatico) → stream log
Stream di log strutturati (level/message/attributes/logger) separato dagli errori, correlato via
trace_id (stesso della richiesta). Bufferizzato e spedito a batch a /logs. message e
attributes passano dallo scrubber (denylist chiavi + scrub_message_patterns) come gli errori.
# API esplicita — il keyword `logger:` è la sorgente del log; ogni altra keyword è un attributo
CloseYourIt.log(:info, "ordine creato", order_id: order.id, amount: order.amount)
CloseYourIt.log(:warning, "retry pagamento", attempt: 2)
# Interfaccia ::Logger-like (level via metodo)
CloseYourIt.logger.warn("cache miss", key: cache_key)
CloseYourIt.logger.error("job fallito", job: self.class.name)
La sorgente del log (campo logger) si imposta con .named — child logger immutabile, stesso
idioma di dart/js. Il logger radice resta invariato:
payments = CloseYourIt.logger.named("payments") # child con sorgente "payments"
payments.warn("retry", attempt: 3) # logger="payments", attributes={attempt: 3}
Sull'interfaccia .logger una chiave logger: è sempre un attributo dati (non viene hijackata
come sorgente): CloseYourIt.logger.warn("disco pieno", logger: "/dev/sda1") mette logger negli
attributes. Sorgente (.named) e attributo logger possono coesistere senza collidere.
Ogni log sotto logs_min_level (default :info) è scartato prima di essere costruito o spedito —
CloseYourIt.log e CloseYourIt.logger.* usano la mappa di livelli (debug < info < warning <
error < fatal), identica agli SDK dart/js. Un CloseYourIt.logger.debug(...) in un loop caldo con
la soglia di default non produce alcun invio.
Con Rails, capture_rails_logs = true inoltra anche Rails.logger allo stream, ma con una soglia
dedicata capture_rails_logs_min_level (default :warn, distinta da logs_min_level): in
produzione ad alto traffico Rails logga a :info ogni richiesta (Started GET, Rendered, ...) e un
default a :info inonderebbe lo stream col rumore del framework — la soglia conservativa lo evita.
Abbassala esplicitamente (es. config.capture_rails_logs_min_level = :info) solo se vuoi anche gli
info del framework. I log dei background job ereditano il trace_id = job_id, così log ed errore
dello stesso job si correlano.
Flush di fine-vita
Il buffer log viene svuotato automaticamente allo shutdown del processo: CloseYourIt.init registra
un at_exit che drena anche il worker asincrono (attende gli invii in volo, non solo il flush del
buffer), così i log sotto-batch di rake/CLI non vanno persi all'uscita. SIGTERM (deploy/Kamal)
termina il processo senza eseguire gli at_exit: imposta trap_signals = true per intercettarlo e
convertirlo in un exit pulito, oppure chiama CloseYourIt.shutdown esplicitamente prima di terminare.
Contesto, breadcrumbs e messaggi (manuale)
CloseYourIt.set_user(id: account.id) # solo id; email/ip solo se send_pii
CloseYourIt.set_tag(:tenant, current_tenant.slug)
CloseYourIt.set_context(:billing, { plan: "pro" })
CloseYourIt.set_extra(:cart_size, cart.size)
CloseYourIt.configure_scope { |s| s.set_tag(:area, "checkout") }
CloseYourIt.(message: "coupon applicato", category: "ui") # cronologia pre-crash
CloseYourIt.("cache miss storm", level: "warning") # messaggio diagnostico
Lo scope (user/tag/contesto/breadcrumbs) è per-richiesta/job e viene allegato automaticamente all'evento d'errore catturato nello stesso contesto di esecuzione.
Query lente (automatico, con Rails) → metriche
Il Railtie si iscrive a sql.active_record: ogni query oltre slow_query_threshold_ms (esclusi
SCHEMA/CACHE) viene inviata come slow_query alla pipeline metriche. Lo SQL è offuscato (bind esclusi).
Metodi lenti → metriche
# Blocco ad-hoc
CloseYourIt.measure("checkout.total") do
calcolo_pesante
end
# Macro su un metodo (wrap automatico, firma e valore di ritorno invariati)
class Report
include CloseYourIt::Monitor
def generate(...) = ...
monitor :generate
end
Viene inviato un slow_method solo se la durata supera slow_method_threshold_ms. Gli argomenti
del metodo non vengono mai inviati (solo label, durata, file:riga).
Performance issues (opt-in)
slow_query e slow_method sono metriche puntuali (una query/un metodo lenti). Oltre a queste,
con detect_performance_issues = true la gemma profila ogni query della richiesta
(fingerprint + call-site) e, a fine richiesta, emette 0..N verdetti performance_issue sulla
pipeline metriche (/api/v1/projects/:id/metrics). Un verdetto non è una singola query lenta: è un
pattern dell'intera richiesta, aggregato e correlato a log/errori via trace_id. I quattro
subtype:
n_plus_one— lo stesso[fingerprint SQL, call-site]eseguito più din_plus_one_thresholdvolte (default 10) nella stessa richiesta: il classico N+1. Un verdetto per gruppo, con lo SQL già offuscato e ilsource(file:riga) che lo ha originato.high_query_count— troppe query in una richiesta: numero totale oltrequery_count_threshold(default 100) oppure tempo DB totale oltrequery_time_threshold_ms(default 500 ms).slow_request— durata totale della richiesta oltreslow_request_threshold_ms(default 1000 ms), conroute,query_counte tempo DB come contesto.slow_external_http— una singola chiamata HTTP esterna (Net::HTTP) oltreslow_external_threshold_ms(default 1000 ms). Richiedecapture_external_http(defaulttrue, che strumentaNet::HTTPsolo quando la detection è attiva); host e path templatizzato, mai la query string.
CloseYourIt.init do |c|
# …endpoint_url / token / project_id…
c.detect_performance_issues = true
end
Con Rails il wiring è automatico (il Railtie si iscrive a process_action.action_controller):
attivato il master switch, non serve altro codice. Le soglie sopra sono conservative (poco rumore) e
tutte ritoccabili.
Overhead — perché è opt-in (default OFF). Per aggregare i verdetti la detection profila ogni query della richiesta (fingerprint + call-site): un costo non trascurabile, da attivare consapevolmente per-app.
slow_query/slow_methodrestano attive e indipendenti da questo switch. (Pendant server-side dei verdettiperformance_issuedi closeyourit-js: qui isubtypesono quelli del backend Rails — N+1, query count, request/HTTP esterne lente.)
Privacy & PII
Privacy-by-default (send_pii = false). In sintesi:
- SQL: inviato il template, mai i bind values;
obfuscate_sqlmaschera anche i literal inline. - Chiavi sensibili (
password,token,authorization,cookie,secret,api_key,csrf,credit_card,cvv,ssn,iban, …) →[FILTERED]; estendibili confilter_parameters. - Messaggi d'eccezione: inviati per il debug, ma redigibili con
scrub_message_patterns/before_send. - Mai inviati: variabili locali dei frame, argomenti dei metodi, IP/cookie/Authorization, token (che viaggia solo nell'header su HTTPS).
Rischio residuo:
exception.valuee i nomi tabella/colonna nello SQL possono contenere dati di dominio. Usabefore_send/scrub_message_patternsper azzerarli. Dettaglio inPDR.md§9.
Diagnostica
Il trasporto è fire-and-forget: non solleva mai e non blocca la request. Per non lasciare
fallimenti silenziosi, ogni risposta HTTP non-2xx (es. 401 token errato, 404 progetto
inesistente) viene loggata a warn, così come gli eventi scartati a coda piena. I contatori
sono ispezionabili a runtime:
CloseYourIt.stats.to_h
# => { enqueued: 128, dropped: 0, sent: 126, failed: 2 }
enqueued— eventi accettati per l'inviodropped— scartati perché la coda async era piena (mai backpressure)sent— risposta HTTP 2xxfailed— errore di rete o status non-2xx (vedi i log awarn)
Sviluppo
bundle install
bundle exec rspec # test (WebMock, niente rete reale)
bundle exec rubocop # lint (omakase)
COVERAGE_ENFORCE=1 bundle exec rspec # gate coverage ≥90% line