closeyourit-ruby 0.7.0 → 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 +19 -0
- data/lib/closeyourit/configuration.rb +17 -2
- data/lib/closeyourit/rails/active_job_extension.rb +60 -4
- data/lib/closeyourit/rails/net_http_patch.rb +50 -0
- data/lib/closeyourit/rails/railtie.rb +3 -2
- data/lib/closeyourit/rails/request_context.rb +19 -2
- data/lib/closeyourit/scope.rb +11 -1
- data/lib/closeyourit/trace_context.rb +109 -0
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +1 -0
- metadata +2 -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
|
@@ -105,6 +105,25 @@ end
|
|
|
105
105
|
| `slow_job_threshold_ms` | `5000` | Durata del job (ms) oltre cui = `slow_job` |
|
|
106
106
|
| `job_queue_latency_threshold_ms` | `60000` | Attesa in coda (enqueue→esecuzione, ms) oltre cui = `job_queue_latency` |
|
|
107
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.
|
|
108
127
|
|
|
109
128
|
## Cosa cattura
|
|
110
129
|
|
|
@@ -41,10 +41,10 @@ module CloseYourIt
|
|
|
41
41
|
:query_time_threshold_ms, :slow_request_threshold_ms, :slow_external_threshold_ms,
|
|
42
42
|
:capture_external_http, :trap_signals,
|
|
43
43
|
:monitor_jobs, :slow_job_threshold_ms, :job_queue_latency_threshold_ms,
|
|
44
|
-
:jobs_sample_rate
|
|
44
|
+
:jobs_sample_rate, :propagate_trace_context
|
|
45
45
|
attr_writer :release, :project_root
|
|
46
46
|
attr_reader :excluded_exceptions, :excluded_log_patterns, :excluded_query_patterns,
|
|
47
|
-
:filter_parameters, :scrub_message_patterns
|
|
47
|
+
:filter_parameters, :scrub_message_patterns, :trace_propagation_allowlist
|
|
48
48
|
|
|
49
49
|
def initialize
|
|
50
50
|
@endpoint_url = ENV.fetch("CLOSEYOURIT_ENDPOINT_URL", nil)
|
|
@@ -148,6 +148,15 @@ module CloseYourIt
|
|
|
148
148
|
@job_queue_latency_threshold_ms = 60_000 # attesa enqueue→esecuzione oltre cui = job_queue_latency
|
|
149
149
|
@jobs_sample_rate = 1.0 # frazione dei candidati oltre soglia effettivamente inviata
|
|
150
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
|
+
|
|
151
160
|
# Radice del progetto: base per il filename relativo dei frame (culprit cross-SDK). Lazy:
|
|
152
161
|
# auto-rilevata da Rails.root o Dir.pwd al primo accesso se non impostata esplicitamente.
|
|
153
162
|
@project_root = nil
|
|
@@ -177,6 +186,12 @@ module CloseYourIt
|
|
|
177
186
|
@filter_parameters = Array(list)
|
|
178
187
|
end
|
|
179
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
|
+
|
|
180
195
|
def scrub_message_patterns=(list)
|
|
181
196
|
@scrub_message_patterns = Array(list)
|
|
182
197
|
end
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -75,8 +75,9 @@ module CloseYourIt
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
# Strumenta
|
|
79
|
-
#
|
|
78
|
+
# Strumenta Net::HTTP: rileva le chiamate esterne lente (solo con detect_performance_issues) e
|
|
79
|
+
# propaga il trace context W3C (solo con propagate_trace_context). Prepend incondizionato — i due
|
|
80
|
+
# opt-in sono valutati per-chiamata nel patch; con entrambi OFF è di fatto no-op (chiama super).
|
|
80
81
|
initializer "closeyourit.instrument_net_http" do
|
|
81
82
|
require "net/http"
|
|
82
83
|
::Net::HTTP.prepend(CloseYourIt::Rails::NetHTTPPatch) unless ::Net::HTTP.ancestors.include?(CloseYourIt::Rails::NetHTTPPatch)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require_relative "../trace_context"
|
|
4
5
|
|
|
5
6
|
module CloseYourIt
|
|
6
7
|
module Rails
|
|
@@ -22,8 +23,13 @@ module CloseYourIt
|
|
|
22
23
|
|
|
23
24
|
def call(env)
|
|
24
25
|
if enabled?
|
|
25
|
-
#
|
|
26
|
-
|
|
26
|
+
# Contesto di trace W3C (solo con propagazione opt-in): adottato dall'header entrante o generato.
|
|
27
|
+
context = trace_context_for(env)
|
|
28
|
+
CloseYourIt::Scope.current.trace_context = context
|
|
29
|
+
# trace_id sempre (correlazione log↔errori), anche con capture_request OFF. Con un trace context
|
|
30
|
+
# W3C il trace_id degli eventi È il trace-id W3C → l'errore/metrica si allinea alla traccia
|
|
31
|
+
# distribuita propagata a valle (mapping trace_id, CYRB-15); altrimenti request_id come prima.
|
|
32
|
+
CloseYourIt::Scope.current.trace_id = context ? context.trace_id : trace_id_for(env)
|
|
27
33
|
# Correlazione errore server ↔ session replay: l'id dal cookie finisce sullo scope
|
|
28
34
|
# → contexts.replay.replay_id dell'evento (stesso punto del percorso JS).
|
|
29
35
|
CloseYourIt::Scope.current.replay_session_id = replay_session_id_for(env)
|
|
@@ -46,6 +52,17 @@ module CloseYourIt
|
|
|
46
52
|
false
|
|
47
53
|
end
|
|
48
54
|
|
|
55
|
+
# Contesto di trace W3C della richiesta, SOLO con la propagazione opt-in attiva (altrimenti nil →
|
|
56
|
+
# comportamento storico invariato). Adotta un traceparent/tracestate entrante valido (correlazione
|
|
57
|
+
# distribuita), altrimenti genera un root: così anche i servizi che ORIGINANO traffico partono con
|
|
58
|
+
# un trace-id W3C propagabile a valle. Un traceparent malformato è ignorato → si genera un root.
|
|
59
|
+
def trace_context_for(env)
|
|
60
|
+
return nil unless CloseYourIt.configuration.propagate_trace_context
|
|
61
|
+
|
|
62
|
+
CloseYourIt::TraceContext.parse(env["HTTP_TRACEPARENT"], env["HTTP_TRACESTATE"]) ||
|
|
63
|
+
CloseYourIt::TraceContext.generate
|
|
64
|
+
end
|
|
65
|
+
|
|
49
66
|
# Riusa il request id di Rails/Rack se presente (stessa correlazione dei log applicativi),
|
|
50
67
|
# altrimenti ne genera uno.
|
|
51
68
|
def trace_id_for(env)
|
data/lib/closeyourit/scope.rb
CHANGED
|
@@ -18,6 +18,13 @@ module CloseYourIt
|
|
|
18
18
|
store[STORAGE_KEY] ||= new
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Re-installa uno scope salvato in precedenza. Serve a `after_discard` (ActiveJob 7.1+) per
|
|
22
|
+
# riprendere lo scope arricchito durante il `perform` — tag/contesti/breadcrumb, incluse le query —
|
|
23
|
+
# dopo che il reset di fine `perform` l'ha sganciato dallo storage (CYRB-19). Simmetrico a `reset!`.
|
|
24
|
+
def current=(scope)
|
|
25
|
+
store[STORAGE_KEY] = scope
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
# Azzera lo scope corrente — chiamato in `ensure` da middleware e job (su Puma il
|
|
22
29
|
# thread è riusato: senza reset lo scope colerebbe nella richiesta successiva).
|
|
23
30
|
def reset!
|
|
@@ -35,7 +42,7 @@ module CloseYourIt
|
|
|
35
42
|
end
|
|
36
43
|
end
|
|
37
44
|
|
|
38
|
-
attr_accessor :request, :trace_id, :rack_env, :replay_session_id
|
|
45
|
+
attr_accessor :request, :trace_id, :rack_env, :replay_session_id, :trace_context
|
|
39
46
|
attr_reader :user, :tags, :extra, :contexts, :breadcrumbs
|
|
40
47
|
|
|
41
48
|
def initialize
|
|
@@ -81,6 +88,9 @@ module CloseYourIt
|
|
|
81
88
|
@rack_env = nil
|
|
82
89
|
@trace_id = nil
|
|
83
90
|
@replay_session_id = nil
|
|
91
|
+
# Contesto di trace W3C della richiesta (CloseYourIt::TraceContext): popolato solo con la
|
|
92
|
+
# propagazione opt-in attiva, consumato dal patch Net::HTTP per gli header d'uscita.
|
|
93
|
+
@trace_context = nil
|
|
84
94
|
@breadcrumbs = BreadcrumbBuffer.new(CloseYourIt.configuration.max_breadcrumbs)
|
|
85
95
|
@performance_profile = nil
|
|
86
96
|
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module CloseYourIt
|
|
6
|
+
# Contesto di trace W3C (`traceparent`/`tracestate`, https://www.w3.org/TR/trace-context/).
|
|
7
|
+
# NON è un tracer: non apre span né misura tempi. È un "propagation bridge" minimale (il ticket:
|
|
8
|
+
# "Non costruire tracing custom completo") — adotta un contesto entrante valido facendo pass-through
|
|
9
|
+
# di trace-id/parent-id/flag, oppure ne genera uno root, e sa serializzarsi negli header d'uscita.
|
|
10
|
+
# Volutamente separato dai formati proprietari: sullo standard, senza dipendere da un vendor.
|
|
11
|
+
class TraceContext
|
|
12
|
+
# traceparent = version "-" trace-id "-" parent-id "-" trace-flags (55 char per la versione 00).
|
|
13
|
+
# `rest` cattura eventuali campi futuri: ammessi solo da versioni > 00 (forward-compat), vietati su 00.
|
|
14
|
+
TRACEPARENT = /
|
|
15
|
+
\A
|
|
16
|
+
(?<version>[0-9a-f]{2})-
|
|
17
|
+
(?<trace_id>[0-9a-f]{32})-
|
|
18
|
+
(?<parent_id>[0-9a-f]{16})-
|
|
19
|
+
(?<flags>[0-9a-f]{2})
|
|
20
|
+
(?<rest>-.*)?
|
|
21
|
+
\z
|
|
22
|
+
/x
|
|
23
|
+
|
|
24
|
+
FORBIDDEN_VERSION = "ff"
|
|
25
|
+
CURRENT_VERSION = "00"
|
|
26
|
+
ZERO_TRACE_ID = ("0" * 32).freeze
|
|
27
|
+
ZERO_PARENT_ID = ("0" * 16).freeze
|
|
28
|
+
FLAG_SAMPLED = 0x01
|
|
29
|
+
|
|
30
|
+
# tracestate: lista di membri `key=value` separati da virgola, max 32 (W3C §3.3.1).
|
|
31
|
+
TRACESTATE_MAX_MEMBERS = 32
|
|
32
|
+
# key: lowercase alnum iniziale + set ristretto (incluso `@`/`/` per le chiavi tenant@vendor).
|
|
33
|
+
# value: caratteri stampabili 0x20–0x7E esclusi `,` (0x2C) e `=` (0x3D).
|
|
34
|
+
TRACESTATE_MEMBER = %r{\A[a-z0-9][a-z0-9_\-*/@]*=[\x20-\x2b\x2d-\x3c\x3e-\x7e]+\z}
|
|
35
|
+
|
|
36
|
+
attr_reader :trace_id, :parent_id, :flags, :tracestate
|
|
37
|
+
|
|
38
|
+
def initialize(trace_id:, parent_id:, flags:, tracestate: nil)
|
|
39
|
+
@trace_id = trace_id
|
|
40
|
+
@parent_id = parent_id
|
|
41
|
+
@flags = flags
|
|
42
|
+
@tracestate = tracestate
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# Adotta un traceparent entrante. Ritorna nil se malformato (→ il chiamante genera un root o
|
|
47
|
+
# lascia il contesto assente). Pass-through: mantiene trace-id/parent-id/flag verbatim così il
|
|
48
|
+
# bridge non inventa span. Il tracestate viene sanificato (membri invalidi scartati, cap a 32).
|
|
49
|
+
def parse(traceparent, tracestate = nil)
|
|
50
|
+
match = TRACEPARENT.match(traceparent.to_s.strip)
|
|
51
|
+
return nil unless match
|
|
52
|
+
return nil if match[:version] == FORBIDDEN_VERSION
|
|
53
|
+
return nil if match[:version] == CURRENT_VERSION && match[:rest]
|
|
54
|
+
return nil if match[:trace_id] == ZERO_TRACE_ID
|
|
55
|
+
return nil if match[:parent_id] == ZERO_PARENT_ID
|
|
56
|
+
|
|
57
|
+
new(
|
|
58
|
+
trace_id: match[:trace_id],
|
|
59
|
+
parent_id: match[:parent_id],
|
|
60
|
+
flags: match[:flags].to_i(16),
|
|
61
|
+
tracestate: sanitize_tracestate(tracestate)
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Nuovo contesto root (nessun traceparent entrante valido). Genera trace-id (16 byte) e parent-id
|
|
66
|
+
# (8 byte) casuali — SecureRandom è fork-safe, così un worker forkato non riusa gli id del padre.
|
|
67
|
+
# `sampled` fissa il flag: un root che apriamo noi traccia di default.
|
|
68
|
+
def generate(sampled: true)
|
|
69
|
+
new(
|
|
70
|
+
trace_id: SecureRandom.hex(16),
|
|
71
|
+
parent_id: SecureRandom.hex(8),
|
|
72
|
+
flags: sampled ? FLAG_SAMPLED : 0,
|
|
73
|
+
tracestate: nil
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Trattiene solo i membri ben formati, nell'ordine originale, fino a 32. Ritorna nil se non ne
|
|
80
|
+
# resta nessuno → così non propaghiamo mai un tracestate spazzatura o sovradimensionato.
|
|
81
|
+
def sanitize_tracestate(tracestate)
|
|
82
|
+
return nil if tracestate.nil?
|
|
83
|
+
|
|
84
|
+
valid = tracestate.to_s.split(",").map(&:strip)
|
|
85
|
+
.select { |member| TRACESTATE_MEMBER.match?(member) }
|
|
86
|
+
.first(TRACESTATE_MAX_MEMBERS)
|
|
87
|
+
valid.empty? ? nil : valid.join(",")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def sampled?
|
|
92
|
+
(flags & FLAG_SAMPLED) != 0
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# traceparent d'uscita, sempre versione 00 (l'unica che sappiamo emettere). I flag sono ri-emessi
|
|
96
|
+
# per intero (i bit riservati vanno propagati as-is), formattati su due cifre esadecimali.
|
|
97
|
+
def traceparent
|
|
98
|
+
format("%s-%s-%s-%02x", CURRENT_VERSION, trace_id, parent_id, flags & 0xff)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Header di propagazione W3C: traceparent (+ tracestate se presente). MAI `baggage`: può contenere
|
|
102
|
+
# contesto interno/PII e non deve varcare il confine (ticket: "baggage sensibile non riceve header").
|
|
103
|
+
def headers
|
|
104
|
+
result = { "traceparent" => traceparent }
|
|
105
|
+
result["tracestate"] = tracestate if tracestate && !tracestate.empty?
|
|
106
|
+
result
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/closeyourit/version.rb
CHANGED
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"
|
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
|
|
@@ -85,6 +85,7 @@ files:
|
|
|
85
85
|
- lib/closeyourit/subscribers/job_performance.rb
|
|
86
86
|
- lib/closeyourit/subscribers/request_performance.rb
|
|
87
87
|
- lib/closeyourit/subscribers/slow_query.rb
|
|
88
|
+
- lib/closeyourit/trace_context.rb
|
|
88
89
|
- lib/closeyourit/transport.rb
|
|
89
90
|
- lib/closeyourit/version.rb
|
|
90
91
|
homepage: https://github.com/bussolabs/closeyourit-ruby
|