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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '083ced8226884d6ceed86ae4da2a17cb7f9bfdfa5f3cd6073ac09268c4545d6e'
4
- data.tar.gz: 75b18abef8b8c45ae14647c4bc102276286a2c71c1056d37274d0fe350f20555
3
+ metadata.gz: 1482102089ba9e319a50f2d3822b63fc76ddc9a63c6c629f1d7cdc53022ca362
4
+ data.tar.gz: f312feb36b22702dca3109c62f623ba4484d25caf82dc9b25c807fe89f2ea2ba
5
5
  SHA512:
6
- metadata.gz: c9285e453091b8f89bc360e1a274fc72fa7b312cd2fd2ac7f8dddb0636a7a49e6f7782f0af5b8e2cb20275e841566731fe48174dddf6f502cb9eb92646bae216
7
- data.tar.gz: 1bcd7478c0d2d5572c0fa761e0b16a6bacffef89bd227a36fae117cea468fdd538bf9f011d839fa378a29871a1b2e8efdfc3ef2c467df684096eff585206a7d3
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, poi ri-solleva. La logica vive in `.monitor` per essere
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; cattura l'errore (handled:false) e
16
- # ri-solleva; resetta lo scope a fine job (no bleed tra job sullo stesso thread).
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
- CloseYourIt.capture_exception(e, handled: false)
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 le chiamate HTTP esterne (Net::HTTP) per rilevare quelle lente nella finestra della
79
- # richiesta. Il patch è no-op (chiama super) se la detection è OFF → overhead trascurabile.
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
- # trace_id sempre (correlazione log↔errori), anche con capture_request OFF.
26
- CloseYourIt::Scope.current.trace_id = trace_id_for(env)
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
@@ -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.7.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