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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1482102089ba9e319a50f2d3822b63fc76ddc9a63c6c629f1d7cdc53022ca362
|
|
4
|
+
data.tar.gz: f312feb36b22702dca3109c62f623ba4484d25caf82dc9b25c807fe89f2ea2ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f6a4d467f80966599d057452927a75c17126cb01def6de0d1e365cb0fa5dcbd458122e3b860b05906d35bb307b29eed0d8e9ee9b5afb1b4008170dd3462205d
|
|
7
|
+
data.tar.gz: 6f4a5ee0fb8dea5b160c1447624d1f409a1a36df63700c46dc66390c9d4fc2ab932e04a1c50c2dacec9c0202f73af7b697250786bfd52f8d6303fdaaebfdbff8
|
data/README.md
CHANGED
|
@@ -74,6 +74,7 @@ end
|
|
|
74
74
|
| `async_threads` | `cpu/2` | Thread di invio; `0` = sincrono (test) |
|
|
75
75
|
| `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)) |
|
|
76
76
|
| `slow_query_threshold_ms` | `100` | Soglia query lente |
|
|
77
|
+
| `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 |
|
|
77
78
|
| `slow_method_threshold_ms` | `200` | Soglia metodi lenti |
|
|
78
79
|
| `capture_request` | `true` | Cattura il contesto HTTP della richiesta (method/url/header allowlist) |
|
|
79
80
|
| `request_header_allowlist` | `Accept`, `Content-Type`, `User-Agent`, `Referer` | Header inviati (mai Authorization/Cookie) |
|
|
@@ -92,6 +93,7 @@ end
|
|
|
92
93
|
| `logs_flush_interval` | `5` | Secondi tra i flush periodici del buffer log |
|
|
93
94
|
| `capture_rails_logs` | `false` | Inoltra anche `Rails.logger` allo stream — attenzione al volume (soglia `capture_rails_logs_min_level`) |
|
|
94
95
|
| `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 |
|
|
96
|
+
| `excluded_log_patterns` | `[]` | Righe del broadcast `Rails.logger` da scartare per **testo** — **Regexp** (pattern) o **String** (testo letterale). Per il rumore che non è un'eccezione; le eccezioni le copre già `excluded_exceptions` |
|
|
95
97
|
| `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](#performance-issues-opt-in)) |
|
|
96
98
|
| `n_plus_one_threshold` | `10` | Ripetizioni dello stesso `[fingerprint, call-site]` in una richiesta oltre cui = `n_plus_one` |
|
|
97
99
|
| `query_count_threshold` | `100` | Query totali in una richiesta oltre cui = `high_query_count` |
|
|
@@ -99,6 +101,29 @@ end
|
|
|
99
101
|
| `slow_request_threshold_ms` | `1000` | Durata totale della richiesta (ms) oltre cui = `slow_request` |
|
|
100
102
|
| `slow_external_threshold_ms` | `1000` | Durata di una singola HTTP esterna (ms) oltre cui = `slow_external_http` |
|
|
101
103
|
| `capture_external_http` | `true` | Strumenta `Net::HTTP` per rilevare le HTTP esterne (effettivo solo con `detect_performance_issues`) |
|
|
104
|
+
| `monitor_jobs` | `true` | Misura durata e attesa in coda dei background job (ActiveJob + Sidekiq) → metriche `slow_job`/`job_queue_latency` (vedi [Metriche dei background job](#metriche-dei-background-job)) |
|
|
105
|
+
| `slow_job_threshold_ms` | `5000` | Durata del job (ms) oltre cui = `slow_job` |
|
|
106
|
+
| `job_queue_latency_threshold_ms` | `60000` | Attesa in coda (enqueue→esecuzione, ms) oltre cui = `job_queue_latency` |
|
|
107
|
+
| `jobs_sample_rate` | `1.0` | Frazione dei job oltre soglia effettivamente inviata (`1.0` tutti, `0.0` niente) |
|
|
108
|
+
| `propagate_trace_context` | `false` | Propaga il **trace context W3C** (`traceparent`/`tracestate`) alle chiamate `Net::HTTP` e mappa il `trace_id` degli eventi sul trace-id W3C entrante (vedi [Propagazione W3C](#propagazione-w3c-opt-in)) |
|
|
109
|
+
| `trace_propagation_allowlist` | `[]` | Host autorizzati a ricevere gli header W3C — **String** (host esatto, case-insensitive) o **Regexp** (sottodomini). Vuoto = nessuna destinazione |
|
|
110
|
+
|
|
111
|
+
## Propagazione W3C (opt-in)
|
|
112
|
+
|
|
113
|
+
Correla le richieste Ruby e le chiamate in uscita con lo standard **[W3C Trace Context](https://www.w3.org/TR/trace-context/)**
|
|
114
|
+
(`traceparent`/`tracestate`) — nessun formato proprietario. È un *propagation bridge*, non un tracer
|
|
115
|
+
completo: adotta il contesto entrante o ne genera uno root, e lo inoltra alle chiamate `Net::HTTP`.
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
CloseYourIt.init do |c|
|
|
119
|
+
c.propagate_trace_context = true
|
|
120
|
+
c.trace_propagation_allowlist = [ "api.interno.example", /\.svc\.internal\z/ ]
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- **Ingresso**: un `traceparent` entrante valido diventa il `trace_id` degli eventi CloseYourIt → l'errore/metrica della richiesta si allinea alla traccia distribuita. Un header malformato viene ignorato (si genera un root); un servizio d'origine (senza header) parte comunque con un contesto W3C.
|
|
125
|
+
- **Uscita**: gli header vengono iniettati **solo** verso gli host della `trace_propagation_allowlist` (limitata per destinazione) e mai verso l'endpoint CloseYourIt stesso. Un redirect verso un host non elencato non riceve nulla.
|
|
126
|
+
- **Privacy**: verso host non autorizzati non parte alcun header; l'header `baggage` (che può portare contesto interno/PII) **non viene mai** emesso.
|
|
102
127
|
|
|
103
128
|
## Cosa cattura
|
|
104
129
|
|
|
@@ -172,6 +197,21 @@ Abbassala esplicitamente (es. `config.capture_rails_logs_min_level = :info`) sol
|
|
|
172
197
|
`info` del framework. I log dei background job ereditano il `trace_id = job_id`, così log ed errore
|
|
173
198
|
dello stesso job si correlano.
|
|
174
199
|
|
|
200
|
+
Il broadcast rispetta anche **`excluded_exceptions`**: se la riga nomina una classe esclusa non viene
|
|
201
|
+
inoltrata. Serve perché un'eccezione tolta dal canale errori rientrava da quello dei log — Rails la
|
|
202
|
+
registra comunque con `logger.error`. Con le esclusioni di default questo copre già
|
|
203
|
+
`ActionController::RoutingError`, cioè le favicon mancanti e le scansioni dei bot, che in produzione
|
|
204
|
+
sono la voce di gran lunga più numerosa dello stream. Per il rumore ripetuto che **non** è
|
|
205
|
+
un'eccezione c'è `excluded_log_patterns`:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
config.excluded_log_patterns = [ /SolidQueue-[\d.]+ Error in thread/, "Rendered layout" ]
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Le stringhe valgono come testo letterale (vengono escapate), i `Regexp` come pattern. Entrambe le
|
|
212
|
+
liste gatano **solo** il mirror di `Rails.logger`: un `CloseYourIt.log` scritto di proposito non viene
|
|
213
|
+
mai silenziato.
|
|
214
|
+
|
|
175
215
|
#### Flush di fine-vita
|
|
176
216
|
|
|
177
217
|
Il buffer log viene svuotato automaticamente allo **shutdown del processo**: `CloseYourIt.init` registra
|
|
@@ -180,6 +220,31 @@ buffer), così i log sotto-batch di rake/CLI non vanno persi all'uscita. `SIGTER
|
|
|
180
220
|
termina il processo **senza** eseguire gli `at_exit`: imposta `trap_signals = true` per intercettarlo e
|
|
181
221
|
convertirlo in un exit pulito, oppure chiama `CloseYourIt.shutdown` esplicitamente prima di terminare.
|
|
182
222
|
|
|
223
|
+
#### Server che forkano (Puma cluster, Sidekiq, Unicorn)
|
|
224
|
+
|
|
225
|
+
I server che precaricano l'app (`preload_app!`) inizializzano la gemma nel processo **master**, poi
|
|
226
|
+
forkano i worker. Il worker pool asincrono e il timer del buffer log sono **thread**, e i thread non
|
|
227
|
+
sopravvivono a un `fork`: nel figlio quelle risorse sarebbero ereditate ma inerti e gli eventi non
|
|
228
|
+
partirebbero mai. La gemma **rileva il cambio di PID da sé** (lazy, al primo evento del figlio) e ricrea
|
|
229
|
+
worker e buffer nel processo corrente — nella maggior parte dei casi non devi fare nulla.
|
|
230
|
+
|
|
231
|
+
Per ricrearle **subito** dopo il fork (invece di attendere il primo evento) chiama
|
|
232
|
+
`CloseYourIt.after_fork` nel worker hook del tuo server:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
# Puma (config/puma.rb)
|
|
236
|
+
on_worker_boot { CloseYourIt.after_fork }
|
|
237
|
+
|
|
238
|
+
# Sidekiq (config/initializers/sidekiq.rb)
|
|
239
|
+
Sidekiq.configure_server { |config| config.on(:startup) { CloseYourIt.after_fork } }
|
|
240
|
+
|
|
241
|
+
# Unicorn (config/unicorn.rb)
|
|
242
|
+
after_fork { |server, worker| CloseYourIt.after_fork }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`CloseYourIt.after_fork` abbandona le risorse ereditate **senza drenarle** (non tenta un join sui thread
|
|
246
|
+
del padre e non riflusha il buffer, che rispedirebbe eventi del padre) ed è sicuro da chiamare più volte.
|
|
247
|
+
|
|
183
248
|
### Contesto, breadcrumbs e messaggi (manuale)
|
|
184
249
|
|
|
185
250
|
```ruby
|
|
@@ -201,6 +266,21 @@ all'evento d'errore catturato nello stesso contesto di esecuzione.
|
|
|
201
266
|
Il Railtie si iscrive a `sql.active_record`: ogni query oltre `slow_query_threshold_ms` (esclusi
|
|
202
267
|
`SCHEMA`/`CACHE`) viene inviata come `slow_query` alla pipeline metriche. Lo SQL è **offuscato** (bind esclusi).
|
|
203
268
|
|
|
269
|
+
Sono escluse anche le query che combaciano con `excluded_query_patterns` — di default le tabelle di
|
|
270
|
+
servizio del Solid stack (`solid_queue_`, `solid_cache_`, `solid_cable_`). Sono infrastruttura del
|
|
271
|
+
framework: una loro query lenta non si corregge leggendo il proprio repo, dice solo che il database è
|
|
272
|
+
in contesa, e in compenso sommerge le query dell'applicazione (su un backend reale erano il **62%**
|
|
273
|
+
dei campioni). Per misurarle davvero azzera la lista:
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
config.excluded_query_patterns = [] # misura tutto
|
|
277
|
+
config.excluded_query_patterns += [ "pg_stat_activity" ] # o aggiungi le tue
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Il filtro vale solo per la **misura**: le breadcrumb «quali query prima del crash» e il profiling N+1
|
|
281
|
+
continuano a vedere tutte le query, perché lì una tabella di servizio letta molte volte è essa stessa
|
|
282
|
+
un sintomo.
|
|
283
|
+
|
|
204
284
|
### Metodi lenti → metriche
|
|
205
285
|
|
|
206
286
|
```ruby
|
|
@@ -257,6 +337,36 @@ tutte ritoccabili.
|
|
|
257
337
|
> switch. (Pendant server-side dei verdetti `performance_issue` di closeyourit-js: qui i `subtype`
|
|
258
338
|
> sono quelli del backend Rails — N+1, query count, request/HTTP esterne lente.)
|
|
259
339
|
|
|
340
|
+
## Metriche dei background job
|
|
341
|
+
|
|
342
|
+
Oltre agli **errori** dei job (sezione precedente), la gemma misura **automaticamente** durata di
|
|
343
|
+
esecuzione e attesa in coda dei background job e, oltre le soglie, invia metriche `performance_issue`
|
|
344
|
+
sulla pipeline metriche (`/api/v1/projects/:id/metrics`). Funziona con **ActiveJob** (via notifiche
|
|
345
|
+
`ActiveSupport::Notifications`) e con **Sidekiq** (server middleware), senza strumentazione manuale.
|
|
346
|
+
Due `subtype`:
|
|
347
|
+
|
|
348
|
+
- **`slow_job`** — la durata del `perform` supera `slow_job_threshold_ms` (default 5000 ms).
|
|
349
|
+
- **`job_queue_latency`** — l'attesa in coda (dall'enqueue all'inizio dell'esecuzione) supera
|
|
350
|
+
`job_queue_latency_threshold_ms` (default 60000 ms): coda intasata o worker insufficienti.
|
|
351
|
+
|
|
352
|
+
Ogni metrica porta la **label** (nome della classe del job), la **queue**, l'**adapter**
|
|
353
|
+
(`active_job`/`sidekiq`), l'**attempt** (numero di esecuzione: rende visibili i **retry**), la
|
|
354
|
+
**release** e il **trace_id** (= `job_id` / `jid`, che correla la metrica a log ed errori dello stesso
|
|
355
|
+
job). **Gli argomenti del job non vengono mai inviati** (solo la label).
|
|
356
|
+
|
|
357
|
+
È **ON di default** (a differenza dei verdetti performance opt-in): la visibilità dei job lenti, in
|
|
358
|
+
ritardo o ripetutamente ritentati non deve richiedere configurazione in ogni app, e l'overhead è basso
|
|
359
|
+
(una notifica per job, non il profiling di ogni query). Il rumore è tenuto a bada dalle **soglie** (i
|
|
360
|
+
job normali restano sotto soglia → nessuna metrica) e dal **sampling** (`jobs_sample_rate`, applicato
|
|
361
|
+
solo ai job già oltre soglia). Per spegnerlo del tutto:
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
CloseYourIt.init do |c|
|
|
365
|
+
# …endpoint_url / token / project_id…
|
|
366
|
+
c.monitor_jobs = false
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
260
370
|
## Privacy & PII
|
|
261
371
|
|
|
262
372
|
Privacy-by-default (`send_pii = false`). In sintesi:
|
|
@@ -277,17 +387,45 @@ Privacy-by-default (`send_pii = false`). In sintesi:
|
|
|
277
387
|
Il trasporto è fire-and-forget: non solleva mai e non blocca la request. Per non lasciare
|
|
278
388
|
fallimenti silenziosi, ogni risposta HTTP non-2xx (es. `401` token errato, `404` progetto
|
|
279
389
|
inesistente) viene loggata a `warn`, così come gli eventi scartati a coda piena. I contatori
|
|
280
|
-
sono ispezionabili a runtime:
|
|
390
|
+
sono ispezionabili a runtime (`snapshot` è un alias di `to_h`):
|
|
281
391
|
|
|
282
392
|
```ruby
|
|
283
393
|
CloseYourIt.stats.to_h
|
|
284
|
-
# => { enqueued: 128, dropped:
|
|
394
|
+
# => { enqueued: 128, dropped: 1, sent: 125, failed: 2, timeout: 1 }
|
|
285
395
|
```
|
|
286
396
|
|
|
287
397
|
- `enqueued` — eventi accettati per l'invio
|
|
288
|
-
- `dropped` — scartati
|
|
398
|
+
- `dropped` — scartati prima dell'invio: coda async piena, `before_send` → `nil`/eccezione, sampling
|
|
289
399
|
- `sent` — risposta HTTP 2xx
|
|
290
400
|
- `failed` — errore di rete o status non-2xx (vedi i log a `warn`)
|
|
401
|
+
- `timeout` — sotto-conteggio di `failed`: fallimenti per timeout di rete (connettività)
|
|
402
|
+
|
|
403
|
+
### Hook `on_diagnostic` (opt-in)
|
|
404
|
+
|
|
405
|
+
Per osservare il ciclo di vita di ogni evento senza abilitare log rumorosi, registra un hook. È
|
|
406
|
+
invocato a ogni tappa — `:enqueue`, `:send`, `:drop`, `:timeout`, `:shutdown` — con dettagli privi
|
|
407
|
+
di dati sensibili:
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
CloseYourIt.init do |c|
|
|
411
|
+
# ...
|
|
412
|
+
c.on_diagnostic = ->(event, details) do
|
|
413
|
+
StatsD.increment("closeyourit.#{event}", tags: details) # es. inoltra a metriche interne
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
- `:enqueue` — `{ path: … }` accodato per l'invio
|
|
419
|
+
- `:send` — `{ status: 202 }` consegnato (2xx)
|
|
420
|
+
- `:drop` — scartato: `{ reason: :queue_full | :before_send | :sampled | :error | :response | :network, … }`
|
|
421
|
+
- `:timeout` — `{ error: "Net::OpenTimeout" }` timeout di rete
|
|
422
|
+
- `:shutdown` — `{ stats: {…} }` snapshot alla terminazione del processo, emesso una sola volta per
|
|
423
|
+
sessione (best-effort: invii ancora in volo oltre il breve timeout di drain possono non esservi
|
|
424
|
+
riflessi — il drain non blocca l'uscita)
|
|
425
|
+
|
|
426
|
+
L'hook è **locale e non ricorsivo**: non invia mai telemetria, e una notifica emessa dal suo interno
|
|
427
|
+
è soppressa — non può innescare loop di auto-monitoraggio. Un hook che solleva è isolato (loggato,
|
|
428
|
+
mai propagato nell'app). Tienilo veloce: gira sincrono nel thread chiamante, come `before_send`.
|
|
291
429
|
|
|
292
430
|
## Sviluppo
|
|
293
431
|
|
data/lib/closeyourit/client.rb
CHANGED
|
@@ -21,11 +21,19 @@ module CloseYourIt
|
|
|
21
21
|
def capture_event(event)
|
|
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).
|
|
26
|
+
CloseYourIt.stats.increment(:dropped)
|
|
27
|
+
CloseYourIt.notify_diagnostic(:drop, reason: :before_send)
|
|
28
|
+
return nil
|
|
29
|
+
end
|
|
25
30
|
|
|
26
31
|
path = event.ingest_path(@configuration.project_id)
|
|
27
32
|
accepted = @worker.perform { @transport.send_event(payload, path: path) }
|
|
28
|
-
|
|
33
|
+
if accepted
|
|
34
|
+
CloseYourIt.stats.increment(:enqueued)
|
|
35
|
+
CloseYourIt.notify_diagnostic(:enqueue, path: path)
|
|
36
|
+
end
|
|
29
37
|
payload
|
|
30
38
|
rescue StandardError => e
|
|
31
39
|
# La telemetria non deve MAI propagare nel path dell'app ospite: capture_event è invocato dal
|
|
@@ -33,6 +41,8 @@ module CloseYourIt
|
|
|
33
41
|
# malformato) e before_send sono valutati qui in modo sincrono → se sollevano, assorbiamo,
|
|
34
42
|
# logghiamo e scartiamo l'evento invece di disturbare la query ospite. Vedi CYRB-2.
|
|
35
43
|
CloseYourIt.internal_logger.error("CloseYourIt client: #{e.class}: #{e.message}")
|
|
44
|
+
CloseYourIt.stats.increment(:dropped)
|
|
45
|
+
CloseYourIt.notify_diagnostic(:drop, reason: :error, error: e.class.name)
|
|
36
46
|
nil
|
|
37
47
|
end
|
|
38
48
|
|
|
@@ -45,13 +55,25 @@ module CloseYourIt
|
|
|
45
55
|
return nil if events.nil? || events.empty?
|
|
46
56
|
|
|
47
57
|
payloads = events.map(&:to_h)
|
|
48
|
-
|
|
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
|
|
49
68
|
return nil if payloads.empty?
|
|
50
69
|
|
|
51
70
|
path = events.first.ingest_path(@configuration.project_id)
|
|
52
71
|
payloads.each_slice(LOGS_MAX_BATCH) do |chunk|
|
|
53
72
|
accepted = @worker.perform { @transport.send_event(chunk, path: path) }
|
|
54
|
-
|
|
73
|
+
next unless accepted
|
|
74
|
+
|
|
75
|
+
CloseYourIt.stats.increment(:enqueued)
|
|
76
|
+
CloseYourIt.notify_diagnostic(:enqueue, path: path, batch: chunk.size)
|
|
55
77
|
end
|
|
56
78
|
payloads
|
|
57
79
|
end
|
|
@@ -15,7 +15,18 @@ module CloseYourIt
|
|
|
15
15
|
# Header HTTP catturati nel contesto request (mai Authorization/Cookie → niente PII/segreti).
|
|
16
16
|
DEFAULT_REQUEST_HEADER_ALLOWLIST = %w[Accept Content-Type User-Agent Referer].freeze
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# Tabelle di servizio del Solid stack: sono infrastruttura del framework, non codice
|
|
19
|
+
# dell'applicazione. Una loro query lenta non si può correggere leggendo il proprio repo — dice
|
|
20
|
+
# solo che il database è in contesa, cosa che le query dell'app raccontano già. Escluse per
|
|
21
|
+
# DEFAULT dalla misura dei rallentamenti perché altrimenti la sommergono: su closeyourit-rails il
|
|
22
|
+
# 2026-07-30 erano circa 2.740 campioni su 4.400 (62%), tutti fra i 250 e i 655 ms (CYRB-18).
|
|
23
|
+
DEFAULT_EXCLUDED_QUERY_PATTERNS = [
|
|
24
|
+
/\bsolid_queue_/,
|
|
25
|
+
/\bsolid_cache_/,
|
|
26
|
+
/\bsolid_cable_/
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
attr_accessor :endpoint_url, :token, :project_id, :environment, :before_send, :on_diagnostic,
|
|
19
30
|
:async_threads, :background_worker_max_queue,
|
|
20
31
|
:slow_query_threshold_ms, :slow_method_threshold_ms,
|
|
21
32
|
:send_pii, :obfuscate_sql, :send_server_name,
|
|
@@ -28,9 +39,12 @@ module CloseYourIt
|
|
|
28
39
|
:capture_rails_logs, :logs_min_level, :capture_rails_logs_min_level,
|
|
29
40
|
:detect_performance_issues, :n_plus_one_threshold, :query_count_threshold,
|
|
30
41
|
:query_time_threshold_ms, :slow_request_threshold_ms, :slow_external_threshold_ms,
|
|
31
|
-
:capture_external_http, :trap_signals
|
|
42
|
+
:capture_external_http, :trap_signals,
|
|
43
|
+
:monitor_jobs, :slow_job_threshold_ms, :job_queue_latency_threshold_ms,
|
|
44
|
+
:jobs_sample_rate, :propagate_trace_context
|
|
32
45
|
attr_writer :release, :project_root
|
|
33
|
-
attr_reader :excluded_exceptions, :
|
|
46
|
+
attr_reader :excluded_exceptions, :excluded_log_patterns, :excluded_query_patterns,
|
|
47
|
+
:filter_parameters, :scrub_message_patterns, :trace_propagation_allowlist
|
|
34
48
|
|
|
35
49
|
def initialize
|
|
36
50
|
@endpoint_url = ENV.fetch("CLOSEYOURIT_ENDPOINT_URL", nil)
|
|
@@ -42,6 +56,12 @@ module CloseYourIt
|
|
|
42
56
|
@excluded_exceptions = DEFAULT_EXCLUDED_EXCEPTIONS.dup
|
|
43
57
|
@before_send = nil
|
|
44
58
|
|
|
59
|
+
# Hook diagnostico opt-in: `->(event, details) { ... }` invocato a ogni tappa del ciclo di vita
|
|
60
|
+
# di un evento (:enqueue, :send, :drop, :timeout, :shutdown) con dettagli privi di dati sensibili
|
|
61
|
+
# (es. `{ reason: :queue_full }`, `{ status: 429 }`). Locale e non ricorsivo: non invia telemetria
|
|
62
|
+
# e non può innescare loop di auto-monitoraggio (vedi CloseYourIt.notify_diagnostic, CYRB-12).
|
|
63
|
+
@on_diagnostic = nil
|
|
64
|
+
|
|
45
65
|
@async_threads = default_threads
|
|
46
66
|
@background_worker_max_queue = 30
|
|
47
67
|
|
|
@@ -52,6 +72,9 @@ module CloseYourIt
|
|
|
52
72
|
|
|
53
73
|
@slow_query_threshold_ms = 100
|
|
54
74
|
@slow_method_threshold_ms = 200
|
|
75
|
+
# Query da NON misurare come rallentamento (match sul testo SQL). Default: le tabelle di servizio
|
|
76
|
+
# del Solid stack. Chi vuole misurarle davvero azzera la lista.
|
|
77
|
+
@excluded_query_patterns = DEFAULT_EXCLUDED_QUERY_PATTERNS.dup
|
|
55
78
|
|
|
56
79
|
@send_pii = false
|
|
57
80
|
@obfuscate_sql = true
|
|
@@ -98,6 +121,10 @@ module CloseYourIt
|
|
|
98
121
|
# inonderebbe lo stream con decine di migliaia di log-entry/min (CYRB-7). Chi vuole anche gli info
|
|
99
122
|
# del framework la abbassa esplicitamente (es. :info).
|
|
100
123
|
@capture_rails_logs_min_level = :warn
|
|
124
|
+
# Rumore del broadcast Rails.logger che NON è un'eccezione, e quindi excluded_exceptions non può
|
|
125
|
+
# coprire: righe ripetute del framework o di una gemma. Regexp sul testo del messaggio, default
|
|
126
|
+
# vuoto. Vale SOLO per il broadcast automatico, mai per CloseYourIt.log esplicito.
|
|
127
|
+
@excluded_log_patterns = []
|
|
101
128
|
|
|
102
129
|
# Performance issue detection (verdetti aggregati: N+1, slow request, HTTP esterne lente).
|
|
103
130
|
# OPT-IN, default OFF: profila OGNI query della richiesta → overhead non trascurabile, va attivato
|
|
@@ -110,6 +137,26 @@ module CloseYourIt
|
|
|
110
137
|
@slow_external_threshold_ms = 1000 # singola chiamata HTTP esterna
|
|
111
138
|
@capture_external_http = true # strumenta Net::HTTP (solo se detect_performance_issues)
|
|
112
139
|
|
|
140
|
+
# Metriche dei background job: durata di esecuzione e attesa in coda (queue latency) per ActiveJob
|
|
141
|
+
# e Sidekiq. ON di default (a differenza di detect_performance_issues): la visibilità dei job
|
|
142
|
+
# lenti/in ritardo/ritentati non deve richiedere strumentazione manuale in ogni app (CYRB-14), e
|
|
143
|
+
# l'overhead è basso — una notifica per job, non il profiling di ogni query. Il rumore è tenuto a
|
|
144
|
+
# bada dalle soglie (job normali sotto soglia = niente metrica) e dal sampling. La label è il nome
|
|
145
|
+
# della classe del job; gli argomenti non vengono MAI inviati.
|
|
146
|
+
@monitor_jobs = true
|
|
147
|
+
@slow_job_threshold_ms = 5000 # durata del perform oltre cui = slow_job
|
|
148
|
+
@job_queue_latency_threshold_ms = 60_000 # attesa enqueue→esecuzione oltre cui = job_queue_latency
|
|
149
|
+
@jobs_sample_rate = 1.0 # frazione dei candidati oltre soglia effettivamente inviata
|
|
150
|
+
|
|
151
|
+
# Propagazione W3C trace context (traceparent/tracestate) verso i servizi esterni chiamati via
|
|
152
|
+
# Net::HTTP. OPT-IN, default OFF: iniettare header d'uscita attraversa un trust boundary e va deciso
|
|
153
|
+
# per-app. È limitata PER DESTINAZIONE dalla allowlist (host esatti, case-insensitive, o Regexp per i
|
|
154
|
+
# sottodomini); lista vuota = nessuna destinazione. Mai verso host non elencati, mai come `baggage`
|
|
155
|
+
# (che può portare PII). In ingresso un traceparent valido diventa il trace_id degli eventi
|
|
156
|
+
# CloseYourIt → gli errori/metriche della richiesta si correlano alla traccia distribuita (CYRB-15).
|
|
157
|
+
@propagate_trace_context = false
|
|
158
|
+
@trace_propagation_allowlist = []
|
|
159
|
+
|
|
113
160
|
# Radice del progetto: base per il filename relativo dei frame (culprit cross-SDK). Lazy:
|
|
114
161
|
# auto-rilevata da Rails.root o Dir.pwd al primo accesso se non impostata esplicitamente.
|
|
115
162
|
@project_root = nil
|
|
@@ -123,10 +170,28 @@ module CloseYourIt
|
|
|
123
170
|
@excluded_exceptions = Array(list).map { |item| item.is_a?(Regexp) ? item : item.to_s }
|
|
124
171
|
end
|
|
125
172
|
|
|
173
|
+
# Pattern del broadcast Rails.logger da scartare. Le stringhe diventano Regexp (match letterale
|
|
174
|
+
# sul testo): chi scrive `config.excluded_log_patterns = ["Rendered layout"]` intende quello.
|
|
175
|
+
def excluded_log_patterns=(list)
|
|
176
|
+
@excluded_log_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Query da non misurare. Stessa normalizzazione di excluded_log_patterns: String = testo letterale
|
|
180
|
+
# (un nome di tabella si scrive così, non come pattern), Regexp = pattern.
|
|
181
|
+
def excluded_query_patterns=(list)
|
|
182
|
+
@excluded_query_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
|
|
183
|
+
end
|
|
184
|
+
|
|
126
185
|
def filter_parameters=(list)
|
|
127
186
|
@filter_parameters = Array(list)
|
|
128
187
|
end
|
|
129
188
|
|
|
189
|
+
# Destinazioni autorizzate a ricevere gli header di trace W3C. String = host esatto (match
|
|
190
|
+
# case-insensitive), Regexp = pattern (per sottodomini/famiglie di host). Lista vuota = nessuno.
|
|
191
|
+
def trace_propagation_allowlist=(list)
|
|
192
|
+
@trace_propagation_allowlist = Array(list)
|
|
193
|
+
end
|
|
194
|
+
|
|
130
195
|
def scrub_message_patterns=(list)
|
|
131
196
|
@scrub_message_patterns = Array(list)
|
|
132
197
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "../event"
|
|
5
|
+
|
|
6
|
+
module CloseYourIt
|
|
7
|
+
# Metrica `kind=performance_issue` per un background job che ha superato una soglia di DURATA
|
|
8
|
+
# (`subtype=slow_job`) o di ATTESA in coda (`subtype=job_queue_latency`). Spedita sulla pipeline
|
|
9
|
+
# metriche (`/api/v1/projects/:id/metrics`), come slow_query/slow_method e i verdetti performance.
|
|
10
|
+
#
|
|
11
|
+
# `label` è il nome della CLASSE del job — mai gli argomenti (privacy: come slow_method invia solo
|
|
12
|
+
# label/durata). `attempt` rende visibili i retry; `trace_id` (= job_id ActiveJob / jid Sidekiq)
|
|
13
|
+
# correla la metrica a log ed errori dello stesso job. `duration_ms` è il valore misurato del subtype
|
|
14
|
+
# (durata del perform per slow_job, attesa in coda per job_queue_latency) — stessa convenzione del
|
|
15
|
+
# Rollup. I campi nil vengono omessi.
|
|
16
|
+
class JobMetricEvent < Event
|
|
17
|
+
def initialize(attrs, configuration)
|
|
18
|
+
super(configuration)
|
|
19
|
+
@attrs = attrs
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
compact(
|
|
24
|
+
"kind" => "performance_issue",
|
|
25
|
+
"subtype" => @attrs[:subtype],
|
|
26
|
+
"sample_id" => SecureRandom.uuid,
|
|
27
|
+
"duration_ms" => @attrs[:duration_ms]&.round(2),
|
|
28
|
+
"occurred_at" => @occurred_at,
|
|
29
|
+
"environment" => environment,
|
|
30
|
+
"release" => @configuration.release,
|
|
31
|
+
"trace_id" => @attrs[:trace_id],
|
|
32
|
+
"label" => @attrs[:job_class],
|
|
33
|
+
"queue" => @attrs[:queue],
|
|
34
|
+
"adapter" => @attrs[:adapter],
|
|
35
|
+
"attempt" => @attrs[:attempt],
|
|
36
|
+
"sdk" => sdk
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ingest_path(project_id)
|
|
41
|
+
"/api/v1/projects/#{project_id}/metrics"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -37,6 +37,12 @@ module CloseYourIt
|
|
|
37
37
|
# before_send che solleva su UN evento non deve propagare nell'app né uccidere il TimerTask
|
|
38
38
|
# (che altrimenti smette di flushare → log accumulati e persi). Ingoiato e loggato.
|
|
39
39
|
CloseYourIt.internal_logger.error("CloseYourIt log buffer: #{e.class}: #{e.message}")
|
|
40
|
+
# Il batch è già stato drenato: se flush_logs solleva, quei log sono persi. Contabilizzali come
|
|
41
|
+
# scarti per non lasciare un fallimento silenzioso (CYRB-12). `batch.to_a` è nil-safe.
|
|
42
|
+
batch.to_a.size.times do
|
|
43
|
+
CloseYourIt.stats.increment(:dropped)
|
|
44
|
+
CloseYourIt.notify_diagnostic(:drop, reason: :error)
|
|
45
|
+
end
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def shutdown
|
|
@@ -3,17 +3,45 @@
|
|
|
3
3
|
module CloseYourIt
|
|
4
4
|
module Rails
|
|
5
5
|
# Incluso in ActiveJob::Base (via railtie `on_load(:active_job)`): cattura gli errori dei job
|
|
6
|
-
# (oggi persi) con il contesto del job
|
|
6
|
+
# (oggi persi) con il contesto del job. La logica vive in `.monitor`/`.report_discarded` per essere
|
|
7
7
|
# testabile senza ActiveSupport/ActiveJob.
|
|
8
|
+
#
|
|
9
|
+
# CYRB-19: `around_perform` gira DENTRO `perform_now`, mentre `retry_on`/`discard_on` (rescue_from)
|
|
10
|
+
# sono valutati FUORI, dopo i callback. Catturare nell'around_perform significa quindi segnalare
|
|
11
|
+
# OGNI tentativo — anche quelli che verranno ritentati con successo — perché il reporter vede
|
|
12
|
+
# l'errore prima che il retry possa zittirlo, e ogni tentativo solleva una nuova istanza (la
|
|
13
|
+
# deduplica interna, per-istanza, non interviene). Su ActiveJob 7.1+ deleghiamo la segnalazione ad
|
|
14
|
+
# `after_discard`, che Rails invoca al fallimento definitivo (retry_on esauriti o eccezione non
|
|
15
|
+
# gestita), mai sui tentativi che `retry_on` ritenta → una sola occorrenza per job.
|
|
16
|
+
#
|
|
17
|
+
# Limiti noti (per costruzione di ActiveJob, non del client):
|
|
18
|
+
# - `after_discard` NON scatta per gli errori intercettati da un `rescue_from` custom: sono gestiti
|
|
19
|
+
# dall'app, quindi non li segnaliamo più come "non gestiti" (prima lo facevamo, impropriamente).
|
|
20
|
+
# - I retry a livello di ADAPTER (es. Sidekiq) SENZA `retry_on` risollevano l'errore non gestito:
|
|
21
|
+
# `after_discard` scatta a ogni esecuzione, quindi lì la de-duplicazione per tentativo non si
|
|
22
|
+
# applica (comportamento invariato rispetto a prima).
|
|
23
|
+
# Sulle versioni prive di `after_discard` restiamo al fallback legacy (cattura nell'around_perform).
|
|
8
24
|
module ActiveJobExtension
|
|
25
|
+
# Scope arricchito durante il `perform` tramandato ad `after_discard`. Legato all'ISTANZA del job
|
|
26
|
+
# (ogni retry ne crea una nuova, deserializzata) → vive esattamente quanto serve, niente bleed tra
|
|
27
|
+
# job o thread; il reset di fine `perform` lo sgancia solo dallo storage, non muta l'oggetto.
|
|
28
|
+
STASHED_SCOPE_IVAR = :@__closeyourit_stashed_scope
|
|
29
|
+
|
|
9
30
|
def self.included(base)
|
|
10
31
|
base.around_perform do |job, block|
|
|
11
32
|
CloseYourIt::Rails::ActiveJobExtension.monitor(job) { block.call }
|
|
12
33
|
end
|
|
34
|
+
|
|
35
|
+
return unless base.respond_to?(:after_discard)
|
|
36
|
+
|
|
37
|
+
base.after_discard do |job, exception|
|
|
38
|
+
CloseYourIt::Rails::ActiveJobExtension.report_discarded(job, exception)
|
|
39
|
+
end
|
|
13
40
|
end
|
|
14
41
|
|
|
15
|
-
# Esegue il job arricchendo lo scope con tag/context;
|
|
16
|
-
#
|
|
42
|
+
# Esegue il job arricchendo lo scope con tag/context; resetta lo scope a fine job (no bleed tra
|
|
43
|
+
# job sullo stesso thread). Su ActiveJob 7.1+ NON cattura l'errore (lo fa `after_discard` solo al
|
|
44
|
+
# fallimento definitivo): qui tramanda lo scope al job e ri-solleva. Sul fallback legacy cattura.
|
|
17
45
|
def self.monitor(job)
|
|
18
46
|
return yield unless CloseYourIt.configuration.report_active_job_errors
|
|
19
47
|
|
|
@@ -21,13 +49,41 @@ module CloseYourIt
|
|
|
21
49
|
apply_job_scope(job)
|
|
22
50
|
yield
|
|
23
51
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
24
|
-
|
|
52
|
+
if report_on_discard?(job)
|
|
53
|
+
job.instance_variable_set(STASHED_SCOPE_IVAR, CloseYourIt::Scope.current)
|
|
54
|
+
else
|
|
55
|
+
CloseYourIt.capture_exception(e, handled: false)
|
|
56
|
+
end
|
|
25
57
|
raise
|
|
26
58
|
ensure
|
|
27
59
|
CloseYourIt::Scope.reset!
|
|
28
60
|
end
|
|
29
61
|
end
|
|
30
62
|
|
|
63
|
+
# Aggancio `after_discard` (ActiveJob 7.1+): il job è definitivamente fallito, quindi segnaliamo
|
|
64
|
+
# l'errore UNA sola volta (handled:false). Riprendiamo lo scope arricchito durante il `perform`
|
|
65
|
+
# (tramandato da `monitor`) così il report conserva breadcrumb/tag/contesti raccolti nel job;
|
|
66
|
+
# `apply_job_scope` rinfresca i campi standard con `executions` finale senza perdere i custom.
|
|
67
|
+
def self.report_discarded(job, exception)
|
|
68
|
+
return unless CloseYourIt.configuration.report_active_job_errors
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
stashed = job.instance_variable_get(STASHED_SCOPE_IVAR)
|
|
72
|
+
CloseYourIt::Scope.current = stashed if stashed
|
|
73
|
+
apply_job_scope(job)
|
|
74
|
+
CloseYourIt.capture_exception(exception, handled: false)
|
|
75
|
+
ensure
|
|
76
|
+
job.remove_instance_variable(STASHED_SCOPE_IVAR) if job.instance_variable_defined?(STASHED_SCOPE_IVAR)
|
|
77
|
+
CloseYourIt::Scope.reset!
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# true quando ActiveJob espone `after_discard` (7.1+): la segnalazione è delegata lì, così
|
|
82
|
+
# `monitor` non cattura i tentativi intermedi. false → fallback legacy (cattura nell'around_perform).
|
|
83
|
+
def self.report_on_discard?(job)
|
|
84
|
+
job.class.respond_to?(:after_discard)
|
|
85
|
+
end
|
|
86
|
+
|
|
31
87
|
def self.apply_job_scope(job)
|
|
32
88
|
CloseYourIt.set_tag("job.class", job.class.name)
|
|
33
89
|
CloseYourIt.set_tag("job.queue", job.queue_name) if job.respond_to?(:queue_name)
|
|
@@ -18,13 +18,20 @@ module CloseYourIt
|
|
|
18
18
|
self.level = LEVEL_BY_SYMBOL.fetch(min_level.to_sym, 2)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Sovrascrive il punto unico di ::Logger: filtra per soglia e re-inoltra a
|
|
21
|
+
# Sovrascrive il punto unico di ::Logger: filtra per soglia e per esclusioni, poi re-inoltra a
|
|
22
|
+
# CloseYourIt.log. Il secondo filtro esiste perché la soglia da sola non distingue il rumore: una
|
|
23
|
+
# `ActionController::RoutingError` da favicon mancante è un `logger.error` come un altro, e senza
|
|
24
|
+
# controllare il TESTO rientrava dal canale log dopo essere stata esclusa da quello degli errori
|
|
25
|
+
# (CYRB-17). Il gating vive qui e non in `CloseYourIt.log`: quel metodo serve anche il dev che
|
|
26
|
+
# scrive un log di proposito, e le sue righe non si silenziano.
|
|
22
27
|
def add(severity, message = nil, progname = nil)
|
|
23
28
|
severity ||= ::Logger::UNKNOWN
|
|
24
29
|
return true if severity < level
|
|
25
30
|
|
|
26
31
|
text = message || (block_given? ? yield : nil) || progname
|
|
27
|
-
|
|
32
|
+
return true if text.nil? || CloseYourIt.ignored_log_message?(text)
|
|
33
|
+
|
|
34
|
+
CloseYourIt.log(SEVERITY_LEVELS.fetch(severity, "info"), text)
|
|
28
35
|
true
|
|
29
36
|
end
|
|
30
37
|
end
|
|
@@ -12,6 +12,9 @@ module CloseYourIt
|
|
|
12
12
|
module NetHTTPPatch
|
|
13
13
|
def request(req, body = nil, &block)
|
|
14
14
|
config = CloseYourIt.configuration
|
|
15
|
+
# Propagazione W3C: indipendente dal profiling (ha il suo opt-in), va fatta PRIMA del round-trip
|
|
16
|
+
# perché aggiunge header alla richiesta in uscita.
|
|
17
|
+
inject_trace_context(config, req)
|
|
15
18
|
return super unless config.detect_performance_issues && config.capture_external_http
|
|
16
19
|
|
|
17
20
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -25,6 +28,53 @@ module CloseYourIt
|
|
|
25
28
|
|
|
26
29
|
private
|
|
27
30
|
|
|
31
|
+
# Gestisce gli header di trace W3C sulla richiesta in uscita (solo con la propagazione opt-in ON).
|
|
32
|
+
# Verso una destinazione autorizzata inietta traceparent/tracestate; verso qualsiasi altra li
|
|
33
|
+
# RIMUOVE. La rimozione è la difesa contro il leak su redirect cross-host: se lo stesso oggetto
|
|
34
|
+
# request viene riusato per seguire un redirect verso un host non in allowlist, gli header della
|
|
35
|
+
# chiamata precedente non devono sopravvivere ("host esterni non ricevono header interni" — CYRB-15).
|
|
36
|
+
# baggage non viene mai né letto né emesso. Difensivo: mai solleva per colpa della propagazione.
|
|
37
|
+
def inject_trace_context(config, req)
|
|
38
|
+
return unless config.propagate_trace_context
|
|
39
|
+
|
|
40
|
+
context = deliverable_context(config)
|
|
41
|
+
if context.nil?
|
|
42
|
+
strip_trace_headers(req)
|
|
43
|
+
else
|
|
44
|
+
context.headers.each { |name, value| req[name] = value }
|
|
45
|
+
end
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Il trace context da consegnare a QUESTA destinazione, o nil se non va propagato nulla: host
|
|
51
|
+
# assente, endpoint CloseYourIt stesso (niente auto-propagazione), destinazione fuori allowlist,
|
|
52
|
+
# o scope privo di contesto.
|
|
53
|
+
def deliverable_context(config)
|
|
54
|
+
host = address
|
|
55
|
+
return nil if host.nil? || own_endpoint?(config, host)
|
|
56
|
+
return nil unless destination_allowed?(config, host)
|
|
57
|
+
|
|
58
|
+
CloseYourIt::Scope.current.trace_context
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# La destinazione è autorizzata a ricevere il trace context? String = host esatto (case-insensitive),
|
|
62
|
+
# Regexp = pattern (sottodomini/famiglie). Lista vuota → sempre false (nessuna destinazione).
|
|
63
|
+
def destination_allowed?(config, host)
|
|
64
|
+
config.trace_propagation_allowlist.any? do |pattern|
|
|
65
|
+
pattern.is_a?(Regexp) ? pattern.match?(host) : pattern.to_s.casecmp?(host)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Rimuove gli header di trace W3C che una chiamata precedente sullo stesso oggetto request possa
|
|
70
|
+
# aver lasciato. Solo i nostri header, mai altro; no-op se il request non li supporta.
|
|
71
|
+
def strip_trace_headers(req)
|
|
72
|
+
return unless req.respond_to?(:delete)
|
|
73
|
+
|
|
74
|
+
req.delete("traceparent")
|
|
75
|
+
req.delete("tracestate")
|
|
76
|
+
end
|
|
77
|
+
|
|
28
78
|
def record_external(config, req, duration_ms)
|
|
29
79
|
host = address
|
|
30
80
|
return if host.nil? || own_endpoint?(config, host)
|