chronos-ruby 1.1.0 → 1.2.0.pre.1

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: 0127def72275a2d4c08314040c04e34a9606fed13982d84b06c465dee7be4cf6
4
- data.tar.gz: 99f272314517e6a525223ce86964b70fcdfce717eb0e434b9c77e275031913b7
3
+ metadata.gz: c13c2858edcb41d4b2a0bfc83049349ee13a5a9ecee873798750b4bf9f1da48c
4
+ data.tar.gz: a21babcc28ce763e7d867f35064735d661214724c1ed9d256e8d174a1a04c5dc
5
5
  SHA512:
6
- metadata.gz: 93b37af1a5b51284a6c6de1c19e12de18ac23b04bfa4a04f33b7fce5cb611ec6f940d4ec2508c8a2b8d72f91e9a2a9a812a3b48d72f60dfefd642fb7cf3fe138
7
- data.tar.gz: 4c653acb6d7aa90d534a7d496568fca64fb9ba9bce9039c6ca68fad71c1b5aef3f4e5de541a3f3d0f6064f08653359bcc2afc30e3a2d82c910942f0c1e103d4a
6
+ metadata.gz: 81ab0672785236e1ffaeecf0fbb5ae3187f319be349a06f724eee50dd0d20963afb74a84485267e8ea5e3a7dd37dacef15ab456783fee622bef901110b98344a
7
+ data.tar.gz: 835ee551ddcec646a44376dcdfc09748e12c0fa360fee62365455e6776503a0597baed504a33276059853e652f5a90458ad7ae43bb582f7647303af9940a5d1f
data/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes are documented here. The project follows Semantic Versioning
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.0.pre.1] - 2026-08-11
8
+
9
+ ### Added
10
+
11
+ - fiber-aware execution context with a thread-local fallback;
12
+ - Sidekiq 7 and Rails 7 error reporter integration using public extension APIs;
13
+ - Action Cable notification spans and a dependency-free Faraday middleware;
14
+ - optional W3C `traceparent` propagation and an OpenTelemetry bridge that consumes the active span without installing an SDK or exporter.
15
+
16
+ ### Changed
17
+
18
+ - the transitional runtime range is Ruby 2.7 through 3.4 and the default context strategy is `:fiber_local`;
19
+ - Active Job and Sidekiq envelopes may propagate allowlisted span ID and trace flags in addition to trace/request IDs.
20
+
21
+ ### Security
22
+
23
+ - W3C identifiers are syntax-validated, all-zero IDs are rejected, and Rails error reporter context is reduced to bounded allowlisted metadata.
24
+
7
25
  ## [1.1.0] - 2026-08-05
8
26
 
9
27
  ### Added
data/README.md CHANGED
@@ -24,7 +24,7 @@ A gem não varre variáveis de ambiente, sistema de arquivos ou lockfiles e não
24
24
 
25
25
  ## Versões Ruby e Rails suportadas
26
26
 
27
- A versão 1.1.0 suporta Ruby puro e Rack em Ruby 2.2.10, 2.3.8, 2.4.10, 2.5.9 e 2.6.10. As combinações Rails validadas são Rails 4.2 com Ruby 2.2.10/2.3.8 e Rails 5.2 com Ruby 2.5.9/2.6.10. Sidekiq 4.2.10 com Ruby 2.2.10 e Sidekiq 5.2.10 com Ruby 2.5.9 também possuem gates dedicados. Ruby 2.7/Rails 6 não é declarado nesta release porque ainda não possui aplicação e matriz completas.
27
+ A versão candidata 1.2.0.pre.1 inicia a linha transitional para Ruby 2.7–3.4, Rails 7.x e Sidekiq 7. Ela adiciona contexto por Fiber, Rails Error Reporter, Action Cable, Faraday, Trace Context W3C opcional e coexistência com um SDK OpenTelemetry configurado. Use-a explicitamente em staging enquanto a matriz transitional é validada; as linhas legadas permanecem disponíveis em releases anteriores.
28
28
 
29
29
  Rubies e frameworks antigos estão fora do suporte de segurança de seus mantenedores. A Chronos oferece compatibilidade técnica, não manutenção de segurança do runtime. Veja [Compatibilidade](docs/compatibility.md).
30
30
 
@@ -204,14 +204,16 @@ Por padrão, SQL bruto e binds não são lidos pelo pipeline de análise. A insp
204
204
 
205
205
  ## Sidekiq e Active Job
206
206
 
207
- A versão `0.6.0.pre.1` introduziu middleware Sidekiq 4/5; a API estável mantém o require explícito:
207
+ A integração mantém o require explícito e suporta a API pública de middleware do Sidekiq 7:
208
+
209
+ O suporte histórico começou em `0.6.0.pre.1`; aplicações em Sidekiq 4/5 devem permanecer numa release legada compatível.
208
210
 
209
211
  ```ruby
210
212
  gem "sidekiq", "~> 5.0"
211
213
  gem "chronos-ruby", "~> 1.1.0", :require => "chronos/sidekiq"
212
214
  ```
213
215
 
214
- O envelope de contexto não altera argumentos públicos e contém somente IDs limitados de trace/request. Active Job usa um campo serializado com namespace (`chronos_context`) e hooks públicos. Erros aninhados são deduplicados e reerguidos. Veja [Sidekiq legado](docs/modules/sidekiq-legacy.md), [Active Job](docs/modules/active-job.md) e [Jobs](docs/modules/job-monitoring.md).
216
+ O envelope de contexto não altera argumentos públicos e contém somente IDs limitados de trace/span/request e flags. Active Job usa um campo serializado com namespace (`chronos_context`) e hooks públicos. Erros aninhados são deduplicados e reerguidos. Veja [Sidekiq legado](docs/modules/sidekiq-legacy.md), [Active Job](docs/modules/active-job.md) e [Jobs](docs/modules/job-monitoring.md).
215
217
 
216
218
  ## Deploy tracking
217
219
 
@@ -319,7 +321,7 @@ Execute suíte e lint no runtime atual:
319
321
  bundle _1.17.3_ exec rake
320
322
  ```
321
323
 
322
- A matriz CI cobre Ruby 2.2.10–2.6.10, aplicações Rails 4.2/5.2 e Sidekiq 4/5. O workflow de release repete toda a matriz, documentação, benchmark comparativo e carga antes de publicar.
324
+ A matriz CI cobre a linha transitional e os checks de pacote/documentação; workflows legados preservam evidência das releases anteriores.
323
325
 
324
326
  ## Contribuição
325
327
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Adiado; limite aceito para 1.0 legado.
5
+ Aceito para 1.2.
6
6
 
7
7
  ## Contexto
8
8
 
@@ -10,7 +10,7 @@ O Chronos precisa correlacionar trace/request sem duplicar SDKs ou impor depend
10
10
 
11
11
  ## Decisão
12
12
 
13
- Manter IDs de correlação e portas independentes do fornecedor na 1.0. Não depender de OpenTelemetry nem instalar instrumentação global na linha legado. Uma ponte opcional será projetada na linha transitional/modern e deverá traduzir somente campos permitidos.
13
+ Manter IDs de correlação e portas independentes do fornecedor. Na 1.2, a ponte opcional consulta somente o span atual de um SDK carregado, traduz `trace_id`, `span_id` e flags, e nunca instala SDK, instrumentação ou exporter. IDs Chronos explícitos têm precedência; a presença de OTel deve ser usada pelas integrações para evitar spans paralelos equivalentes.
14
14
 
15
15
  ## Alternativas
16
16
 
@@ -22,4 +22,4 @@ O protocolo v1 permanece estável e aplicações legadas não recebem novas depe
22
22
 
23
23
  ## Consequências negativas
24
24
 
25
- Não propagação automática com ecossistemas OpenTelemetry na 1.0; integrações futuras exigirão contrato e matriz próprios.
25
+ A ponte não exporta spans OTel completos nem controla exporters; consumidores que precisam desses dados continuam responsáveis pela configuração do SDK.
@@ -1,39 +1,33 @@
1
1
  # Compatibility
2
2
 
3
- Chronos Ruby 1.1 is the current stable legacy line. Technical compatibility does not make an end-of-life Ruby, Rails, Rack, or Sidekiq release secure. The planned Ruby 2.7/Rails 6 transitional matrix remains deferred until it has dedicated CI and a real application gate.
3
+ Chronos Ruby 1.2.0.pre.1 is the first candidate of the transitional line for Rails 7 and Sidekiq 7. Combinations remain `Best effort` until their dedicated CI and real-application gates pass. Technical compatibility does not make an end-of-life Ruby, Rails, Rack, or Sidekiq release secure. Versions 1.0 and 1.1 remain the frozen legacy line.
4
4
 
5
5
  ## Core and Rack
6
6
 
7
7
  | Ruby | Status | Evidence |
8
8
  |---|---|---|
9
- | 2.2.10 | Supported | Full unit, integration, contract, Rack, concurrency, fork, transport, privacy, and lint gate |
10
- | 2.3.8 | Supported | Same dedicated Docker gate |
11
- | 2.4.10 | Supported | Same dedicated Docker gate |
12
- | 2.5.9 | Supported | Same dedicated Docker gate |
13
- | 2.6.10 | Supported | Same dedicated Docker gate |
14
- | 2.7 and newer | Unsupported in 1.x legacy | Belongs to the transitional or modern lines |
9
+ | 2.7–3.2 | Best effort | Transitional matrix introduced in this candidate; promotion requires green CI evidence |
10
+ | 3.3–3.4 | Best effort | Core checks run in modern CI; Rails 8-specific behavior belongs to 2.x |
11
+ | Earlier than 2.7 | Unsupported in 1.2 | Use an appropriate frozen legacy release |
15
12
 
16
13
  ## Rails
17
14
 
18
15
  | Ruby | Rails | Status | Evidence |
19
16
  |---|---|---|---|
20
- | 2.2.10 | 4.2 | Supported | Real application boot, successful/error request, SQL, view, cache, Active Job, mailer, fake endpoint, flush, and shutdown |
21
- | 2.3.8 | 4.2 | Supported | Same dedicated application gate |
22
- | 2.5.9 | 5.2 | Supported | Same dedicated application gate |
23
- | 2.6.10 | 5.2 | Supported | Same dedicated application gate |
24
- | Other Ruby/Rails pairs | — | Unsupported | No complete release gate; feature detection alone is not a support claim |
17
+ | 2.7–3.2 | 7.0–7.1 | Best effort | Real Rails applications and the complete integration gate are pending |
18
+ | 3.1–3.4 | 7.2 | Best effort | Feature and package checks; use a validated application combination before production rollout |
19
+ | Rails 4–6 | — | Unsupported in 1.2 | Use a matching earlier Chronos release |
25
20
 
26
21
  ## Sidekiq
27
22
 
28
23
  | Ruby | Sidekiq | Status | Evidence |
29
24
  |---|---|---|---|
30
- | 2.2.10 | 4.2.10 | Supported | Real-gem client/server middleware smoke with context, success/failure, and deduplication |
31
- | 2.5.9 | 5.2.10 | Supported | Same dedicated application gate |
32
- | Other Ruby/Sidekiq pairs | — | Unsupported | No complete release gate |
25
+ | 2.7–3.4 | 7.x | Best effort | Real Sidekiq 7 smoke applications are pending |
26
+ | Sidekiq 4–6 | — | Unsupported in 1.2 | Use a matching earlier Chronos release |
33
27
 
34
28
  Active Job uses the public `serialize`, `deserialize`, and `perform_now` extension points with a bounded namespaced field. Support follows the validated Rails pairs above. Adapters that bypass these hooks require their own evidence.
35
29
 
36
- The release workflow repeats every supported pair before publishing. Historical 1.0 evidence remains in [Version 1.0 readiness](release-1.0-readiness.md); the 1.1 candidate is recorded in [Version 1.1 readiness](release-1.1-readiness.md).
30
+ Historical evidence remains in [Version 1.0 readiness](release-1.0-readiness.md) and [Version 1.1 readiness](release-1.1-readiness.md).
37
31
 
38
32
  Status meanings:
39
33
 
@@ -47,7 +47,7 @@
47
47
  | `sampling_rate` | Optional | `1.0` | Local upper bound for event sampling |
48
48
  | `enabled_event_types` | Optional | exception, request, query, job, cache, external_http, dependencies, deploy, metric_batch | Local allowlist for supported event envelopes |
49
49
  | `max_remote_send_interval` | Optional | `60.0` | Local upper bound for remotely requested send spacing |
50
- | `context_store` | Optional | `:thread_local` | `:thread_local` or an object implementing `get`, `set`, `clear`, and `with_context` |
50
+ | `context_store` | Optional | `:fiber_local` | `:fiber_local`, `:thread_local`, or an object implementing `get`, `set`, `clear`, and `with_context` |
51
51
  | `breadcrumb_capacity` | Optional | `20` | Positive count of newest breadcrumbs retained per execution |
52
52
  | `breadcrumb_max_bytes` | Optional | `2048` | Maximum bytes per normalized breadcrumb; minimum `128` |
53
53
  | `rails_enabled` | Optional | `true` | Enables automatic Rails middleware and subscribers |
@@ -75,6 +75,8 @@
75
75
  | `apm_transaction_max_connections` | Optional | `100` | Maximum concurrently tracked transaction connections; range 1–500 |
76
76
  | `external_http_enabled` | Optional | `false` | Allows explicit per-instance outbound `Net::HTTP` instrumentation |
77
77
  | `external_http_trace_headers` | Optional | `true` | Propagates Chronos trace/request headers on instrumented requests |
78
+ | `w3c_trace_context` | Optional | `false` | Injects a validated W3C `traceparent` header when trace and span IDs exist |
79
+ | `opentelemetry_bridge` | Optional | `true` | Consumes the current OpenTelemetry span when an SDK is already active; installs nothing |
78
80
  | `cache_key_mode` | Optional | `:none` | `:none` omits keys; `:sha256` emits a project-scoped key hash |
79
81
  | `dependency_reporting` | Optional | `true` | Emits one bounded dependency event per configured agent |
80
82
  | `dependency_max_items` | Optional | `100` | Loaded gem entries retained in the inventory; range 1–200 |
@@ -100,7 +102,7 @@ Chronos.configure do |config|
100
102
  config.max_retries = 3
101
103
  config.backlog_size = 100
102
104
  config.circuit_failure_threshold = 5
103
- config.context_store = :thread_local
105
+ config.context_store = :fiber_local
104
106
  config.breadcrumb_capacity = 20
105
107
  config.rails_capture_in_test = false
106
108
  config.rails_capture_in_console = false
@@ -125,6 +127,8 @@ Chronos.configure do |config|
125
127
  config.apm_transaction_max_connections = 100
126
128
  config.external_http_enabled = false
127
129
  config.external_http_trace_headers = true
130
+ config.w3c_trace_context = false
131
+ config.opentelemetry_bridge = true
128
132
  config.cache_key_mode = :none
129
133
  config.dependency_reporting = true
130
134
  config.dependency_max_items = 100
@@ -6,7 +6,7 @@ O contexto relaciona uma exceção ou métrica ao request/job atual sem criar de
6
6
 
7
7
  ## Fluxo e classes
8
8
 
9
- `Chronos::Ports::ContextStore` define `get`, `set`, `clear` e `with_context`. Na linha legado, `Chronos::Adapters::ThreadLocalContextStore` isola o valor por thread. A fachada combina o contexto explícito com o escopo atual; o serializer sanitiza e limita tudo antes da fila. Rack limpa o escopo em `ensure`, enquanto Sidekiq e Active Job propagam apenas `trace_id` e `request_id`.
9
+ `Chronos::Ports::ContextStore` define `get`, `set`, `clear` e `with_context`. Na linha 1.2, `Chronos::Adapters::FiberLocalContextStore` usa storage do Fiber quando disponível e volta com segurança ao adapter thread-local. A fachada combina o contexto explícito com o escopo atual; o serializer sanitiza e limita tudo antes da fila. Rack limpa o escopo em `ensure`, enquanto Sidekiq e Active Job propagam somente `trace_id`, `span_id`, `trace_flags` e `request_id`.
10
10
 
11
11
  ## Extensão, riscos e exemplo
12
12
 
@@ -10,6 +10,7 @@ Chronos.configure do |config|
10
10
  # connection settings omitted
11
11
  config.external_http_enabled = true
12
12
  config.external_http_trace_headers = true
13
+ config.w3c_trace_context = true
13
14
  end
14
15
 
15
16
  http = Net::HTTP.new("payments.example.com", 443)
@@ -18,10 +19,10 @@ Chronos.instrument_net_http(http)
18
19
  response = http.request(Net::HTTP::Get.new("/health"))
19
20
  ```
20
21
 
21
- The event contains a bounded lowercase host, uppercase method, response status, monotonic duration, timeout flag, connection-error flag, and error class. A request made inside a Chronos context receives `X-Chronos-Trace-ID` and `X-Chronos-Request-ID` unless the application already set those headers. Disable propagation with `external_http_trace_headers = false`.
22
+ The event contains a bounded lowercase host, uppercase method, response status, monotonic duration, timeout flag, connection-error flag, and error class. A request made inside a Chronos context receives `X-Chronos-Trace-ID` and `X-Chronos-Request-ID` unless the application already set those headers. With `w3c_trace_context = true`, Net::HTTP and `Chronos::Integrations::FaradayMiddleware` also preserve an existing header or inject a validated `traceparent`. Disable all propagation with `external_http_trace_headers = false`.
22
23
 
23
24
  The wrapper never reads or records the path, query string, Authorization, other request headers, request body, response headers/body, or exception message. The native streaming block is forwarded and the identical HTTP exception is re-raised. Telemetry failures are contained.
24
25
 
25
- Successful and failed calls become bounded `external_http` APM groups keyed only by host and method. A call carrying a trace ID contributes its duration to the enclosing request's `external_http` breakdown. Faraday, HTTP.rb, Excon, and RestClient are outside this release.
26
+ Successful and failed calls become bounded `external_http` APM groups keyed only by host and method. A call carrying a trace ID contributes its duration to the enclosing request's `external_http` breakdown. Faraday is supported through explicit middleware installation; HTTP.rb, Excon, and RestClient remain outside this release.
26
27
 
27
28
  Installation is idempotent per object. A `false` result means collection is disabled, the object is incompatible or already instrumented, or installation was contained after an internal error.
@@ -0,0 +1,57 @@
1
+ module Chronos
2
+ module Adapters
3
+ # Stores execution context on the current Fiber when Ruby exposes Fiber storage.
4
+ #
5
+ # @responsibility Isolate context between concurrent fibers and restore nested scopes.
6
+ # @motivation Rails 7 applications increasingly multiplex work on one thread.
7
+ # @limits Fiber storage is not propagated automatically to newly-created fibers.
8
+ # @collaborators ContextStore port and Agent composition root.
9
+ # @thread_safety Each fiber owns its value and may be used from concurrent threads.
10
+ # @compatibility Ruby 3.2+ Fiber storage; falls back to thread-local storage otherwise.
11
+ # @example
12
+ # store.with_context(:request_id => "r1") { store.get }
13
+ # @errors Previous context is restored even when the block raises.
14
+ # @performance Constant-time storage operations with a bounded hash merge.
15
+ class FiberLocalContextStore
16
+ def initialize(fallback = ThreadLocalContextStore.new)
17
+ @key = "chronos_context_#{object_id}".to_sym
18
+ @fallback = fallback
19
+ end
20
+
21
+ def get
22
+ supported? ? (Fiber[@key] || {}) : @fallback.get
23
+ end
24
+
25
+ def set(context)
26
+ raise ArgumentError, "context must be a Hash" unless context.is_a?(Hash)
27
+
28
+ supported? ? Fiber[@key] = context : @fallback.set(context)
29
+ end
30
+
31
+ def clear
32
+ supported? ? Fiber[@key] = nil : @fallback.clear
33
+ nil
34
+ end
35
+
36
+ def with_context(context)
37
+ previous = get
38
+ set(previous.merge(valid_context(context)))
39
+ yield
40
+ ensure
41
+ previous && !previous.empty? ? set(previous) : clear
42
+ end
43
+
44
+ private
45
+
46
+ def supported?
47
+ Fiber.respond_to?(:[]) && Fiber.respond_to?(:[]=)
48
+ end
49
+
50
+ def valid_context(context)
51
+ raise ArgumentError, "context must be a Hash" unless context.is_a?(Hash)
52
+
53
+ context
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/chronos/agent.rb CHANGED
@@ -148,7 +148,8 @@ module Chronos
148
148
  end
149
149
 
150
150
  def external_http_integration_options
151
- {:enabled => @config.external_http_enabled, :trace_headers => @config.external_http_trace_headers}
151
+ {:enabled => @config.external_http_enabled, :trace_headers => @config.external_http_trace_headers,
152
+ :w3c_trace_context => @config.w3c_trace_context}
152
153
  end
153
154
 
154
155
  def cache_integration_options
@@ -165,6 +166,10 @@ module Chronos
165
166
  "request_id" => nested["request_id"] || nested[:request_id] ||
166
167
  request["request_id"] || request[:request_id]
167
168
  }
169
+ if @config.opentelemetry_bridge
170
+ otel = Integrations::OpenTelemetry.current_context
171
+ values = otel.merge(values.delete_if { |_key, value| value.to_s.empty? })
172
+ end
168
173
  values.delete_if { |_key, value| value.to_s.empty? }
169
174
  rescue StandardError
170
175
  {}
@@ -237,6 +242,7 @@ module Chronos
237
242
 
238
243
  def build_context_store(strategy)
239
244
  return Adapters::ThreadLocalContextStore.new if strategy == :thread_local
245
+ return Adapters::FiberLocalContextStore.new if strategy == :fiber_local
240
246
 
241
247
  strategy
242
248
  end
@@ -110,6 +110,8 @@ module Chronos
110
110
  errors = []
111
111
  errors << "external_http_enabled must be true or false" unless boolean?(external_http_enabled)
112
112
  errors << "external_http_trace_headers must be true or false" unless boolean?(external_http_trace_headers)
113
+ errors << "w3c_trace_context must be true or false" unless boolean?(w3c_trace_context)
114
+ errors << "opentelemetry_bridge must be true or false" unless boolean?(opentelemetry_bridge)
113
115
  errors << "cache_key_mode must be :none or :sha256" unless [:none, :sha256].include?(cache_key_mode)
114
116
  errors << "dependency_reporting must be true or false" unless boolean?(dependency_reporting)
115
117
  unless dependency_max_items.is_a?(Integer) && dependency_max_items >= 1 && dependency_max_items <= 200
@@ -67,8 +67,8 @@ module Chronos
67
67
 
68
68
  def context_errors
69
69
  errors = []
70
- unless context_store == :thread_local || compatible_context_store?
71
- errors << "context_store must be :thread_local or implement get, set, clear, and with_context"
70
+ unless [:thread_local, :fiber_local].include?(context_store) || compatible_context_store?
71
+ errors << "context_store must be :thread_local, :fiber_local, or implement get, set, clear, and with_context"
72
72
  end
73
73
  errors << "breadcrumb_capacity must be a positive integer" unless positive_integer?(breadcrumb_capacity)
74
74
  unless breadcrumb_max_bytes.is_a?(Integer) && breadcrumb_max_bytes >= 128
@@ -54,6 +54,7 @@ module Chronos
54
54
  :apm_query_inspection_min_duration_ms, :apm_query_inspection_max_queries,
55
55
  :apm_transaction_tracking_enabled, :apm_transaction_max_connections,
56
56
  :external_http_enabled, :external_http_trace_headers,
57
+ :w3c_trace_context, :opentelemetry_bridge,
57
58
  :cache_key_mode, :dependency_reporting, :dependency_max_items
58
59
  ].freeze
59
60
 
@@ -123,7 +124,7 @@ module Chronos
123
124
  @user_agent = "chronos-ruby/#{Chronos::VERSION}"
124
125
  @max_payload_size = 1_048_576
125
126
  @gzip = false
126
- @context_store = :thread_local
127
+ @context_store = :fiber_local
127
128
  @breadcrumb_capacity = 20
128
129
  @breadcrumb_max_bytes = 2048
129
130
  end
@@ -167,6 +168,8 @@ module Chronos
167
168
  def initialize_observability_defaults
168
169
  @external_http_enabled = false
169
170
  @external_http_trace_headers = true
171
+ @w3c_trace_context = false
172
+ @opentelemetry_bridge = true
170
173
  @cache_key_mode = :none
171
174
  @dependency_reporting = true
172
175
  @dependency_max_items = 100
@@ -0,0 +1,38 @@
1
+ module Chronos
2
+ module Core
3
+ # Parses and formats the W3C Trace Context headers without an OpenTelemetry dependency.
4
+ module TraceContext
5
+ TRACEPARENT = /\A([\da-f]{2})-([\da-f]{32})-([\da-f]{16})-([\da-f]{2})\z/.freeze
6
+
7
+ module_function
8
+
9
+ def parse(value)
10
+ match = TRACEPARENT.match(value.to_s.downcase)
11
+ return {} unless match && match[1] == "00"
12
+ return {} if match[2] == ("0" * 32) || match[3] == ("0" * 16)
13
+
14
+ {"trace_id" => match[2], "span_id" => match[3], "trace_flags" => match[4]}
15
+ rescue StandardError
16
+ {}
17
+ end
18
+
19
+ def format(context)
20
+ trace_id = value(context, "trace_id")
21
+ span_id = value(context, "span_id")
22
+ flags = value(context, "trace_flags")
23
+ return nil unless trace_id =~ /\A[\da-f]{32}\z/ && span_id =~ /\A[\da-f]{16}\z/
24
+ return nil if trace_id == ("0" * 32) || span_id == ("0" * 16)
25
+
26
+ "00-#{trace_id}-#{span_id}-#{flags =~ /\A[\da-f]{2}\z/ ? flags : '01'}"
27
+ rescue StandardError
28
+ nil
29
+ end
30
+
31
+ def value(context, key)
32
+ return "" unless context.is_a?(Hash)
33
+
34
+ (context[key] || context[key.to_sym]).to_s.downcase
35
+ end
36
+ end
37
+ end
38
+ end
@@ -40,7 +40,7 @@ module Chronos
40
40
  def envelope(notifier)
41
41
  source = notifier.respond_to?(:propagation_context) ? notifier.propagation_context : {}
42
42
  source = {} unless source.is_a?(Hash)
43
- context = %w(trace_id request_id).each_with_object({}) do |key, result|
43
+ context = %w(trace_id span_id trace_flags request_id).each_with_object({}) do |key, result|
44
44
  value = source[key] || source[key.to_sym]
45
45
  result[key] = bounded(value) unless value.to_s.empty?
46
46
  end
@@ -57,7 +57,7 @@ module Chronos
57
57
  source = value["context"] || value[:context]
58
58
  return {} unless source.is_a?(Hash)
59
59
 
60
- %w(trace_id request_id).each_with_object({}) do |key, result|
60
+ %w(trace_id span_id trace_flags request_id).each_with_object({}) do |key, result|
61
61
  candidate = source[key] || source[key.to_sym]
62
62
  result[key] = bounded(candidate) unless candidate.to_s.empty?
63
63
  end
@@ -0,0 +1,76 @@
1
+ module Chronos
2
+ module Integrations
3
+ # Optional Faraday middleware for outbound timing and W3C propagation.
4
+ # @responsibility Instrument one explicit Faraday connection without global patches.
5
+ # @motivation Modern Rails applications commonly use Faraday for outbound HTTP.
6
+ # @limits It never reads paths, queries, headers other than trace fields, or bodies.
7
+ # @collaborators Faraday middleware stack, TraceContext, and Chronos facade.
8
+ # @thread_safety Calls retain request state in local variables.
9
+ # @compatibility Faraday 1.x and 2.x middleware contracts.
10
+ # @example builder.use Chronos::Integrations::FaradayMiddleware
11
+ # @errors Original client exceptions are recorded by class and re-raised.
12
+ # @performance Two clock reads and one bounded asynchronous event per request.
13
+ class FaradayMiddleware
14
+ def initialize(app, options = {})
15
+ @app = app
16
+ @notifier = options[:notifier] || Chronos
17
+ @clock = options[:clock] || proc { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
18
+ end
19
+
20
+ def call(environment)
21
+ started_at = @clock.call
22
+ inject_headers(environment)
23
+ response = @app.call(environment)
24
+ if response.respond_to?(:on_complete)
25
+ response.on_complete { |completed| record(completed, started_at, nil) }
26
+ else
27
+ record(environment, started_at, nil)
28
+ end
29
+ response
30
+ rescue StandardError => error
31
+ record(environment, started_at, error) if started_at
32
+ raise
33
+ end
34
+
35
+ private
36
+
37
+ def inject_headers(environment)
38
+ options = integration_options
39
+ return unless options[:trace_headers] && environment.respond_to?(:request_headers)
40
+
41
+ context = @notifier.respond_to?(:propagation_context) ? @notifier.propagation_context : {}
42
+ headers = environment.request_headers
43
+ traceparent = Core::TraceContext.format(context) if options[:w3c_trace_context]
44
+ headers["traceparent"] ||= traceparent if traceparent
45
+ headers["X-Chronos-Trace-ID"] ||= context["trace_id"] if context["trace_id"]
46
+ headers["X-Chronos-Request-ID"] ||= context["request_id"] if context["request_id"]
47
+ rescue StandardError
48
+ nil
49
+ end
50
+
51
+ def record(environment, started_at, error)
52
+ url = environment.url if environment.respond_to?(:url)
53
+ payload = {
54
+ "host" => (url.host.to_s if url && url.respond_to?(:host)),
55
+ "method" => (environment.method.to_s.upcase if environment.respond_to?(:method)),
56
+ "status" => (environment.status.to_i if environment.respond_to?(:status) && environment.status),
57
+ "duration_ms" => ((@clock.call - started_at) * 1000.0).round(3),
58
+ "error_class" => (error.class.name.to_s if error)
59
+ }
60
+ payload.delete_if { |_key, value| value.nil? || value == "" }
61
+ @notifier.record_event("external_http", payload)
62
+ rescue StandardError
63
+ false
64
+ end
65
+
66
+ def integration_options
67
+ return {:enabled => true, :trace_headers => true, :w3c_trace_context => false} unless
68
+ @notifier.respond_to?(:external_http_integration_options)
69
+
70
+ @notifier.external_http_integration_options
71
+ rescue StandardError
72
+ {:enabled => false, :trace_headers => false, :w3c_trace_context => false}
73
+ end
74
+ end
75
+ end
76
+ end
@@ -53,7 +53,8 @@ module Chronos
53
53
  {
54
54
  :notifier => notifier,
55
55
  :clock => options[:clock] || proc { monotonic_time },
56
- :trace_headers => configured.fetch(:trace_headers, true)
56
+ :trace_headers => configured.fetch(:trace_headers, true),
57
+ :w3c_trace_context => configured.fetch(:w3c_trace_context, false)
57
58
  }
58
59
  end
59
60
 
@@ -106,6 +107,9 @@ module Chronos
106
107
  end
107
108
  set_header(request, "X-Chronos-Trace-ID", context["trace_id"] || context[:trace_id])
108
109
  set_header(request, "X-Chronos-Request-ID", context["request_id"] || context[:request_id])
110
+ if options[:w3c_trace_context]
111
+ set_header(request, "traceparent", Core::TraceContext.format(context))
112
+ end
109
113
  rescue StandardError
110
114
  nil
111
115
  end
@@ -0,0 +1,44 @@
1
+ module Chronos
2
+ module Integrations
3
+ # Optional, dependency-free bridge to an already configured OpenTelemetry SDK.
4
+ module OpenTelemetry
5
+ module_function
6
+
7
+ def current_context
8
+ return {} unless defined?(::OpenTelemetry::Trace) && ::OpenTelemetry::Trace.respond_to?(:current_span)
9
+
10
+ span = ::OpenTelemetry::Trace.current_span
11
+ context = span.context if span && span.respond_to?(:context)
12
+ return {} unless context && (!context.respond_to?(:valid?) || context.valid?)
13
+
14
+ trace_id = hex_identifier(context, :hex_trace_id, :trace_id, 32)
15
+ span_id = hex_identifier(context, :hex_span_id, :span_id, 16)
16
+ return {} if trace_id.empty? || span_id.empty?
17
+
18
+ {"trace_id" => trace_id, "span_id" => span_id,
19
+ "trace_flags" => sampled?(context) ? "01" : "00", "source" => "opentelemetry"}
20
+ rescue StandardError
21
+ {}
22
+ end
23
+
24
+ def active?
25
+ !current_context.empty?
26
+ end
27
+
28
+ def hex_identifier(context, hex_method, numeric_method, width)
29
+ value = context.public_send(hex_method) if context.respond_to?(hex_method)
30
+ value ||= context.public_send(numeric_method).to_i.to_s(16) if context.respond_to?(numeric_method)
31
+ value.to_s.downcase.rjust(width, "0")[-width, width]
32
+ rescue StandardError
33
+ ""
34
+ end
35
+
36
+ def sampled?(context)
37
+ flags = context.trace_flags if context.respond_to?(:trace_flags)
38
+ flags.respond_to?(:sampled?) ? flags.sampled? : flags.to_i.odd?
39
+ rescue StandardError
40
+ false
41
+ end
42
+ end
43
+ end
44
+ end
@@ -62,7 +62,7 @@ module Chronos
62
62
  def request_capture_context(env)
63
63
  request = request_values(env)
64
64
  {
65
- :context => {"request" => request, "trace_id" => trace_id(env)},
65
+ :context => trace_context(env).merge("request" => request),
66
66
  :parameters => parameters(env),
67
67
  :user => hash_value(env["chronos.user"])
68
68
  }
@@ -124,7 +124,13 @@ module Chronos
124
124
  end
125
125
 
126
126
  def trace_id(env)
127
- env["chronos.trace_id"] || SecureRandom.uuid
127
+ trace_context(env)["trace_id"]
128
+ end
129
+
130
+ def trace_context(env)
131
+ parsed = Core::TraceContext.parse(env["HTTP_TRACEPARENT"])
132
+ parsed["trace_id"] = env["chronos.trace_id"] || SecureRandom.hex(16) if parsed.empty?
133
+ parsed
128
134
  end
129
135
 
130
136
  def response_size(headers)
@@ -110,7 +110,7 @@ module Chronos
110
110
  source = @notifier.propagation_context
111
111
  return {} unless source.is_a?(Hash)
112
112
 
113
- %w(trace_id request_id).each_with_object({}) do |key, result|
113
+ %w(trace_id span_id trace_flags request_id).each_with_object({}) do |key, result|
114
114
  value = source[key] || source[key.to_sym]
115
115
  result[key] = value.to_s unless value.to_s.empty?
116
116
  end
@@ -0,0 +1,39 @@
1
+ module Chronos
2
+ module Rails
3
+ # Sends exceptions observed by the Rails 7 error reporter to Chronos once.
4
+ # @responsibility Translate the public Rails reporter callback into one notice.
5
+ # @motivation Rails can report handled errors that never reach Rack middleware.
6
+ # @limits Only handled, severity, source, and one bounded component are retained.
7
+ # @collaborators ActiveSupport::ErrorReporter and Chronos facade.
8
+ # @thread_safety Instances contain only an immutable notifier reference.
9
+ # @compatibility Rails 7 public error reporter subscriber API.
10
+ # @example Rails.error.subscribe(ErrorReporterSubscriber.new)
11
+ # @errors Agent and context failures are contained and return false.
12
+ # @performance Allocates one small allowlisted metadata hash per report.
13
+ class ErrorReporterSubscriber
14
+ def initialize(notifier = Chronos)
15
+ @notifier = notifier
16
+ end
17
+
18
+ def report(error, handled:, severity:, context:, source: nil)
19
+ details = {
20
+ :context => {"rails_error_reporter" => {
21
+ "handled" => handled == true, "severity" => severity.to_s, "source" => source.to_s
22
+ }}
23
+ }
24
+ details[:context]["rails_error_reporter"]["component"] = component(context)
25
+ @notifier.notify_once(error, details)
26
+ rescue StandardError
27
+ false
28
+ end
29
+
30
+ private
31
+
32
+ def component(context)
33
+ return "" unless context.is_a?(Hash)
34
+
35
+ (context[:controller] || context["controller"] || context[:job] || context["job"]).to_s[0, 128]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -34,6 +34,7 @@ module Chronos
34
34
 
35
35
  install_middleware(application, options)
36
36
  install_active_job
37
+ install_error_reporter
37
38
  @subscriber.install
38
39
  self.class.applications[application.object_id] = true
39
40
  end
@@ -65,6 +66,18 @@ module Chronos
65
66
  Chronos::Integrations::ActiveJob.install(::ActiveJob::Base, @notifier)
66
67
  end
67
68
 
69
+ def install_error_reporter
70
+ return false unless defined?(::Rails) && ::Rails.respond_to?(:error)
71
+
72
+ reporter = ::Rails.error
73
+ return false unless reporter && reporter.respond_to?(:subscribe)
74
+
75
+ reporter.subscribe(ErrorReporterSubscriber.new(@notifier))
76
+ true
77
+ rescue StandardError
78
+ false
79
+ end
80
+
68
81
  def environment
69
82
  defined?(::Rails) && ::Rails.respond_to?(:env) ? ::Rails.env.to_s : nil
70
83
  end
@@ -17,6 +17,7 @@ module Chronos
17
17
  process_action.action_controller render_template.action_view sql.active_record
18
18
  deliver.action_mailer perform.active_job cache_read.active_support
19
19
  cache_write.active_support cache_fetch_hit.active_support
20
+ perform_action.action_cable transmit.action_cable broadcast.action_cable
20
21
  ).freeze
21
22
 
22
23
  @mutex = Mutex.new
@@ -105,6 +106,8 @@ module Chronos
105
106
  when "sql.active_record" then sql(payload, duration)
106
107
  when "deliver.action_mailer" then mailer(payload, duration)
107
108
  when "perform.active_job" then active_job(payload, duration)
109
+ when "perform_action.action_cable", "transmit.action_cable", "broadcast.action_cable"
110
+ action_cable(name, payload, duration)
108
111
  else cache(name, payload, duration)
109
112
  end
110
113
  end
@@ -345,6 +348,15 @@ module Chronos
345
348
  @notifier.record_event("cache", data)
346
349
  end
347
350
 
351
+ def action_cable(name, payload, duration)
352
+ data = {
353
+ "kind" => "action_cable", "operation" => name.split(".").first,
354
+ "channel" => safe_class_name(value(payload, :channel)),
355
+ "action" => value(payload, :action).to_s[0, 128], "duration_ms" => duration
356
+ }
357
+ @notifier.record_event("request", data)
358
+ end
359
+
348
360
  def capture_controller_exception(payload)
349
361
  exception = value(payload, :exception_object)
350
362
  details = value(payload, :exception)
data/lib/chronos/rails.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "chronos"
2
2
  require "chronos/rails/active_record_query_inspector"
3
3
  require "chronos/rails/notifications_subscriber"
4
+ require "chronos/rails/error_reporter_subscriber"
4
5
  require "chronos/integrations/active_job"
5
6
  require "chronos/rails/installer"
6
7
 
@@ -1,4 +1,4 @@
1
1
  module Chronos
2
- # Current version of the legacy Chronos Ruby agent.
3
- VERSION = "1.1.0".freeze
2
+ # Current version of the transitional Chronos Ruby agent.
3
+ VERSION = "1.2.0.pre.1".freeze
4
4
  end
data/lib/chronos.rb CHANGED
@@ -16,6 +16,7 @@ require "chronos/core/sensitive_value_filter"
16
16
  require "chronos/core/sanitizer"
17
17
  require "chronos/core/safe_serializer"
18
18
  require "chronos/core/correlation_context"
19
+ require "chronos/core/trace_context"
19
20
  require "chronos/core/deploy_normalizer"
20
21
  require "chronos/core/payload_serializer"
21
22
  require "chronos/core/telemetry_event"
@@ -32,6 +33,7 @@ require "chronos/internal/memory_backlog"
32
33
  require "chronos/internal/worker_pool"
33
34
  require "chronos/adapters/net_http_transport"
34
35
  require "chronos/adapters/thread_local_context_store"
36
+ require "chronos/adapters/fiber_local_context_store"
35
37
  require "chronos/core/breadcrumb"
36
38
  require "chronos/application/retry_policy"
37
39
  require "chronos/application/circuit_breaker"
@@ -49,6 +51,8 @@ require "chronos/observability_facade"
49
51
  require "chronos/integrations"
50
52
  require "chronos/integrations/rack"
51
53
  require "chronos/integrations/rack/middleware"
54
+ require "chronos/integrations/opentelemetry"
55
+ require "chronos/integrations/faraday"
52
56
 
53
57
  # Framework-independent public facade for the Chronos Ruby agent.
54
58
  #
metadata CHANGED
@@ -1,93 +1,91 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chronos-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antonio Jefferson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-05 00:00:00.000000000 Z
11
+ date: 2026-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.17'
19
+ version: '2.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
20
23
  type: :development
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.1'
30
+ - - "<"
25
31
  - !ruby/object:Gem::Version
26
- version: '1.17'
32
+ version: '3'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rake
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
- - - "~>"
37
+ - - ">="
32
38
  - !ruby/object:Gem::Version
33
39
  version: '12.3'
34
- - - ">="
40
+ - - "<"
35
41
  - !ruby/object:Gem::Version
36
- version: 12.3.3
42
+ version: '14'
37
43
  type: :development
38
44
  prerelease: false
39
45
  version_requirements: !ruby/object:Gem::Requirement
40
46
  requirements:
41
- - - "~>"
47
+ - - ">="
42
48
  - !ruby/object:Gem::Version
43
49
  version: '12.3'
44
- - - ">="
50
+ - - "<"
45
51
  - !ruby/object:Gem::Version
46
- version: 12.3.3
52
+ version: '14'
47
53
  - !ruby/object:Gem::Dependency
48
54
  name: rspec
49
55
  requirement: !ruby/object:Gem::Requirement
50
56
  requirements:
51
- - - "~>"
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '3.10'
60
+ - - "<"
52
61
  - !ruby/object:Gem::Version
53
- version: '3.0'
62
+ version: '4'
54
63
  type: :development
55
64
  prerelease: false
56
65
  version_requirements: !ruby/object:Gem::Requirement
57
66
  requirements:
58
- - - "~>"
67
+ - - ">="
59
68
  - !ruby/object:Gem::Version
60
- version: '3.0'
69
+ version: '3.10'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '4'
61
73
  - !ruby/object:Gem::Dependency
62
74
  name: rubocop
63
75
  requirement: !ruby/object:Gem::Requirement
64
76
  requirements:
65
77
  - - "~>"
66
78
  - !ruby/object:Gem::Version
67
- version: 0.49.0
79
+ version: 1.57.0
68
80
  type: :development
69
81
  prerelease: false
70
82
  version_requirements: !ruby/object:Gem::Requirement
71
83
  requirements:
72
84
  - - "~>"
73
85
  - !ruby/object:Gem::Version
74
- version: 0.49.0
75
- - !ruby/object:Gem::Dependency
76
- name: parallel
77
- requirement: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - '='
80
- - !ruby/object:Gem::Version
81
- version: 1.19.2
82
- type: :development
83
- prerelease: false
84
- version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - '='
87
- - !ruby/object:Gem::Version
88
- version: 1.19.2
89
- description: Base do cliente Chronos para excecoes, telemetria e observabilidade em
90
- aplicacoes Ruby legadas.
86
+ version: 1.57.0
87
+ description: Cliente Chronos para excecoes, telemetria e observabilidade em aplicacoes
88
+ Ruby e Rails.
91
89
  email:
92
90
  - antoniojeferson96@gmail.com
93
91
  executables: []
@@ -171,6 +169,7 @@ files:
171
169
  - docs/troubleshooting.md
172
170
  - lib/chronos.rb
173
171
  - lib/chronos/adapters.rb
172
+ - lib/chronos/adapters/fiber_local_context_store.rb
174
173
  - lib/chronos/adapters/net_http_transport.rb
175
174
  - lib/chronos/adapters/thread_local_context_store.rb
176
175
  - lib/chronos/agent.rb
@@ -210,12 +209,15 @@ files:
210
209
  - lib/chronos/core/sql_normalizer.rb
211
210
  - lib/chronos/core/sql_query_analyzer.rb
212
211
  - lib/chronos/core/telemetry_event.rb
212
+ - lib/chronos/core/trace_context.rb
213
213
  - lib/chronos/errors.rb
214
214
  - lib/chronos/integrations.rb
215
215
  - lib/chronos/integrations/active_job.rb
216
216
  - lib/chronos/integrations/capistrano.rb
217
+ - lib/chronos/integrations/faraday.rb
217
218
  - lib/chronos/integrations/job_payload.rb
218
219
  - lib/chronos/integrations/net_http.rb
220
+ - lib/chronos/integrations/opentelemetry.rb
219
221
  - lib/chronos/integrations/rack.rb
220
222
  - lib/chronos/integrations/rack/middleware.rb
221
223
  - lib/chronos/integrations/sidekiq.rb
@@ -232,6 +234,7 @@ files:
232
234
  - lib/chronos/ports/transport.rb
233
235
  - lib/chronos/rails.rb
234
236
  - lib/chronos/rails/active_record_query_inspector.rb
237
+ - lib/chronos/rails/error_reporter_subscriber.rb
235
238
  - lib/chronos/rails/installer.rb
236
239
  - lib/chronos/rails/notifications_subscriber.rb
237
240
  - lib/chronos/rails/railtie.rb
@@ -258,15 +261,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
258
261
  requirements:
259
262
  - - ">="
260
263
  - !ruby/object:Gem::Version
261
- version: 2.2.10
264
+ version: '2.7'
262
265
  - - "<"
263
266
  - !ruby/object:Gem::Version
264
- version: '2.7'
267
+ version: '3.5'
265
268
  required_rubygems_version: !ruby/object:Gem::Requirement
266
269
  requirements:
267
- - - ">="
270
+ - - ">"
268
271
  - !ruby/object:Gem::Version
269
- version: '0'
272
+ version: 1.3.1
270
273
  requirements: []
271
274
  rubygems_version: 3.4.22
272
275
  signing_key: