chronos-ruby 0.1.0.pre.2 → 0.3.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 +4 -4
- data/CHANGELOG.md +56 -0
- data/README.md +51 -21
- data/docs/adr/ADR-005-sanitize-before-backlog.md +2 -2
- data/docs/adr/ADR-011-bounded-resilience-and-remote-control.md +27 -0
- data/docs/architecture.md +5 -4
- data/docs/compatibility.md +6 -6
- data/docs/configuration.md +27 -1
- data/docs/data-collected.md +10 -7
- data/docs/examples/plain-ruby.md +14 -0
- data/docs/modules/async-queue.md +6 -2
- data/docs/modules/notice-pipeline.md +6 -3
- data/docs/modules/remote-configuration.md +54 -0
- data/docs/modules/retry-backlog.md +60 -0
- data/docs/modules/sanitization.md +19 -0
- data/docs/modules/serialization.md +9 -0
- data/docs/modules/transport.md +5 -3
- data/docs/performance.md +39 -2
- data/docs/privacy-lgpd.md +80 -10
- data/docs/troubleshooting.md +2 -2
- data/lib/chronos/adapters/net_http_transport.rb +21 -3
- data/lib/chronos/agent.rb +16 -8
- data/lib/chronos/application/capture_exception.rb +20 -14
- data/lib/chronos/application/circuit_breaker.rb +70 -0
- data/lib/chronos/application/delivery_pipeline.rb +248 -0
- data/lib/chronos/application/remote_configuration.rb +173 -0
- data/lib/chronos/application/retry_policy.rb +57 -0
- data/lib/chronos/configuration.rb +178 -21
- data/lib/chronos/core/notice.rb +1 -1
- data/lib/chronos/core/payload_serializer.rb +39 -89
- data/lib/chronos/core/runtime_info.rb +1 -1
- data/lib/chronos/core/safe_serializer.rb +119 -0
- data/lib/chronos/core/sanitizer.rb +161 -0
- data/lib/chronos/core/sensitive_value_filter.rb +118 -0
- data/lib/chronos/internal/bounded_queue.rb +1 -1
- data/lib/chronos/internal/memory_backlog.rb +65 -0
- data/lib/chronos/internal/worker_pool.rb +5 -6
- data/lib/chronos/ports/transport.rb +4 -3
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +9 -1
- metadata +30 -11
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Safe serialization module
|
|
2
|
+
|
|
3
|
+
`Chronos::Core::SafeSerializer` converts sanitized values into JSON primitives without invoking `to_json` or `to_s` on arbitrary application objects. Unknown objects become a class-name placeholder.
|
|
4
|
+
|
|
5
|
+
The serializer limits depth, visited nodes, hash keys, array items, key bytes, and string bytes. It detects circular hashes and arrays, repairs invalid UTF-8, and contains unreadable values. These structural budgets bound processing time and memory without using asynchronous interruption such as `Timeout` inside application code.
|
|
6
|
+
|
|
7
|
+
`PayloadSerializer` owns the versioned event envelope and composes `Sanitizer` before `SafeSerializer`. If the resulting JSON exceeds `max_payload_size`, caller-controlled fields are compacted. An event that still cannot fit is rejected before queueing.
|
|
8
|
+
|
|
9
|
+
The serializer is stateless across calls and safe for concurrent capture. Tests in `spec/unit/core/safe_serializer_spec.rb` and `spec/unit/core/payload_serializer_spec.rb` cover unsafe objects, cycles, invalid encoding, structural limits, and total payload limits.
|
data/docs/modules/transport.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
`Chronos::Ports::Transport` defines delivery behavior. `Chronos::Adapters::NetHttpTransport` implements it with the Ruby standard library.
|
|
4
4
|
|
|
5
|
-
Each event uses bounded open and read timeouts. TLS verification is enabled by default. Authentication and idempotency values are headers. Redirects are not followed. Statuses are classified as success, client error, rate limited, server error, network error, or closed.
|
|
5
|
+
Each event uses bounded open and read timeouts. TLS verification is enabled by default. Authentication and idempotency values are headers. Redirects are not followed. Statuses are classified as success, request timeout, client error, rate limited, server error, network error, or closed. HTTP `408`, `429`, `5xx`, and network failures are retryable; other `4xx` responses are permanent.
|
|
6
6
|
|
|
7
|
-
The adapter creates a connection per event in version 0.
|
|
7
|
+
The adapter creates a connection per event in version 0.3. This is conservative for legacy applications and avoids shared socket lifecycle state. Retry remains in `DeliveryPipeline`, behind the transport port.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
A successful response may contain `X-Chronos-Remote-Configuration`. The adapter rejects the header when it exceeds `remote_config_max_bytes` or is not a JSON object. It never interprets policy or accepts remote endpoint and credential changes; that allowlist belongs to `RemoteConfiguration`.
|
|
10
|
+
|
|
11
|
+
Risks include proxy credential exposure, TLS incompatibility on old operating systems, endpoint latency, and stale remote policy until another response arrives. Tests use a local fake HTTP server to verify headers, `2xx`, `408`, `429`, `500`, timeout, invalid TLS, and bounded remote policy parsing.
|
data/docs/performance.md
CHANGED
|
@@ -1,16 +1,53 @@
|
|
|
1
1
|
# Performance
|
|
2
2
|
|
|
3
|
-
Performance is a functional requirement, but version 0.
|
|
3
|
+
Performance is a functional requirement, but version 0.3 makes no unverified speed claim.
|
|
4
4
|
|
|
5
5
|
Current controls:
|
|
6
6
|
|
|
7
7
|
- backtraces are limited to 200 frames;
|
|
8
8
|
- hashes and arrays are bounded during serialization;
|
|
9
9
|
- strings and total payload size are bounded;
|
|
10
|
+
- sanitizer traversal and serializer node count are bounded by the event structure;
|
|
10
11
|
- the asynchronous queue has fixed capacity;
|
|
11
12
|
- worker count is fixed and threads start lazily;
|
|
12
13
|
- producers never wait for queue capacity;
|
|
13
14
|
- HTTP open and read timeouts are explicit;
|
|
15
|
+
- retry count, delay, jitter, and `Retry-After` are capped;
|
|
16
|
+
- the circuit breaker suppresses requests during sustained failure;
|
|
17
|
+
- retry backlog capacity is fixed and may be disabled;
|
|
14
18
|
- shutdown and flush have caller-controlled timeouts.
|
|
15
19
|
|
|
16
|
-
Run the scripts under `benchmarks/` and record Ruby version, operating system, CPU, warmup, iteration count, median, and dispersion before publishing results. Request overhead is not measured because automatic request integration is outside version 0.
|
|
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.
|
|
21
|
+
|
|
22
|
+
## Version 0.2 development measurement
|
|
23
|
+
|
|
24
|
+
A local diagnostic run on 2026-07-19 used macOS arm64 (`T8103`), legacy x86_64 Ruby builds, 100 warmup iterations, and 500 measured iterations per script:
|
|
25
|
+
|
|
26
|
+
| Runtime | Asynchronous local capture | Full serialization | Privacy filtering fixture |
|
|
27
|
+
|---|---:|---:|---:|
|
|
28
|
+
| Ruby 2.2.10 | 1,857 µs/event | 4,047 µs/event | 798 µs/pass |
|
|
29
|
+
| Ruby 2.6.3 | 1,904 µs/event | 4,705 µs/event | 1,037 µs/pass |
|
|
30
|
+
|
|
31
|
+
The capture benchmark uses an in-memory transport and includes queue draining. The serialization fixture contains 20 backtrace frames; the filtering fixture contains nested fields and 20 repeated items. These are single aggregate development runs, not published performance claims: they do not provide median or dispersion and must be repeated on controlled hardware before comparison with another agent.
|
|
32
|
+
|
|
33
|
+
## Version 0.3 resilience benchmark
|
|
34
|
+
|
|
35
|
+
Run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ITERATIONS=10000 bundle _1.17.3_ exec ruby benchmarks/retry_backlog.rb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The benchmark opens the circuit after one synthetic network failure, retains at most 100 serialized events, and measures bounded rejection into a full backlog. It performs no network I/O and is intended to detect accidental unbounded growth or excessive control-path overhead, not to claim production throughput.
|
|
42
|
+
|
|
43
|
+
A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 with 1,000 capture, serialization, and filtering iterations plus 10,000 queue and outage iterations:
|
|
44
|
+
|
|
45
|
+
| Measurement | Result |
|
|
46
|
+
|---|---:|
|
|
47
|
+
| Asynchronous local capture | 1,824 µs/event |
|
|
48
|
+
| Full serialization | 3,961 µs/event |
|
|
49
|
+
| Privacy filtering fixture | 777 µs/pass |
|
|
50
|
+
| Bounded queue | 1,648,533 operations/second |
|
|
51
|
+
| Open-circuit backlog handling | 88,922 operations/second |
|
|
52
|
+
|
|
53
|
+
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.
|
data/docs/privacy-lgpd.md
CHANGED
|
@@ -1,16 +1,86 @@
|
|
|
1
1
|
# Privacy and LGPD
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
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.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Default policy
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
| Data | Default behavior |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Keys such as `password`, `secret`, `token`, `authorization`, `cookie`, `session`, `card_number`, `cpf`, and `cnpj` | Replaced with `[FILTERED]` |
|
|
10
|
+
| Bearer tokens and JWT-like strings | Replaced in free text |
|
|
11
|
+
| E-mail addresses | Replaced with `[FILTERED_EMAIL]` |
|
|
12
|
+
| CPF and CNPJ candidates with valid check digits | Replaced with `[FILTERED_DOCUMENT]` |
|
|
13
|
+
| Payment-card candidates that pass the Luhn check | Replaced with `[FILTERED_CARD]` |
|
|
14
|
+
| IPv4 addresses | Last octet replaced with `0` |
|
|
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 |
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
2. allowlist context fields in application code;
|
|
11
|
-
3. inspect payloads against a local fake server before production;
|
|
12
|
-
4. keep TLS verification enabled;
|
|
13
|
-
5. document the lawful purpose and retention policy in the SaaS;
|
|
14
|
-
6. disable the agent in environments where collection is not authorized.
|
|
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.
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
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
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
Chronos.configure do |config|
|
|
26
|
+
# required connection settings omitted
|
|
27
|
+
config.blocklist_keys += [:medical_record, /bank_account/i]
|
|
28
|
+
config.allowlist_keys += [:authorization_state]
|
|
29
|
+
config.hash_keys += [:customer_id]
|
|
30
|
+
config.anonymize_ip = true
|
|
31
|
+
|
|
32
|
+
config.filters << proc do |key, value|
|
|
33
|
+
key.to_s == "internal_reference" ? "[REMOVED]" : value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
An allowlisted key bypasses only key-name redaction. Content detection remains active, so an allowlisted field containing a Bearer token or e-mail address is still filtered. Identifier hashing uses SHA-256 scoped by the public project identifier. It is irreversible output, but low-entropy identifiers may still be guessable and should not be treated as anonymized data.
|
|
39
|
+
|
|
40
|
+
Custom filters receive the key and already sanitized value. If a filter raises, that field becomes `[FILTERED]`; the error does not escape into the host application.
|
|
41
|
+
|
|
42
|
+
## Health applications
|
|
43
|
+
|
|
44
|
+
Do not send diagnoses, exam results, prescriptions, patient names, or free-form clinical notes. Use a scoped opaque identifier only when it is necessary and authorized:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
config.blocklist_keys += [:patient_name, :diagnosis, :clinical_notes]
|
|
48
|
+
config.hash_keys += [:patient_id]
|
|
49
|
+
|
|
50
|
+
Chronos.notify(error, :context => {
|
|
51
|
+
"operation" => "schedule_exam",
|
|
52
|
+
"patient_id" => "internal-opaque-id"
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Financial applications
|
|
57
|
+
|
|
58
|
+
Do not send cardholder names, full account identifiers, CVV, authentication tokens, or transaction payloads. Prefer categorical operational context:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
config.blocklist_keys += [:account_number, :pix_key, :bank_payload]
|
|
62
|
+
config.hash_keys += [:customer_id]
|
|
63
|
+
|
|
64
|
+
Chronos.notify(error, :context => {
|
|
65
|
+
"operation" => "authorize_payment",
|
|
66
|
+
"provider" => "example-gateway",
|
|
67
|
+
"customer_id" => "internal-opaque-id"
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Payload audit procedure
|
|
72
|
+
|
|
73
|
+
1. Point `host` to a local fake HTTP server controlled by the development team.
|
|
74
|
+
2. Exercise representative exception paths with synthetic sensitive fixtures.
|
|
75
|
+
3. Inspect the final JSON body, including exception messages and causes.
|
|
76
|
+
4. Search for every fixture value in plaintext.
|
|
77
|
+
5. Add missing application keys to `blocklist_keys` and repeat the audit.
|
|
78
|
+
6. Keep the audit fixtures synthetic and run the privacy contract in CI.
|
|
79
|
+
|
|
80
|
+
Run the included local example with:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bundle _1.17.3_ exec ruby examples/plain-ruby/privacy_audit.rb
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The output must contain redaction placeholders and must not contain any fixture secret.
|
data/docs/troubleshooting.md
CHANGED
|
@@ -10,7 +10,7 @@ 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.
|
|
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.
|
|
14
14
|
|
|
15
15
|
## Events disappear during shutdown
|
|
16
16
|
|
|
@@ -22,4 +22,4 @@ Workers are recreated after a process fork. Configure before or after forking, t
|
|
|
22
22
|
|
|
23
23
|
## Sensitive data
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Stop sending the affected field, rotate exposed credentials, and follow the incident process for the Chronos SaaS. Add the application-specific key to `blocklist_keys`, create a privacy contract fixture, and repeat the local payload audit. Pattern detection is defensive and cannot recognize every domain-specific identifier.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "net/http"
|
|
2
2
|
require "openssl"
|
|
3
|
+
require "json"
|
|
3
4
|
require "stringio"
|
|
4
5
|
require "uri"
|
|
5
6
|
require "zlib"
|
|
@@ -10,16 +11,17 @@ module Chronos
|
|
|
10
11
|
#
|
|
11
12
|
# @responsibility Send serialized events over bounded HTTPS requests.
|
|
12
13
|
# @motivation Use the Ruby standard library to preserve legacy compatibility.
|
|
13
|
-
# @limits It classifies failures but
|
|
14
|
+
# @limits It classifies failures but leaves retry and backlog policy to the application layer.
|
|
14
15
|
# @collaborators Configuration::Snapshot, SerializedEvent, and TransportResult.
|
|
15
16
|
# @thread_safety Creates a new Net::HTTP connection per call and synchronizes health state.
|
|
16
17
|
# @compatibility Ruby 2.2.10 through Ruby 2.6; no Rails dependency.
|
|
17
18
|
# @example
|
|
18
19
|
# result = transport.send_event(serialized_event)
|
|
19
20
|
# @errors Network, TLS, and HTTP errors are returned, never raised to callers.
|
|
20
|
-
# @performance One bounded network request per
|
|
21
|
+
# @performance One bounded network request per delivery attempt in version 0.3.
|
|
21
22
|
class NetHttpTransport
|
|
22
23
|
EVENT_PATH = "/api/v1/events".freeze
|
|
24
|
+
REMOTE_CONFIGURATION_HEADER = "X-Chronos-Remote-Configuration".freeze
|
|
23
25
|
|
|
24
26
|
def initialize(config, logger = nil)
|
|
25
27
|
@config = config
|
|
@@ -100,7 +102,11 @@ module Chronos
|
|
|
100
102
|
def classify(response)
|
|
101
103
|
code = response.code.to_i
|
|
102
104
|
options = {:status_code => code}
|
|
103
|
-
|
|
105
|
+
if code >= 200 && code < 300
|
|
106
|
+
options[:remote_configuration] = parse_remote_configuration(response)
|
|
107
|
+
return Ports::TransportResult.new(:success, options)
|
|
108
|
+
end
|
|
109
|
+
return Ports::TransportResult.new(:request_timeout, options) if code == 408
|
|
104
110
|
if code == 429
|
|
105
111
|
options[:retry_after] = response["Retry-After"]
|
|
106
112
|
return Ports::TransportResult.new(:rate_limited, options)
|
|
@@ -110,6 +116,18 @@ module Chronos
|
|
|
110
116
|
Ports::TransportResult.new(:client_error, options)
|
|
111
117
|
end
|
|
112
118
|
|
|
119
|
+
def parse_remote_configuration(response)
|
|
120
|
+
return nil unless @config.remote_configuration
|
|
121
|
+
|
|
122
|
+
value = response[REMOTE_CONFIGURATION_HEADER]
|
|
123
|
+
return nil if value.nil? || value.bytesize > @config.remote_config_max_bytes
|
|
124
|
+
|
|
125
|
+
parsed = JSON.parse(value)
|
|
126
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
127
|
+
rescue JSON::ParserError, EncodingError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
113
131
|
def request_body(body)
|
|
114
132
|
return body unless @config.gzip
|
|
115
133
|
|
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,
|
|
4
|
+
# @responsibility Own transport, resilient delivery pipeline, and the capture use case.
|
|
5
5
|
# @motivation Keep construction details outside the public module facade.
|
|
6
6
|
# @limits It does not integrate with Rails, Rack, or job systems.
|
|
7
|
-
# @collaborators Configuration snapshot, CaptureException,
|
|
7
|
+
# @collaborators Configuration snapshot, CaptureException, DeliveryPipeline, and NetHttpTransport.
|
|
8
8
|
# @thread_safety Runtime collaborators synchronize mutable state.
|
|
9
9
|
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
10
10
|
# @example
|
|
@@ -23,9 +23,16 @@ module Chronos
|
|
|
23
23
|
unless Ports::Transport.compatible?(@transport)
|
|
24
24
|
raise ArgumentError, "transport does not implement the Chronos transport port"
|
|
25
25
|
end
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
pipeline_options = {}
|
|
27
|
+
pipeline_options[:queue] = options[:queue] if options[:queue]
|
|
28
|
+
pipeline_options[:worker_pool] = options[:worker_pool] if options[:worker_pool]
|
|
29
|
+
@delivery_pipeline = options[:delivery_pipeline] || Application::DeliveryPipeline.new(
|
|
30
|
+
config,
|
|
31
|
+
@transport,
|
|
32
|
+
@logger,
|
|
33
|
+
pipeline_options
|
|
34
|
+
)
|
|
35
|
+
@capture = options[:capture] || Application::CaptureException.new(config, @delivery_pipeline, @logger)
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
def notify(exception, context = {})
|
|
@@ -37,21 +44,22 @@ module Chronos
|
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
def flush(timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
40
|
-
@
|
|
47
|
+
@delivery_pipeline.flush(timeout)
|
|
41
48
|
rescue StandardError => error
|
|
42
49
|
@logger.warn("Chronos flush failed: #{error.class}")
|
|
43
50
|
false
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
def close(timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
47
|
-
@
|
|
54
|
+
@delivery_pipeline.close(timeout)
|
|
48
55
|
rescue StandardError => error
|
|
49
56
|
@logger.warn("Chronos close failed: #{error.class}")
|
|
50
57
|
false
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
def diagnostics
|
|
54
|
-
@
|
|
61
|
+
details = @delivery_pipeline.diagnostics
|
|
62
|
+
details[:queue].merge(details)
|
|
55
63
|
end
|
|
56
64
|
end
|
|
57
65
|
end
|
|
@@ -2,10 +2,10 @@ module Chronos
|
|
|
2
2
|
module Application
|
|
3
3
|
# Orchestrates exception normalization, serialization, queueing, and delivery.
|
|
4
4
|
#
|
|
5
|
-
# @responsibility Execute the version 0.
|
|
5
|
+
# @responsibility Execute the version 0.3 exception capture pipeline.
|
|
6
6
|
# @motivation Keep the public facade and transport adapters free of use-case logic.
|
|
7
|
-
# @limits It does not implement
|
|
8
|
-
# @collaborators NoticeBuilder, PayloadSerializer,
|
|
7
|
+
# @limits It does not implement framework hooks or automatic capture.
|
|
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.
|
|
11
11
|
# @example
|
|
@@ -13,20 +13,25 @@ module Chronos
|
|
|
13
13
|
# @errors All internal StandardError failures are contained and logged.
|
|
14
14
|
# @performance Local work is bounded before asynchronous queue insertion.
|
|
15
15
|
class CaptureException
|
|
16
|
-
def initialize(config,
|
|
16
|
+
def initialize(config, delivery_pipeline, logger = nil, options = {})
|
|
17
17
|
@config = config
|
|
18
|
-
@
|
|
19
|
-
@transport = transport
|
|
18
|
+
@delivery_pipeline = delivery_pipeline
|
|
20
19
|
@logger = logger || Internal::SafeLogger.new(config.logger)
|
|
21
20
|
@notice_builder = options[:notice_builder] || Core::NoticeBuilder.new(config)
|
|
22
|
-
@serializer = options[:serializer] || Core::PayloadSerializer.new(
|
|
21
|
+
@serializer = options[:serializer] || Core::PayloadSerializer.new(
|
|
22
|
+
config,
|
|
23
|
+
nil,
|
|
24
|
+
:max_payload_size => proc { @delivery_pipeline.max_payload_size }
|
|
25
|
+
)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def call(exception, context = {})
|
|
26
29
|
return false unless capture_enabled?
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
@
|
|
31
|
+
notice = build_notice(exception, context)
|
|
32
|
+
return false unless @delivery_pipeline.capture_allowed?("exception", notice.fingerprint)
|
|
33
|
+
|
|
34
|
+
@delivery_pipeline.enqueue(@serializer.call(notice))
|
|
30
35
|
rescue StandardError => error
|
|
31
36
|
diagnose(error)
|
|
32
37
|
false
|
|
@@ -35,8 +40,10 @@ module Chronos
|
|
|
35
40
|
def call_sync(exception, context = {})
|
|
36
41
|
return false unless capture_enabled?
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
@
|
|
43
|
+
notice = build_notice(exception, context)
|
|
44
|
+
return false unless @delivery_pipeline.capture_allowed?("exception", notice.fingerprint)
|
|
45
|
+
|
|
46
|
+
@delivery_pipeline.deliver_sync(@serializer.call(notice))
|
|
40
47
|
rescue StandardError => error
|
|
41
48
|
diagnose(error)
|
|
42
49
|
false
|
|
@@ -48,9 +55,8 @@ module Chronos
|
|
|
48
55
|
@config.enabled_for_environment? && @config.error_notifications
|
|
49
56
|
end
|
|
50
57
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
@serializer.call(notice)
|
|
58
|
+
def build_notice(exception, context)
|
|
59
|
+
@notice_builder.call(exception, context)
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
def diagnose(error)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Application
|
|
3
|
+
# Small circuit breaker for retryable transport failures.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Stop delivery attempts temporarily after repeated failures.
|
|
6
|
+
# @motivation Prevent retry storms while the Chronos endpoint is unavailable.
|
|
7
|
+
# @limits It keeps no payload and permits only one half-open probe.
|
|
8
|
+
# @collaborators DeliveryPipeline and a monotonic clock.
|
|
9
|
+
# @thread_safety All transitions are protected by a mutex.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @example
|
|
12
|
+
# transport.send_event(event) if breaker.allow_request?
|
|
13
|
+
# @errors Clock failures are contained by DeliveryPipeline.
|
|
14
|
+
# @performance Constant time with fixed memory.
|
|
15
|
+
class CircuitBreaker
|
|
16
|
+
def initialize(failure_threshold, reset_timeout, clock)
|
|
17
|
+
@failure_threshold = failure_threshold
|
|
18
|
+
@reset_timeout = reset_timeout
|
|
19
|
+
@clock = clock
|
|
20
|
+
@state = :closed
|
|
21
|
+
@failures = 0
|
|
22
|
+
@opened_at = nil
|
|
23
|
+
@probe_in_flight = false
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def allow_request?
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
return true if @state == :closed
|
|
30
|
+
return false if @state == :half_open && @probe_in_flight
|
|
31
|
+
return false unless reset_elapsed?
|
|
32
|
+
|
|
33
|
+
@state = :half_open
|
|
34
|
+
@probe_in_flight = true
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def record_success
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@state = :closed
|
|
42
|
+
@failures = 0
|
|
43
|
+
@opened_at = nil
|
|
44
|
+
@probe_in_flight = false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def record_failure
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
@failures += 1
|
|
51
|
+
if @state == :half_open || @failures >= @failure_threshold
|
|
52
|
+
@state = :open
|
|
53
|
+
@opened_at = @clock.call
|
|
54
|
+
end
|
|
55
|
+
@probe_in_flight = false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def state
|
|
60
|
+
@mutex.synchronize { @state }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def reset_elapsed?
|
|
66
|
+
@opened_at && (@clock.call - @opened_at >= @reset_timeout)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|