chronos-ruby 0.3.0.pre.1 → 0.4.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: db591fb9c6019444d079a5a5c683be8776b68010e3665e4a09656c2bdf568b46
4
- data.tar.gz: 601f34cdaa1f9e06e2748a820b4228bad3c354a8c7feb562473d4268dc217117
3
+ metadata.gz: 1ab10ffdc0cff4274241465904b18e4bceb4fa230ea9fddb4b8be419c08e3463
4
+ data.tar.gz: 2d6d778012088192efed14e0ab64958716c8a6b4c3e697127c52573dc4dcd47b
5
5
  SHA512:
6
- metadata.gz: 1c4adaeb1222449ba2b7a736f1e28e701d00a1faf3733f42ffa753759854c3deed7a4c9e4c33f2ea997aa9445f057b51641b3829fb49b2012e6668687a1df4b0
7
- data.tar.gz: 3809e42fbaed2f3290464cdc7a1e962a70e39fd8c3f7071b6016ee53c53406fca84a250f375e0a46506560c9bdc603f4ceb13721b79b4c6225b28a1e94abdf49
6
+ metadata.gz: a8bb789313b2b7d360b6a8c08e5b5cf731aa1325912be7b560a1869b3d8717e4e779610e92591c1a2c436f5d14cd39e28f1e5db837d954def1db232788284d2d
7
+ data.tar.gz: 93a311d6905d83dee031a88cb5887854a32e60d26eceb0dc0446a8244c77a2c9fa763089bc5ec5fa25d265cb5881c3775dafe6361b1e84a1c6044aa89df220ee
data/CHANGELOG.md CHANGED
@@ -11,6 +11,29 @@ All notable changes are documented here. The project follows Semantic Versioning
11
11
  - legacy CI now resolves Bundler 1.17.3 through `Gem.bin_path` on RubyGems versions that do not support the `_version_` executable selector.
12
12
  - documentation verification now reads source and Markdown files explicitly as UTF-8 on legacy container locales.
13
13
 
14
+ ## [0.4.0.pre.1] - 2026-07-20
15
+
16
+ ### Added
17
+
18
+ - Rack-protocol middleware for automatic exception capture with propagation of the original failure;
19
+ - request context for method, normalized route, status, duration, request ID, optional user agent, host, path, controller/action, approximate response size, user, and sanitized parameters;
20
+ - selectable context-store port with a thread-local legacy adapter and guaranteed scope cleanup;
21
+ - bounded circular breadcrumbs for custom, log, request, query, external HTTP, cache, and job categories;
22
+ - public `Chronos.with_context` and `Chronos.add_breadcrumb` APIs;
23
+ - Rack context contract, concurrency tests, module documentation, ADR, executable example, and request-overhead benchmark.
24
+
25
+ ### Changed
26
+
27
+ - manual notifications now inherit the current execution context and breadcrumbs;
28
+ - version advanced to `0.4.0.pre.1`.
29
+
30
+ ### Known limitations
31
+
32
+ - generic route normalization cannot identify every application-specific dynamic segment;
33
+ - thread-local context does not propagate into application-created threads or fibers;
34
+ - deferred exceptions raised only while enumerating a streaming response body are not captured;
35
+ - Rails-specific installation and route discovery remain planned for version 0.5.
36
+
14
37
  ## [0.3.0.pre.1] - 2026-07-20
15
38
 
16
39
  ### Added
data/README.md CHANGED
@@ -1,16 +1,18 @@
1
1
  # Chronos Ruby
2
2
 
3
- Chronos Ruby is the framework-independent client for sending Ruby application errors to Chronos. Version 0.3 adds bounded resilience to the privacy-focused legacy foundation: manual exceptions are sanitized before queueing, retried under a finite policy, retained in a fixed-size memory backlog during outages, and controlled by a restricted remote policy.
3
+ Chronos Ruby is the framework-independent client for sending Ruby application errors to Chronos. Version 0.4 adds automatic Rack exception capture, isolated request context, and bounded breadcrumbs to the privacy and resilience foundations from earlier releases.
4
4
 
5
5
  ## What the gem collects
6
6
 
7
- For each exception, version 0.3 can collect:
7
+ For each exception, version 0.4 can collect:
8
8
 
9
9
  - exception class, message, structured backtrace, and chained causes;
10
10
  - timestamp, severity, tags, and an optional fingerprint;
11
11
  - application-supplied context, parameters, session, and user fields;
12
12
  - Ruby version, engine, platform, process ID, opaque thread ID, and hostname;
13
13
  - application version, environment, and service name.
14
+ - Rack method, normalized route, status, duration, request ID, host, query-free path, optional user agent, controller/action, response size, trace ID, and already-parsed parameters when the middleware is used;
15
+ - bounded breadcrumbs explicitly supplied by the application or integration.
14
16
 
15
17
  See [Data collected](docs/data-collected.md) for the complete field table.
16
18
 
@@ -20,7 +22,7 @@ Chronos Ruby does not inspect environment variables, request bodies, cookies, HT
20
22
 
21
23
  ## Supported Ruby and Rails versions
22
24
 
23
- Version 0.x targets Ruby 2.2.10 through Ruby 2.6. Version 0.3 is independent of Rails and does not yet declare support for any Rails integration. All supported combinations must pass dedicated CI before being listed as supported.
25
+ Version 0.x targets Ruby 2.2.10 through Ruby 2.6. Version 0.4 implements the Rack protocol but does not yet declare Rails support. All supported combinations must pass dedicated CI before being listed as supported.
24
26
 
25
27
  See [Compatibility](docs/compatibility.md).
26
28
 
@@ -29,7 +31,7 @@ See [Compatibility](docs/compatibility.md).
29
31
  The current public build is a pre-release. Add its exact version to the application's `Gemfile`:
30
32
 
31
33
  ```ruby
32
- gem "chronos-ruby", "0.3.0.pre.1"
34
+ gem "chronos-ruby", "0.4.0.pre.1"
33
35
  ```
34
36
 
35
37
  Install with a Bundler version compatible with the application. For the oldest supported runtime:
@@ -47,7 +49,7 @@ gem install chronos-ruby --pre
47
49
 
48
50
  ## Rails installation
49
51
 
50
- Rails automatic integration is not part of version 0.3. A Rails application may use the plain Ruby API, but this does not constitute declared Rails support. Rack and Rails adapters are planned for later legacy releases.
52
+ Rails automatic installation is not part of version 0.4. A Rails application may install the Rack middleware manually, but this does not constitute declared Rails support. Railtie and generator support are planned for version 0.5.
51
53
 
52
54
  ## Minimum configuration
53
55
 
@@ -70,7 +72,14 @@ HTTPS verification is enabled by default. HTTP requires explicitly setting `ssl_
70
72
 
71
73
  ## Automatic capture
72
74
 
73
- Automatic exception capture is not implemented in version 0.3. Applications must call `Chronos.notify` or `Chronos.notify_sync`. Rack, Rails, and worker hooks will be introduced only after their compatibility suites exist.
75
+ Rack applications can capture unhandled exceptions automatically and preserve the application error semantics:
76
+
77
+ ```ruby
78
+ use Chronos::Integrations::Rack::Middleware,
79
+ :include_user_agent => false
80
+ ```
81
+
82
+ The middleware notifies asynchronously and re-raises the same exception. It never reads the request body, raw query string, cookies, authorization headers, or response body. See [Rack integration and context](docs/modules/rack-context.md).
74
83
 
75
84
  ## Manual capture
76
85
 
@@ -101,15 +110,25 @@ User data is opt-in and must contain only values your application is allowed to
101
110
  Chronos.notify(error, :user => {"id" => "customer-42", "role" => "operator"})
102
111
  ```
103
112
 
104
- Version 0.3 sanitizes this context before delivery and before it can enter retry storage. Data minimization remains the application's responsibility.
113
+ Version 0.4 sanitizes this context before delivery and before it can enter retry storage. Data minimization remains the application's responsibility.
105
114
 
106
115
  ## Breadcrumbs
107
116
 
108
- Breadcrumbs are not implemented in version 0.3.
117
+ Breadcrumbs use a fixed circular buffer scoped to the current execution:
118
+
119
+ ```ruby
120
+ Chronos.add_breadcrumb(
121
+ :category => "custom",
122
+ :message => "payment started",
123
+ :metadata => {"provider" => "example"}
124
+ )
125
+ ```
126
+
127
+ No log, SQL, HTTP, cache, job, request body, or response body payload is collected automatically. Unknown categories become `custom`, and metadata is bounded and sanitized before queueing.
109
128
 
110
129
  ## Filters and LGPD
111
130
 
112
- Version 0.3 recursively redacts sensitive keys and detects Bearer tokens, JWTs, e-mail addresses, CPF, CNPJ, and valid payment-card candidates in free text. IPv4 addresses are anonymized by default. Applications can add blocklist matchers, hash selected identifiers, or install custom filters:
131
+ Version 0.4 recursively redacts sensitive keys and detects Bearer tokens, JWTs, e-mail addresses, CPF, CNPJ, and valid payment-card candidates in free text. IPv4 addresses are anonymized by default. Applications can add blocklist matchers, hash selected identifiers, or install custom filters:
113
132
 
114
133
  ```ruby
115
134
  Chronos.configure do |config|
@@ -133,19 +152,19 @@ Chronos.configure do |config|
133
152
  end
134
153
  ```
135
154
 
136
- Local exception-specific ignore callbacks are not available in version 0.3. The server may provide a bounded list of exact fingerprints to ignore; it cannot provide regular expressions or executable rules.
155
+ Local exception-specific ignore callbacks are not available in version 0.4. The server may provide a bounded list of exact fingerprints to ignore; it cannot provide regular expressions or executable rules.
137
156
 
138
157
  ## Performance monitoring
139
158
 
140
- Request, SQL, cache, job, and external HTTP monitoring are not implemented in version 0.3. The local capture pipeline is bounded and HTTP delivery runs outside the caller thread when `Chronos.notify` is used.
159
+ Request timing events, SQL, cache, job, and external HTTP monitoring are not implemented in version 0.4. Rack exception context is bounded, and HTTP delivery runs outside the caller thread when `Chronos.notify` is used.
141
160
 
142
161
  ## Sidekiq and Active Job
143
162
 
144
- Sidekiq and Active Job integrations are not implemented in version 0.3. Calling the manual API from a job is possible, but automatic capture and deduplication are not yet guaranteed.
163
+ Sidekiq and Active Job integrations are not implemented in version 0.4. Calling the manual API from a job is possible, but automatic capture and deduplication are not yet guaranteed.
145
164
 
146
165
  ## Deploy tracking
147
166
 
148
- Deploy notifications are not implemented in version 0.3. `app_version` may be included in exception events for release correlation.
167
+ Deploy notifications are not implemented in version 0.4. `app_version` may be included in exception events for release correlation.
149
168
 
150
169
  ## Asynchronous queue
151
170
 
@@ -168,7 +187,7 @@ Use `Chronos.flush(timeout)` to wait for accepted events and `Chronos.close(time
168
187
 
169
188
  ## Retry and backlog
170
189
 
171
- Version 0.3 retries network errors, HTTP `408`, `429`, and `5xx` responses with exponential backoff, bounded jitter, and a finite attempt count. Other `4xx` responses are permanent and are not retried. A circuit breaker pauses requests after repeated failures, preventing retry storms.
190
+ The resilience layer introduced in version 0.3 retries network errors, HTTP `408`, `429`, and `5xx` responses with exponential backoff, bounded jitter, and a finite attempt count. Other `4xx` responses are permanent and are not retried. A circuit breaker pauses requests after repeated failures, preventing retry storms.
172
191
 
173
192
  After retries are exhausted, the already sanitized `SerializedEvent` may enter a fixed-capacity memory backlog. The backlog drops new items when full, is lost when the process exits, and never writes to disk. A later successful half-open probe drains backlog items as new events arrive.
174
193
 
@@ -182,7 +201,8 @@ The code follows hexagonal boundaries:
182
201
  - `Chronos::Application` coordinates capture;
183
202
  - `Chronos::Application::DeliveryPipeline` owns bounded retry and remote policy;
184
203
  - `Chronos::Ports` defines delivery behavior;
185
- - `Chronos::Adapters` implements Net::HTTP delivery;
204
+ - `Chronos::Adapters` implements Net::HTTP delivery and thread-local context;
205
+ - `Chronos::Integrations::Rack` implements optional automatic Rack capture;
186
206
  - `Chronos::Internal` owns bounded queueing, workers, and defensive logging.
187
207
 
188
208
  The core has no dependency on Rails, Rack, Sidekiq, or ActiveSupport. See [Architecture](docs/architecture.md).
@@ -210,6 +230,9 @@ Chronos.configure do |config|
210
230
  config.circuit_failure_threshold = 5
211
231
  config.circuit_reset_timeout = 30.0
212
232
  config.remote_configuration = true
233
+ config.context_store = :thread_local
234
+ config.breadcrumb_capacity = 20
235
+ config.breadcrumb_max_bytes = 2048
213
236
  end
214
237
  ```
215
238
 
@@ -221,7 +244,7 @@ Configuration errors are raised during `Chronos.configure`. Capture and delivery
221
244
 
222
245
  ## Benchmark
223
246
 
224
- Run the version 0.3 benchmarks with:
247
+ Run the version 0.4 benchmarks with:
225
248
 
226
249
  ```bash
227
250
  bundle _1.17.3_ exec ruby benchmarks/capture_exception.rb
@@ -229,13 +252,14 @@ bundle _1.17.3_ exec ruby benchmarks/serialization.rb
229
252
  bundle _1.17.3_ exec ruby benchmarks/filtering.rb
230
253
  bundle _1.17.3_ exec ruby benchmarks/queue.rb
231
254
  bundle _1.17.3_ exec ruby benchmarks/retry_backlog.rb
255
+ bundle _1.17.3_ exec ruby benchmarks/request_overhead.rb
232
256
  ```
233
257
 
234
258
  Results depend on runtime, hardware, and payload. No performance comparison is claimed until repeatable measurements are published.
235
259
 
236
260
  ## Migration from Airbrake
237
261
 
238
- An Airbrake migration guide will be added before the legacy 1.0 release. Version 0.3 does not claim API compatibility or automatic replacement.
262
+ An Airbrake migration guide will be added before the legacy 1.0 release. Version 0.4 does not claim API compatibility or automatic replacement.
239
263
 
240
264
  ## Local development
241
265
 
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://chronos.example/schemas/rack-context-v1.schema.json",
4
+ "title": "Chronos Rack exception context v1",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": ["request", "trace_id", "breadcrumbs"],
8
+ "properties": {
9
+ "trace_id": {"type": "string", "minLength": 1},
10
+ "request": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["method", "route", "status", "duration_ms", "request_id", "host", "path"],
14
+ "properties": {
15
+ "method": {"type": "string"},
16
+ "route": {"type": "string"},
17
+ "status": {"type": "integer"},
18
+ "duration_ms": {"type": "number", "minimum": 0},
19
+ "request_id": {"type": ["string", "null"]},
20
+ "user_agent": {"type": ["string", "null"]},
21
+ "host": {"type": "string"},
22
+ "path": {"type": "string"},
23
+ "controller": {"type": ["string", "null"]},
24
+ "action": {"type": ["string", "null"]},
25
+ "response_size": {"type": ["integer", "null"], "minimum": 0}
26
+ }
27
+ },
28
+ "breadcrumbs": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "required": ["category", "message", "metadata", "timestamp"],
34
+ "properties": {
35
+ "category": {"enum": ["custom", "log", "request", "query", "external_http", "cache", "job"]},
36
+ "message": {"type": "string"},
37
+ "metadata": {"type": "object"},
38
+ "timestamp": {"type": "string"}
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,27 @@
1
+ # ADR-012 — Rack capture with selectable execution context
2
+
3
+ ## Status
4
+
5
+ Accepted for version 0.4.
6
+
7
+ ## Context
8
+
9
+ Automatic Rack capture needs request-scoped state, but the legacy line cannot rely on modern fiber-local APIs. Loading the plain Ruby gem must not force Rack into applications that do not use it. Request and response bodies also carry disproportionate privacy and performance risk.
10
+
11
+ ## Decision
12
+
13
+ Implement a Rack-protocol middleware without a runtime Rack dependency. Catch failures from the downstream application call, notify asynchronously, and re-raise the same exception. Never read `rack.input`, raw query strings, cookies, authorization headers, or response body enumeration. Use already-parsed parameter hashes only.
14
+
15
+ Define a `ContextStore` port and use a selectable thread-local adapter by default. Restore nested scopes in `ensure`. Keep breadcrumbs in a fixed circular per-execution buffer and sanitize them with the rest of the event before queueing.
16
+
17
+ ## Alternatives
18
+
19
+ Requiring Rack from the core was rejected because it would add dependency and resolution risk to plain Ruby legacy applications. Global or process-wide context was rejected because concurrent requests would leak data. Automatic body parsing and body proxies were deferred because they change I/O behavior and complicate streaming lifecycle ownership.
20
+
21
+ ## Positive consequences
22
+
23
+ Plain Ruby loading remains dependency-free, concurrent request context is isolated, middleware failure semantics are preserved, and diagnostic memory is bounded.
24
+
25
+ ## Negative consequences
26
+
27
+ Thread-local context does not propagate to application-created threads or fibers. Generic route normalization is less precise than router-specific integration. Exceptions raised only during deferred response-body enumeration are outside version 0.4 capture.
data/docs/architecture.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Architecture
2
2
 
3
- Chronos Ruby 0.3 uses hexagonal boundaries so the legacy core remains independent of frameworks and delivery infrastructure.
3
+ Chronos Ruby 0.4 uses hexagonal boundaries so the legacy core remains independent of frameworks and delivery infrastructure.
4
4
 
5
5
  ```mermaid
6
6
  flowchart TB
@@ -11,18 +11,26 @@ flowchart TB
11
11
  Application --> Delivery[Application / DeliveryPipeline]
12
12
  Delivery --> Internal[Internal / BoundedQueue, WorkerPool, and MemoryBacklog]
13
13
  Internal --> Ports
14
+ Rack[Integrations / Rack middleware] --> Facade
15
+ Facade --> Context[Ports / ContextStore]
16
+ Context --> ThreadLocal[Adapters / ThreadLocalContextStore]
14
17
  ```
15
18
 
16
19
  ## Boundaries
17
20
 
18
21
  - Domain/Core owns immutable event values and Ruby normalization.
19
22
  - Application owns use-case ordering and failure containment.
20
- - Ports define behavior expected from infrastructure.
21
- - Adapters contain Net::HTTP and TLS behavior.
23
+ - Ports define behavior expected from transport and execution-context infrastructure.
24
+ - Adapters contain Net::HTTP, TLS, and thread-local context behavior.
22
25
  - Internal contains private concurrency and diagnostic mechanisms.
26
+ - Integrations contain optional framework entry points and never enter the domain boundary.
23
27
 
24
28
  The `Chronos` module is a thin facade. Rails, Rack, ActiveSupport, Sidekiq, and job libraries must not be required by the core.
25
29
 
30
+ ## Rack capture flow
31
+
32
+ The Rack middleware establishes a context-store scope, adds a bounded request breadcrumb, and calls the downstream application. An unhandled exception is enriched with request duration and status, captured through the normal notice pipeline, then re-raised unchanged. The context-store adapter restores or clears its value in `ensure`. The middleware implements the Rack protocol without requiring the Rack library.
33
+
26
34
  ## Capture flow
27
35
 
28
36
  An exception becomes an immutable notice. `Sanitizer` removes sensitive values before `SafeSerializer` creates a bounded JSON envelope. Asynchronous capture inserts only that sanitized serialized event into the queue. A fixed worker sends it through `DeliveryPipeline`, which applies finite retry, a circuit breaker, and a fixed memory backlog. Synchronous capture bypasses the queue but uses the same privacy and resilience boundaries.
@@ -4,14 +4,14 @@ Chronos Ruby 0.x is the legacy line. Technical compatibility does not make an en
4
4
 
5
5
  | Ruby | Rails integration | Status | Evidence |
6
6
  |---|---|---|---|
7
- | 2.2.10 | None in 0.3 | Best effort | Dedicated legacy core CI job |
8
- | 2.3.8 | None in 0.3 | Best effort | Dedicated legacy core CI job |
9
- | 2.4.10 | None in 0.3 | Best effort | Dedicated legacy core CI job |
10
- | 2.5.9 | None in 0.3 | Best effort | Dedicated legacy core CI job |
11
- | 2.6.10 | None in 0.3 | Best effort | Dedicated legacy core CI job |
7
+ | 2.2.10 | Rack protocol in 0.4; no Rails | Best effort | Dedicated legacy core CI and Rack contract tests |
8
+ | 2.3.8 | Rack protocol in 0.4; no Rails | Best effort | Dedicated legacy core CI and Rack contract tests |
9
+ | 2.4.10 | Rack protocol in 0.4; no Rails | Best effort | Dedicated legacy core CI and Rack contract tests |
10
+ | 2.5.9 | Rack protocol in 0.4; no Rails | Best effort | Dedicated legacy core CI and Rack contract tests |
11
+ | 2.6.10 | Rack protocol in 0.4; no Rails | Best effort | Dedicated legacy core CI and Rack contract tests |
12
12
  | 2.7 and newer | None in 0.x | Unsupported | Belongs to transitional or modern lines |
13
13
 
14
- No Rails version is declared supported by version 0.3. Rails applications may call the plain Ruby API manually. Rails support requires a real example application, framework integration tests, and a successful dedicated matrix job.
14
+ No Rails version is declared supported by version 0.4. The middleware is tested against the Rack protocol shape without adding a Rack runtime dependency. Rails support requires a real example application, Railtie integration tests, and a successful dedicated matrix job.
15
15
 
16
16
  Status meanings:
17
17
 
@@ -39,8 +39,11 @@
39
39
  | `remote_configuration` | Optional | `true` | Accepts only the documented bounded remote policy fields |
40
40
  | `remote_config_max_bytes` | Optional | `4096` | Maximum remote policy response-header bytes |
41
41
  | `sampling_rate` | Optional | `1.0` | Local upper bound for exception sampling |
42
- | `enabled_event_types` | Optional | `["exception"]` | Local event-type allowlist; only exception exists in version 0.3 |
42
+ | `enabled_event_types` | Optional | `["exception"]` | Local event-type allowlist; only exception exists in version 0.4 |
43
43
  | `max_remote_send_interval` | Optional | `60.0` | Local upper bound for remotely requested send spacing |
44
+ | `context_store` | Optional | `:thread_local` | `:thread_local` or an object implementing `get`, `set`, `clear`, and `with_context` |
45
+ | `breadcrumb_capacity` | Optional | `20` | Positive count of newest breadcrumbs retained per execution |
46
+ | `breadcrumb_max_bytes` | Optional | `2048` | Maximum bytes per normalized breadcrumb; minimum `128` |
44
47
 
45
48
  ```ruby
46
49
  Chronos.configure do |config|
@@ -56,6 +59,8 @@ Chronos.configure do |config|
56
59
  config.max_retries = 3
57
60
  config.backlog_size = 100
58
61
  config.circuit_failure_threshold = 5
62
+ config.context_store = :thread_local
63
+ config.breadcrumb_capacity = 20
59
64
  end
60
65
  ```
61
66
 
@@ -1,6 +1,6 @@
1
1
  # Data collected
2
2
 
3
- Version 0.3 emits only manually submitted exception events. All fields pass through the privacy sanitizer and bounded safe serializer before queueing, retry storage, or delivery.
3
+ Version 0.4 emits exception events submitted manually or captured by the optional Rack middleware. All fields pass through the privacy sanitizer and bounded safe serializer before queueing, retry storage, or delivery.
4
4
 
5
5
  | Data | Default | Source |
6
6
  |---|---|---|
@@ -15,8 +15,14 @@ Version 0.3 emits only manually submitted exception events. All fields pass thro
15
15
  | Context, parameters, session, user | Optional and sanitized | Explicit capture arguments |
16
16
  | Tags and fingerprint | Optional and sanitized | Explicit capture arguments |
17
17
  | IPv4 address in supplied text | Anonymized | Explicit capture arguments |
18
+ | Rack method, normalized route, status, duration, request ID, host, query-free path, trace ID | Collected with Rack middleware | Rack environment and local clock |
19
+ | Rack user agent | Disabled by default | `HTTP_USER_AGENT` when middleware option is enabled |
20
+ | Rack controller/action | Collected when already supplied | Explicit Chronos or Action Dispatch environment values |
21
+ | Rack parameters | Collected only when already parsed | Explicit, Rack, or Action Dispatch parameter hashes |
22
+ | Response size | `Content-Length` when present | Rack response headers |
23
+ | Breadcrumbs | Explicit and bounded | Application and Chronos integration |
18
24
 
19
- The gem never collects request bodies, response bodies, cookies, HTTP authorization headers, environment variables in bulk, source code, SQL bind values, database rows, or installed gems in version 0.3.
25
+ The gem never collects request bodies, response bodies, raw query strings, cookies, HTTP authorization headers, environment variables in bulk, source code, SQL bind values, database rows, or installed gems in version 0.4.
20
26
 
21
27
  The secret `project_key` is an authentication header and is excluded from the JSON payload and logger diagnostics. The envelope field named `project_key` contains the public `project_id` required by the current v1 server contract.
22
28
 
@@ -0,0 +1,59 @@
1
+ # Rack integration, execution context, and breadcrumbs
2
+
3
+ Version 0.4 adds automatic capture through `Chronos::Integrations::Rack::Middleware`. The integration implements the Rack protocol directly and does not require Rack when `chronos-ruby` is loaded. Add the middleware after configuring Chronos:
4
+
5
+ ```ruby
6
+ use Chronos::Integrations::Rack::Middleware,
7
+ :include_user_agent => false
8
+ ```
9
+
10
+ ## Capture behavior
11
+
12
+ The middleware catches an exception raised by the downstream application, submits it asynchronously, and re-raises the same exception object. A notification or logger failure cannot replace the application failure. An `ensure` boundary clears request context in every outcome.
13
+
14
+ Captured request fields are method, normalized route, status, duration in milliseconds, request ID, optional user agent, host, query-free path, controller/action when already supplied, approximate response size from `Content-Length`, a generated or supplied trace ID, already-parsed parameters, and application-supplied user context. The final event serializer sanitizes every field before queueing.
15
+
16
+ The middleware never reads `rack.input`, copies `QUERY_STRING`, parses a body, enumerates a response body, or reads cookies and authorization headers. Parameters are collected only from already-materialized hashes at `rack.request.query_hash`, `action_dispatch.request.query_parameters`, `action_dispatch.request.path_parameters`, or the explicit `chronos.parameters` key. User context is opt-in through `chronos.user`.
17
+
18
+ Without an explicit `chronos.route` or `action_dispatch.route_uri_pattern`, numeric and UUID path segments are replaced with `:id`. This is a conservative cardinality guard, not a complete router-aware normalizer.
19
+
20
+ ## Context store
21
+
22
+ `Chronos::Ports::ContextStore` defines `get`, `set`, `clear`, and `with_context`. The legacy default, `Chronos::Adapters::ThreadLocalContextStore`, isolates each thread and restores nested scopes in `ensure`. It deliberately does not propagate state into newly created threads or fibers.
23
+
24
+ A custom strategy can be selected at configuration time if it implements the complete port:
25
+
26
+ ```ruby
27
+ Chronos.configure do |config|
28
+ # credentials omitted
29
+ config.context_store = MyContextStore.new
30
+ end
31
+ ```
32
+
33
+ Application code can add scoped values. Manual notifications issued inside the block inherit them:
34
+
35
+ ```ruby
36
+ Chronos.with_context(:user => {"id" => "customer-42"}) do
37
+ Chronos.notify(error)
38
+ end
39
+ ```
40
+
41
+ ## Breadcrumbs
42
+
43
+ `Chronos.add_breadcrumb` records an explicit marker in the current execution buffer:
44
+
45
+ ```ruby
46
+ Chronos.add_breadcrumb(
47
+ :category => "custom",
48
+ :message => "payment authorization started",
49
+ :metadata => {"provider" => "example"}
50
+ )
51
+ ```
52
+
53
+ Allowed categories are `custom`, `log`, `request`, `query`, `external_http`, `cache`, and `job`; an unknown value becomes `custom`. No log, SQL, body, cache, job, or external HTTP payload is captured automatically in 0.4. Metadata exists only when the application or a later integration explicitly supplies it.
54
+
55
+ The default circular buffer keeps the newest 20 entries. Each breadcrumb is normalized to JSON primitives with bounded depth, collections, strings, nodes, and serialized bytes. The entire exception payload still passes through the privacy sanitizer before it can enter the queue or retry backlog.
56
+
57
+ ## Concurrency and limitations
58
+
59
+ Concurrent Rack threads receive distinct user, parameter, breadcrumb, and trace values. Context does not cross threads automatically, so applications that start their own thread must establish a new scope explicitly. Version 0.4 captures exceptions raised during the initial Rack application call; deferred failures raised only while a server enumerates a streaming response body are not intercepted.
data/docs/performance.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Performance
2
2
 
3
- Performance is a functional requirement, but version 0.3 makes no unverified speed claim.
3
+ Performance is a functional requirement, but version 0.4 makes no unverified speed claim.
4
4
 
5
5
  Current controls:
6
6
 
@@ -16,8 +16,10 @@ Current controls:
16
16
  - the circuit breaker suppresses requests during sustained failure;
17
17
  - retry backlog capacity is fixed and may be disabled;
18
18
  - shutdown and flush have caller-controlled timeouts.
19
+ - request context and breadcrumbs have fixed structural and byte limits;
20
+ - Rack middleware never consumes request or response bodies.
19
21
 
20
- Run the scripts under `benchmarks/` and record Ruby version, operating system, CPU, warmup, iteration count, median, and dispersion before publishing results. `benchmarks/filtering.rb` measures privacy filtering, while `benchmarks/retry_backlog.rb` measures fixed-memory behavior with an unavailable in-memory transport. Request overhead is not measured because automatic request integration is outside version 0.3.
22
+ Run the scripts under `benchmarks/` and record Ruby version, operating system, CPU, warmup, iteration count, median, and dispersion before publishing results. `benchmarks/filtering.rb` measures privacy filtering, `benchmarks/retry_backlog.rb` measures fixed-memory outage behavior, and `benchmarks/request_overhead.rb` compares a successful direct Rack-protocol call with the middleware path.
21
23
 
22
24
  ## Version 0.2 development measurement
23
25
 
@@ -51,3 +53,13 @@ A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 with 1,000
51
53
  | Open-circuit backlog handling | 88,922 operations/second |
52
54
 
53
55
  The outage run retained exactly 100 events and rejected 9,900 additional events without growing the backlog. These are single development runs without median or dispersion and are not comparative performance claims.
56
+
57
+ ## Version 0.4 Rack middleware benchmark
58
+
59
+ Run:
60
+
61
+ ```bash
62
+ ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/request_overhead.rb
63
+ ```
64
+
65
+ A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 and 10,000 successful Rack-protocol calls. The direct application calls took 0.013272 seconds and middleware calls took 0.539758 seconds, for an estimated 52.649 microseconds of middleware work per request. The fixture uses no network, error capture, or response-body enumeration. This is a single development measurement, not a production latency claim; controlled warmup, median, and dispersion remain required before publishing a performance claim.
data/docs/privacy-lgpd.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Privacy and LGPD
2
2
 
3
- Version 0.3 sanitizes every exception event before JSON serialization, queueing, retry backlog, or transport. This reduces accidental exposure, but the host application remains responsible for lawful purpose, minimization, access control, retention, and responses to data-subject requests.
3
+ Version 0.4 sanitizes every exception event before JSON serialization, queueing, retry backlog, or transport. This reduces accidental exposure, but the host application remains responsible for lawful purpose, minimization, access control, retention, and responses to data-subject requests.
4
4
 
5
5
  ## Default policy
6
6
 
@@ -13,9 +13,15 @@ Version 0.3 sanitizes every exception event before JSON serialization, queueing,
13
13
  | Payment-card candidates that pass the Luhn check | Replaced with `[FILTERED_CARD]` |
14
14
  | IPv4 addresses | Last octet replaced with `0` |
15
15
  | Unknown Ruby objects | Represented by class name without calling application serialization |
16
- | Request/response bodies, cookies, HTTP headers, SQL binds, environment variables | Never collected automatically in version 0.3 |
16
+ | Request/response bodies, raw query strings, cookies, authorization headers, SQL binds, environment variables | Never collected automatically in version 0.4 |
17
17
 
18
- The retry backlog exists only in process memory, accepts only `SerializedEvent`, has a fixed capacity, and disappears on process exit. Version 0.3 does not persist telemetry to disk.
18
+ The retry backlog exists only in process memory, accepts only `SerializedEvent`, has a fixed capacity, and disappears on process exit. Version 0.4 does not persist telemetry to disk.
19
+
20
+ ## Rack context and breadcrumbs
21
+
22
+ The Rack middleware copies only bounded operational fields and parameter hashes that another component already parsed. It does not read `rack.input` or copy `QUERY_STRING`. User context is opt-in through `chronos.user`, and user agent collection is disabled unless the middleware option enables it.
23
+
24
+ Breadcrumbs contain category, bounded message, bounded metadata, and timestamp. No raw request, response, SQL, cache, job, log, or external HTTP payload is added automatically. Rack context and breadcrumbs pass through the same sanitizer as manual context before queueing.
19
25
 
20
26
  Blocklist matching accepts `String`, `Symbol`, and `Regexp`. String and Symbol matching is case-insensitive after punctuation normalization and also protects namespaced keys such as `user_password`.
21
27
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Configuration raises an error
4
4
 
5
- Verify `project_id`, `project_key`, and `host`. HTTPS is required while `ssl_verify` is true. Queue size, worker count, payload size, and timeouts must be positive.
5
+ Verify `project_id`, `project_key`, and `host`. HTTPS is required while `ssl_verify` is true. Queue size, worker count, payload size, breadcrumb capacity, and timeouts must be positive. `breadcrumb_max_bytes` must be at least 128.
6
6
 
7
7
  ## `Chronos.notify` returns false
8
8
 
@@ -10,7 +10,15 @@ The agent may be unconfigured, disabled, ignored in the current environment, una
10
10
 
11
11
  ## `Chronos.notify_sync` returns false
12
12
 
13
- Check DNS, TLS certificates, credentials, HTTP status, proxy configuration, and timeout values. Version 0.3 retries only network errors, HTTP `408`, `429`, and `5xx` responses. Inspect `agent.diagnostics` when constructing an agent directly to see retry state, backlog usage, and the circuit state.
13
+ Check DNS, TLS certificates, credentials, HTTP status, proxy configuration, and timeout values. The resilience layer retries only network errors, HTTP `408`, `429`, and `5xx` responses. Inspect `agent.diagnostics` when constructing an agent directly to see retry state, backlog usage, and the circuit state.
14
+
15
+ ## Rack exception is not captured
16
+
17
+ Confirm that `Chronos.configure` runs before the middleware handles requests and that the middleware wraps the application component that raises. Version 0.4 captures exceptions raised by the initial downstream Rack call; an exception raised later while a server enumerates a streaming response body is outside this release. The original exception is always re-raised, so the server log should still show it.
18
+
19
+ ## Context appears missing
20
+
21
+ The legacy context store is thread-local. A new application-created thread does not inherit context. Establish a new `Chronos.with_context` scope inside that thread, and issue manual notification before the scope exits. For Rack capture, supply user and explicit parameters through the documented environment keys.
14
22
 
15
23
  ## Events disappear during shutdown
16
24
 
@@ -0,0 +1,52 @@
1
+ module Chronos
2
+ module Adapters
3
+ # Stores execution context in the current Ruby thread.
4
+ #
5
+ # @responsibility Isolate request context and restore nested scopes reliably.
6
+ # @motivation Legacy Ruby does not provide a portable fiber-local storage API.
7
+ # @limits Context does not propagate to new threads or across processes.
8
+ # @collaborators ContextStore port and Rack middleware.
9
+ # @thread_safety Each thread owns an independent value; one instance may be shared.
10
+ # @compatibility Ruby 2.2.10 through Ruby 2.6.
11
+ # @example
12
+ # store.with_context(:request_id => "r1") { store.get }
13
+ # @errors The previous value is restored in ensure even when the block raises.
14
+ # @performance Reads and writes are constant-time thread-local operations.
15
+ class ThreadLocalContextStore
16
+ def initialize
17
+ @key = "chronos_context_#{object_id}".freeze
18
+ end
19
+
20
+ def get
21
+ Thread.current[@key] || {}
22
+ end
23
+
24
+ def set(context)
25
+ raise ArgumentError, "context must be a Hash" unless context.is_a?(Hash)
26
+
27
+ Thread.current[@key] = context
28
+ end
29
+
30
+ def clear
31
+ Thread.current[@key] = nil
32
+ nil
33
+ end
34
+
35
+ def with_context(context)
36
+ previous = Thread.current[@key]
37
+ set(merge_context(previous || {}, context))
38
+ yield
39
+ ensure
40
+ previous ? Thread.current[@key] = previous : clear
41
+ end
42
+
43
+ private
44
+
45
+ def merge_context(current, additional)
46
+ raise ArgumentError, "context must be a Hash" unless additional.is_a?(Hash)
47
+
48
+ current.merge(additional)
49
+ end
50
+ end
51
+ end
52
+ end
data/lib/chronos/agent.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  module Chronos
2
2
  # Runtime composition root for the framework-independent Chronos agent.
3
3
  #
4
- # @responsibility Own transport, resilient delivery pipeline, and the capture use case.
4
+ # @responsibility Own delivery, capture, execution context, and breadcrumb collaborators.
5
5
  # @motivation Keep construction details outside the public module facade.
6
- # @limits It does not integrate with Rails, Rack, or job systems.
7
- # @collaborators Configuration snapshot, CaptureException, DeliveryPipeline, and NetHttpTransport.
6
+ # @limits It does not install Rack, Rails, or job integrations automatically.
7
+ # @collaborators CaptureException, DeliveryPipeline, ContextStore, and BreadcrumbBuffer.
8
8
  # @thread_safety Runtime collaborators synchronize mutable state.
9
9
  # @compatibility Ruby 2.2.10 through Ruby 2.6.
10
10
  # @example
@@ -19,6 +19,10 @@ module Chronos
19
19
  def initialize(config, options = {})
20
20
  @config = config
21
21
  @logger = options[:logger] || Internal::SafeLogger.new(config.logger)
22
+ @context_store = options[:context_store] || build_context_store(config.context_store)
23
+ unless Ports::ContextStore.compatible?(@context_store)
24
+ raise ArgumentError, "context store does not implement the Chronos context-store port"
25
+ end
22
26
  @transport = options[:transport] || Adapters::NetHttpTransport.new(config, @logger)
23
27
  unless Ports::Transport.compatible?(@transport)
24
28
  raise ArgumentError, "transport does not implement the Chronos transport port"
@@ -36,11 +40,27 @@ module Chronos
36
40
  end
37
41
 
38
42
  def notify(exception, context = {})
39
- @capture.call(exception, context)
43
+ @capture.call(exception, context_for_capture(context))
40
44
  end
41
45
 
42
46
  def notify_sync(exception, context = {})
43
- @capture.call_sync(exception, context)
47
+ @capture.call_sync(exception, context_for_capture(context))
48
+ end
49
+
50
+ def with_context(context = {}, &block)
51
+ @context_store.with_context(context, &block)
52
+ end
53
+
54
+ def add_breadcrumb(attributes = {})
55
+ context = @context_store.get
56
+ buffer = context[:__chronos_breadcrumbs]
57
+ buffer ||= Core::BreadcrumbBuffer.new(@config.breadcrumb_capacity, @config.breadcrumb_max_bytes)
58
+ buffer.add(attributes)
59
+ @context_store.set(context.merge(:__chronos_breadcrumbs => buffer))
60
+ true
61
+ rescue StandardError => error
62
+ @logger.warn("Chronos breadcrumb failed: #{error.class}")
63
+ false
44
64
  end
45
65
 
46
66
  def flush(timeout = DEFAULT_FLUSH_TIMEOUT)
@@ -61,5 +81,34 @@ module Chronos
61
81
  details = @delivery_pipeline.diagnostics
62
82
  details[:queue].merge(details)
63
83
  end
84
+
85
+ private
86
+
87
+ def build_context_store(strategy)
88
+ return Adapters::ThreadLocalContextStore.new if strategy == :thread_local
89
+
90
+ strategy
91
+ end
92
+
93
+ def context_for_capture(additional)
94
+ merged = deep_merge(context_hash(@context_store.get), context_hash(additional))
95
+ buffer = merged.delete(:__chronos_breadcrumbs) || merged.delete("__chronos_breadcrumbs")
96
+ if buffer.respond_to?(:to_a)
97
+ merged[:context] = context_hash(merged[:context]).merge("breadcrumbs" => buffer.to_a)
98
+ end
99
+ merged
100
+ rescue StandardError
101
+ context_hash(additional)
102
+ end
103
+
104
+ def deep_merge(left, right)
105
+ left.merge(right) do |_key, old_value, new_value|
106
+ old_value.is_a?(Hash) && new_value.is_a?(Hash) ? deep_merge(old_value, new_value) : new_value
107
+ end
108
+ end
109
+
110
+ def context_hash(value)
111
+ value.is_a?(Hash) ? value : {}
112
+ end
64
113
  end
65
114
  end
@@ -2,9 +2,9 @@ module Chronos
2
2
  module Application
3
3
  # Orchestrates exception normalization, serialization, queueing, and delivery.
4
4
  #
5
- # @responsibility Execute the version 0.3 exception capture pipeline.
5
+ # @responsibility Execute the exception capture pipeline for manual and integration callers.
6
6
  # @motivation Keep the public facade and transport adapters free of use-case logic.
7
- # @limits It does not implement framework hooks or automatic capture.
7
+ # @limits Framework hooks remain separate integration adapters.
8
8
  # @collaborators NoticeBuilder, PayloadSerializer, and DeliveryPipeline.
9
9
  # @thread_safety Collaborators are immutable or synchronized; calls may run concurrently.
10
10
  # @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
@@ -24,6 +24,7 @@ module Chronos
24
24
  token access_token refresh_token private_key client_secret cookie set-cookie
25
25
  session credit_card card_number cvv cpf cnpj
26
26
  ).freeze
27
+ CONTEXT_STORE_METHODS = [:get, :set, :clear, :with_context].freeze
27
28
 
28
29
  ATTRIBUTES = [
29
30
  :project_id, :project_key, :host, :environment, :app_version,
@@ -35,7 +36,8 @@ module Chronos
35
36
  :retry_base_interval, :retry_max_interval, :retry_jitter,
36
37
  :backlog_size, :circuit_failure_threshold, :circuit_reset_timeout,
37
38
  :remote_configuration, :remote_config_max_bytes, :sampling_rate,
38
- :enabled_event_types, :max_remote_send_interval
39
+ :enabled_event_types, :max_remote_send_interval, :context_store,
40
+ :breadcrumb_capacity, :breadcrumb_max_bytes
39
41
  ].freeze
40
42
 
41
43
  attr_accessor(*ATTRIBUTES)
@@ -57,7 +59,7 @@ module Chronos
57
59
  validation_errors.empty?
58
60
  end
59
61
 
60
- def validation_errors
62
+ def validation_errors # rubocop:disable Metrics/AbcSize
61
63
  errors = []
62
64
  if enabled
63
65
  errors << "project_id is required" if blank?(project_id)
@@ -71,6 +73,7 @@ module Chronos
71
73
  errors << "max_payload_size must be a positive integer" unless positive_integer?(max_payload_size)
72
74
  errors.concat(resilience_errors)
73
75
  errors.concat(privacy_errors)
76
+ errors.concat(context_errors)
74
77
  errors
75
78
  end
76
79
 
@@ -97,6 +100,9 @@ module Chronos
97
100
  @user_agent = "chronos-ruby/#{Chronos::VERSION}"
98
101
  @max_payload_size = 1_048_576
99
102
  @gzip = false
103
+ @context_store = :thread_local
104
+ @breadcrumb_capacity = 20
105
+ @breadcrumb_max_bytes = 2048
100
106
  end
101
107
 
102
108
  def initialize_privacy_defaults
@@ -187,6 +193,20 @@ module Chronos
187
193
  errors
188
194
  end
189
195
 
196
+ def context_errors
197
+ errors = []
198
+ unless context_store == :thread_local || CONTEXT_STORE_METHODS.all? do |method_name|
199
+ context_store.respond_to?(method_name)
200
+ end
201
+ errors << "context_store must be :thread_local or implement get, set, clear, and with_context"
202
+ end
203
+ errors << "breadcrumb_capacity must be a positive integer" unless positive_integer?(breadcrumb_capacity)
204
+ unless breadcrumb_max_bytes.is_a?(Integer) && breadcrumb_max_bytes >= 128
205
+ errors << "breadcrumb_max_bytes must be an integer greater than or equal to 128"
206
+ end
207
+ errors
208
+ end
209
+
190
210
  def filter_errors
191
211
  return ["filters must be an array"] unless filters.is_a?(Array)
192
212
  return [] if filters.all? { |filter| filter.respond_to?(:call) }
@@ -278,7 +298,7 @@ module Chronos
278
298
  private
279
299
 
280
300
  def deep_freeze(value)
281
- return value if value.respond_to?(:call)
301
+ return value if value.respond_to?(:call) || context_store?(value)
282
302
 
283
303
  case value
284
304
  when Hash
@@ -291,6 +311,12 @@ module Chronos
291
311
  end
292
312
  value.freeze
293
313
  end
314
+
315
+ def context_store?(value)
316
+ CONTEXT_STORE_METHODS.all? { |method_name| value.respond_to?(method_name) }
317
+ rescue StandardError
318
+ false
319
+ end
294
320
  end
295
321
  end
296
322
  end
@@ -0,0 +1,126 @@
1
+ require "json"
2
+ require "time"
3
+
4
+ module Chronos
5
+ module Core
6
+ # Immutable, bounded diagnostic marker attached to an execution.
7
+ #
8
+ # @responsibility Normalize one breadcrumb without retaining raw application objects.
9
+ # @motivation Preserve useful events leading to an exception with predictable memory use.
10
+ # @limits It does not capture logs, SQL, bodies, or HTTP calls automatically.
11
+ # @collaborators SafeSerializer and BreadcrumbBuffer.
12
+ # @thread_safety Immutable after construction.
13
+ # @compatibility Ruby 2.2.10 through Ruby 2.6.
14
+ # @example
15
+ # breadcrumb.to_h #=> {"category"=>"custom", ...}
16
+ # @errors Unserializable metadata is replaced by SafeSerializer placeholders.
17
+ # @performance Metadata traversal has strict depth, node, item, and byte limits.
18
+ class Breadcrumb
19
+ CATEGORIES = %w(custom log request query external_http cache job).freeze
20
+
21
+ def initialize(attributes, clock = nil, max_bytes = 2048)
22
+ attributes = {} unless attributes.is_a?(Hash)
23
+ clock ||= proc { Time.now }
24
+ serializer = SafeSerializer.new(
25
+ :max_depth => 5, :max_keys => 20, :max_items => 20,
26
+ :max_string_bytes => 512, :max_nodes => 100
27
+ )
28
+ @data = serializer.call(build_data(attributes, clock))
29
+ @data = compact_data(@data, max_bytes) if JSON.generate(@data).bytesize > max_bytes
30
+ deep_freeze(@data)
31
+ freeze
32
+ end
33
+
34
+ def to_h
35
+ @data
36
+ end
37
+
38
+ private
39
+
40
+ def build_data(attributes, clock)
41
+ category = value(attributes, :category).to_s
42
+ category = "custom" unless CATEGORIES.include?(category)
43
+ {
44
+ "category" => category,
45
+ "message" => value(attributes, :message).to_s,
46
+ "metadata" => value(attributes, :metadata) || {},
47
+ "timestamp" => clock.call.utc.iso8601(6)
48
+ }
49
+ end
50
+
51
+ def compact_data(data, max_bytes)
52
+ message_limit = [max_bytes / 4, 32].max
53
+ compacted = {
54
+ "category" => data["category"],
55
+ "message" => SafeSerializer.new.call(data["message"], :max_string_bytes => message_limit),
56
+ "metadata" => {"_truncated" => true},
57
+ "timestamp" => data["timestamp"]
58
+ }
59
+ trim_compacted_message(compacted, max_bytes)
60
+ end
61
+
62
+ def trim_compacted_message(compacted, max_bytes)
63
+ while JSON.generate(compacted).bytesize > max_bytes && !compacted["message"].empty?
64
+ length = [compacted["message"].bytesize - 16, 0].max
65
+ compacted["message"] = SafeSerializer.new.call(
66
+ compacted["message"].byteslice(0, length).to_s,
67
+ :max_string_bytes => length
68
+ )
69
+ end
70
+ compacted
71
+ end
72
+
73
+ def value(attributes, key)
74
+ attributes.key?(key) ? attributes[key] : attributes[key.to_s]
75
+ end
76
+
77
+ def deep_freeze(value)
78
+ if value.is_a?(Hash)
79
+ value.each do |key, child|
80
+ deep_freeze(key)
81
+ deep_freeze(child)
82
+ end
83
+ elsif value.is_a?(Array)
84
+ value.each { |child| deep_freeze(child) }
85
+ end
86
+ value.freeze
87
+ end
88
+ end
89
+
90
+ # Fixed-size circular collection of execution breadcrumbs.
91
+ #
92
+ # @responsibility Retain only the newest bounded breadcrumbs for one execution.
93
+ # @motivation Prevent long requests or noisy instrumentation from growing memory indefinitely.
94
+ # @limits It is process memory only and does not collect events by itself.
95
+ # @collaborators Breadcrumb and Agent.
96
+ # @thread_safety Intended for one execution thread; snapshots are immutable values.
97
+ # @compatibility Ruby 2.2.10 through Ruby 2.6.
98
+ # @example
99
+ # buffer.add(:category => "custom", :message => "started")
100
+ # @errors Invalid attributes are normalized into a safe breadcrumb.
101
+ # @performance Add is constant time and storage never exceeds capacity.
102
+ class BreadcrumbBuffer
103
+ def initialize(capacity, max_bytes = 2048)
104
+ raise ArgumentError, "capacity must be a positive integer" unless capacity.is_a?(Integer) && capacity > 0
105
+
106
+ @capacity = capacity
107
+ @max_bytes = max_bytes
108
+ @items = []
109
+ end
110
+
111
+ def add(attributes)
112
+ @items.shift if @items.length >= @capacity
113
+ @items << Breadcrumb.new(attributes, nil, @max_bytes)
114
+ true
115
+ end
116
+
117
+ def to_a
118
+ @items.map(&:to_h)
119
+ end
120
+
121
+ def size
122
+ @items.size
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,152 @@
1
+ require "securerandom"
2
+
3
+ module Chronos
4
+ module Integrations
5
+ module Rack
6
+ # Captures unhandled Rack exceptions while preserving Rack semantics.
7
+ #
8
+ # @responsibility Build bounded request context, notify Chronos, and re-raise the original exception.
9
+ # @motivation Provide automatic error capture without coupling the core to Rack.
10
+ # @limits It never reads rack.input, enumerates response bodies, or infers Rails routes.
11
+ # @collaborators Rack application and the Chronos facade or Agent.
12
+ # @thread_safety Shared instances keep no per-request mutable state.
13
+ # @compatibility Rack 1.x/2.x protocol; Ruby 2.2.10 through Ruby 2.6.
14
+ # @example
15
+ # use Chronos::Integrations::Rack::Middleware
16
+ # @errors Notification failures are contained; the application exception is always re-raised.
17
+ # @performance Adds bounded hash construction and monotonic-clock reads per request.
18
+ class Middleware
19
+ def initialize(app, options = {})
20
+ @app = app
21
+ @notifier = options[:notifier] || Chronos
22
+ @include_user_agent = options[:include_user_agent] || false
23
+ @clock = options[:clock] || proc { monotonic_time }
24
+ end
25
+
26
+ def call(env)
27
+ started_at = @clock.call
28
+ base = request_capture_context(env)
29
+ @notifier.with_context(base) do
30
+ add_request_breadcrumb("request started", base)
31
+ call_application(env, base, started_at)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def call_application(env, base, started_at)
38
+ response = @app.call(env)
39
+ status = response[0]
40
+ headers = response[1]
41
+ add_request_breadcrumb("request completed", dynamic_request_context(base, status, headers, started_at))
42
+ response
43
+ rescue Exception => error # rubocop:disable Lint/RescueException
44
+ context = dynamic_request_context(base, 500, nil, started_at)
45
+ notify_safely(error, context)
46
+ raise
47
+ end
48
+
49
+ def notify_safely(error, context)
50
+ @notifier.notify(error, context)
51
+ rescue StandardError
52
+ false
53
+ end
54
+
55
+ def request_capture_context(env)
56
+ request = request_values(env)
57
+ {
58
+ :context => {"request" => request, "trace_id" => trace_id(env)},
59
+ :parameters => parameters(env),
60
+ :user => hash_value(env["chronos.user"])
61
+ }
62
+ end
63
+
64
+ def request_values(env)
65
+ values = {
66
+ "method" => env["REQUEST_METHOD"].to_s,
67
+ "route" => normalized_route(env),
68
+ "request_id" => request_id(env),
69
+ "host" => (env["HTTP_HOST"] || env["SERVER_NAME"]).to_s,
70
+ "path" => env["PATH_INFO"].to_s,
71
+ "controller" => controller_action(env, "controller"),
72
+ "action" => controller_action(env, "action")
73
+ }
74
+ values["user_agent"] = env["HTTP_USER_AGENT"].to_s if @include_user_agent
75
+ values
76
+ end
77
+
78
+ def dynamic_request_context(base, status, headers, started_at)
79
+ request = base[:context]["request"].merge(
80
+ "status" => status.to_i,
81
+ "duration_ms" => ((@clock.call - started_at) * 1000.0).round(3),
82
+ "response_size" => response_size(headers)
83
+ )
84
+ {:context => base[:context].merge("request" => request)}
85
+ end
86
+
87
+ def parameters(env)
88
+ result = {}
89
+ [env["rack.request.query_hash"], env["action_dispatch.request.query_parameters"],
90
+ env["action_dispatch.request.path_parameters"], env["chronos.parameters"]].each do |candidate|
91
+ result.merge!(candidate) if candidate.is_a?(Hash)
92
+ end
93
+ result
94
+ rescue StandardError
95
+ {}
96
+ end
97
+
98
+ def normalized_route(env)
99
+ explicit = env["chronos.route"] || env["action_dispatch.route_uri_pattern"]
100
+ return explicit.to_s unless explicit.to_s.empty?
101
+
102
+ env["PATH_INFO"].to_s.split("/").map { |part| dynamic_segment?(part) ? ":id" : part }.join("/")
103
+ end
104
+
105
+ def dynamic_segment?(segment)
106
+ segment =~ /\A\d+\z/ || segment =~ /\A[0-9a-f]{8}-[0-9a-f-]{27,}\z/i
107
+ end
108
+
109
+ def controller_action(env, key)
110
+ explicit = env["chronos.#{key}"]
111
+ paths = env["action_dispatch.request.path_parameters"]
112
+ explicit || (paths[key] if paths.is_a?(Hash)) || (paths[key.to_sym] if paths.is_a?(Hash))
113
+ end
114
+
115
+ def request_id(env)
116
+ env["chronos.request_id"] || env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
117
+ end
118
+
119
+ def trace_id(env)
120
+ env["chronos.trace_id"] || SecureRandom.uuid
121
+ end
122
+
123
+ def response_size(headers)
124
+ return nil unless headers.respond_to?(:each)
125
+
126
+ pair = headers.find { |key, _value| key.to_s.casecmp("content-length").zero? }
127
+ pair ? pair[1].to_i : nil
128
+ rescue StandardError
129
+ nil
130
+ end
131
+
132
+ def add_request_breadcrumb(message, context)
133
+ request = context[:context]["request"]
134
+ @notifier.add_breadcrumb(
135
+ :category => "request", :message => message,
136
+ :metadata => {"method" => request["method"], "route" => request["route"], "status" => request["status"]}
137
+ )
138
+ end
139
+
140
+ def hash_value(value)
141
+ value.is_a?(Hash) ? value : {}
142
+ end
143
+
144
+ def monotonic_time
145
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
146
+ rescue StandardError
147
+ Time.now.to_f
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,12 @@
1
+ module Chronos
2
+ module Integrations
3
+ # Rack protocol integrations that do not require Rack at gem load time.
4
+ #
5
+ # @responsibility Namespace Rack-compatible middleware.
6
+ # @motivation Permit optional use without adding a runtime Rack dependency.
7
+ # @limits It does not provide Rails-specific route discovery.
8
+ # @thread_safety Middleware instances may be shared by concurrent Rack threads.
9
+ # @compatibility Rack 1.x and 2.x protocol shapes on Ruby 2.2.10 through Ruby 2.6.
10
+ module Rack; end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ module Chronos
2
+ # Optional framework entry points kept outside the framework-independent core.
3
+ #
4
+ # @responsibility Namespace integration adapters such as Rack middleware.
5
+ # @motivation Keep framework loading optional for plain Ruby applications.
6
+ # @limits Integrations may depend only on documented public agent behavior.
7
+ # @thread_safety Each integration documents its own guarantees.
8
+ # @compatibility Version 0.4 supports Rack protocol behavior on Ruby 2.2.10 through 2.6.
9
+ module Integrations; end
10
+ end
@@ -0,0 +1,22 @@
1
+ module Chronos
2
+ module Ports
3
+ # Conceptual storage port for execution-scoped context.
4
+ #
5
+ # @responsibility Define get, set, clear, and scoped context behavior.
6
+ # @motivation Let integrations isolate request state without depending on a thread implementation.
7
+ # @limits The port does not prescribe thread, fiber, or distributed propagation semantics.
8
+ # @collaborators Agent and context-store adapters.
9
+ # @thread_safety Implementations must isolate concurrent executions.
10
+ # @compatibility Ruby 2.2.10 through Ruby 2.6.
11
+ # @example
12
+ # Chronos::Ports::ContextStore.compatible?(store) #=> true
13
+ # @errors Compatibility checks never invoke application methods.
14
+ module ContextStore
15
+ REQUIRED_METHODS = [:get, :set, :clear, :with_context].freeze
16
+
17
+ def self.compatible?(object)
18
+ REQUIRED_METHODS.all? { |method_name| object.respond_to?(method_name) }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,4 +1,4 @@
1
1
  module Chronos
2
2
  # Current version of the legacy Chronos Ruby agent.
3
- VERSION = "0.3.0.pre.1".freeze
3
+ VERSION = "0.4.0.pre.1".freeze
4
4
  end
data/lib/chronos.rb CHANGED
@@ -16,23 +16,29 @@ require "chronos/core/sanitizer"
16
16
  require "chronos/core/safe_serializer"
17
17
  require "chronos/core/payload_serializer"
18
18
  require "chronos/ports/transport"
19
+ require "chronos/ports/context_store"
19
20
  require "chronos/internal/safe_logger"
20
21
  require "chronos/internal/bounded_queue"
21
22
  require "chronos/internal/memory_backlog"
22
23
  require "chronos/internal/worker_pool"
23
24
  require "chronos/adapters/net_http_transport"
25
+ require "chronos/adapters/thread_local_context_store"
26
+ require "chronos/core/breadcrumb"
24
27
  require "chronos/application/retry_policy"
25
28
  require "chronos/application/circuit_breaker"
26
29
  require "chronos/application/remote_configuration"
27
30
  require "chronos/application/delivery_pipeline"
28
31
  require "chronos/application/capture_exception"
29
32
  require "chronos/agent"
33
+ require "chronos/integrations"
34
+ require "chronos/integrations/rack"
35
+ require "chronos/integrations/rack/middleware"
30
36
 
31
37
  # Framework-independent public facade for the Chronos Ruby agent.
32
38
  #
33
39
  # @responsibility Configure the agent and expose its small lifecycle API.
34
40
  # @motivation Give applications a stable entry point while internals evolve.
35
- # @limits Version 0.3 captures Ruby exceptions manually; integrations arrive later.
41
+ # @limits Version 0.4 captures Rack failures but does not install middleware automatically.
36
42
  # @collaborators Configuration and Agent.
37
43
  # @thread_safety Agent replacement and lookup are protected by a mutex.
38
44
  # @compatibility Ruby 2.2.10 through Ruby 2.6.
@@ -75,6 +81,20 @@ module Chronos
75
81
  false
76
82
  end
77
83
 
84
+ def with_context(context = {})
85
+ agent = current_agent
86
+ return yield unless agent
87
+
88
+ agent.with_context(context) { yield }
89
+ end
90
+
91
+ def add_breadcrumb(attributes = {})
92
+ agent = current_agent
93
+ agent ? agent.add_breadcrumb(attributes) : false
94
+ rescue StandardError
95
+ false
96
+ end
97
+
78
98
  def configured?
79
99
  !current_agent.nil?
80
100
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chronos-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.pre.1
4
+ version: 0.4.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antonio Jefferson
@@ -87,6 +87,7 @@ files:
87
87
  - README.md
88
88
  - SECURITY.md
89
89
  - contracts/event-v1.schema.json
90
+ - contracts/rack-context-v1.schema.json
90
91
  - docs/adr/ADR-001-core-and-integrations.md
91
92
  - docs/adr/ADR-002-version-lines.md
92
93
  - docs/adr/ADR-003-net-http-legacy-transport.md
@@ -94,6 +95,7 @@ files:
94
95
  - docs/adr/ADR-005-sanitize-before-backlog.md
95
96
  - docs/adr/ADR-006-versioned-event-contract.md
96
97
  - docs/adr/ADR-011-bounded-resilience-and-remote-control.md
98
+ - docs/adr/ADR-012-rack-context-isolation.md
97
99
  - docs/architecture.md
98
100
  - docs/compatibility.md
99
101
  - docs/configuration.md
@@ -102,6 +104,7 @@ files:
102
104
  - docs/modules/async-queue.md
103
105
  - docs/modules/configuration.md
104
106
  - docs/modules/notice-pipeline.md
107
+ - docs/modules/rack-context.md
105
108
  - docs/modules/remote-configuration.md
106
109
  - docs/modules/retry-backlog.md
107
110
  - docs/modules/sanitization.md
@@ -113,6 +116,7 @@ files:
113
116
  - lib/chronos.rb
114
117
  - lib/chronos/adapters.rb
115
118
  - lib/chronos/adapters/net_http_transport.rb
119
+ - lib/chronos/adapters/thread_local_context_store.rb
116
120
  - lib/chronos/agent.rb
117
121
  - lib/chronos/application.rb
118
122
  - lib/chronos/application/capture_exception.rb
@@ -123,6 +127,7 @@ files:
123
127
  - lib/chronos/configuration.rb
124
128
  - lib/chronos/core.rb
125
129
  - lib/chronos/core/backtrace_parser.rb
130
+ - lib/chronos/core/breadcrumb.rb
126
131
  - lib/chronos/core/exception_cause_collector.rb
127
132
  - lib/chronos/core/notice.rb
128
133
  - lib/chronos/core/notice_builder.rb
@@ -132,12 +137,16 @@ files:
132
137
  - lib/chronos/core/sanitizer.rb
133
138
  - lib/chronos/core/sensitive_value_filter.rb
134
139
  - lib/chronos/errors.rb
140
+ - lib/chronos/integrations.rb
141
+ - lib/chronos/integrations/rack.rb
142
+ - lib/chronos/integrations/rack/middleware.rb
135
143
  - lib/chronos/internal.rb
136
144
  - lib/chronos/internal/bounded_queue.rb
137
145
  - lib/chronos/internal/memory_backlog.rb
138
146
  - lib/chronos/internal/safe_logger.rb
139
147
  - lib/chronos/internal/worker_pool.rb
140
148
  - lib/chronos/ports.rb
149
+ - lib/chronos/ports/context_store.rb
141
150
  - lib/chronos/ports/transport.rb
142
151
  - lib/chronos/ruby.rb
143
152
  - lib/chronos/ruby/version.rb