chronos-ruby 0.8.0.pre.1 → 0.9.0.pre.2
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 +58 -0
- data/README.md +40 -10
- data/SECURITY.md +2 -0
- data/contracts/deploy-v1.schema.json +26 -0
- data/contracts/event-v1.schema.json +15 -1
- data/docs/adr/ADR-017-deploy-events-and-correlation.md +25 -0
- data/docs/adr/ADR-018-pre-1.0-hardening.md +9 -0
- data/docs/architecture.md +7 -1
- data/docs/compatibility.md +7 -2
- data/docs/configuration.md +18 -3
- data/docs/data-collected.md +6 -3
- data/docs/deprecation-policy.md +7 -0
- data/docs/examples/plain-ruby.md +7 -1
- data/docs/migration-from-airbrake.md +18 -0
- data/docs/modules/active-job.md +7 -0
- data/docs/modules/configuration.md +2 -0
- data/docs/modules/dependencies.md +2 -2
- data/docs/modules/deploy-tracking.md +86 -0
- data/docs/modules/ignore-rules.md +11 -0
- data/docs/modules/rails-legacy.md +3 -3
- data/docs/modules/sidekiq-legacy.md +1 -1
- data/docs/modules/telemetry-events.md +3 -1
- data/docs/performance.md +26 -1
- data/docs/privacy-lgpd.md +11 -2
- data/docs/protocol-v1.md +5 -0
- data/docs/release-1.0-readiness.md +18 -0
- data/docs/security-review.md +16 -0
- data/docs/semver.md +11 -0
- data/docs/troubleshooting.md +12 -0
- data/lib/chronos/agent.rb +30 -2
- data/lib/chronos/application/capture_exception.rb +7 -0
- data/lib/chronos/application/capture_telemetry.rb +17 -0
- data/lib/chronos/application/dependency_reporter.rb +12 -1
- data/lib/chronos/application/ignore_policy.rb +54 -0
- data/lib/chronos/application/remote_configuration.rb +1 -1
- data/lib/chronos/capistrano.rb +4 -0
- data/lib/chronos/configuration/apm_validation.rb +11 -0
- data/lib/chronos/configuration/validation.rb +16 -1
- data/lib/chronos/configuration.rb +14 -3
- data/lib/chronos/core/correlation_context.rb +65 -0
- data/lib/chronos/core/deploy_normalizer.rb +102 -0
- data/lib/chronos/core/payload_serializer.rb +4 -1
- data/lib/chronos/core/telemetry_event.rb +31 -4
- data/lib/chronos/integrations/active_job.rb +119 -0
- data/lib/chronos/integrations/capistrano.rb +61 -0
- data/lib/chronos/observability_facade.rb +9 -2
- data/lib/chronos/rails/installer.rb +7 -0
- data/lib/chronos/rails/notifications_subscriber.rb +29 -1
- data/lib/chronos/rails.rb +1 -0
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +11 -1
- metadata +19 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Bounded local ignore rules
|
|
2
|
+
|
|
3
|
+
Configure startup rules with `config.ignore_rules` or add a rule to the active agent with `Chronos.ignore_if`. A rule receives an immutable normalized notice and must return exactly `true` to discard it before serialization, queueing, retry, or network work.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Chronos.ignore_if do |notice|
|
|
7
|
+
notice.exception_class == "SomeExpectedError"
|
|
8
|
+
end
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
At most `max_ignore_rules` rules are retained (default 20, hard maximum 100). Registration returns `false` when Chronos is not configured, the object is not callable, or the limit is full. A rule failure is contained and treated as no match. Rules run in the capture caller and therefore must be fast, thread-safe, and free of I/O. Remote configuration cannot add executable rules.
|
|
@@ -28,10 +28,10 @@ The public `ActiveSupport::Notifications.subscribe` API is used for:
|
|
|
28
28
|
| `render_template.action_view` | `request` with `kind=view` | template basename and duration |
|
|
29
29
|
| `sql.active_record` | `query` | operation name, cached flag, and duration |
|
|
30
30
|
| `deliver.action_mailer` | `job` with `kind=mailer` | mailer, action, and duration |
|
|
31
|
-
| `perform.active_job` | `job` | job class, queue,
|
|
31
|
+
| `perform.active_job` | `job` | adapter, job/provider IDs, class, queue, attempts, duration, status, and error class |
|
|
32
32
|
| cache read/write/hit notifications | `cache` | operation, backend, namespace, hit/miss, duration, and optional scoped key hash |
|
|
33
33
|
|
|
34
|
-
Raw SQL, binds, cache keys, mail recipients and bodies, job
|
|
34
|
+
Raw SQL, binds, cache keys, mail recipients and bodies, job arguments, request/response bodies, cookies, and authorization headers are not copied. Documented Active Job IDs are collected, while public job arguments remain unchanged and omitted. Template identifiers are reduced to their basename. Every event passes through `Sanitizer`, `SafeSerializer`, the bounded queue, retry policy, circuit breaker, and memory backlog.
|
|
35
35
|
|
|
36
36
|
## Controller exceptions and deduplication
|
|
37
37
|
|
|
@@ -57,4 +57,4 @@ The repository contains independent applications under `examples/rails-4.2` and
|
|
|
57
57
|
|
|
58
58
|
## Limits
|
|
59
59
|
|
|
60
|
-
Version 0.7 aggregates controller, SQL, and job timings into bounded APM batches. SQL literals and binds remain excluded while a bounded normalized query and fingerprint are produced. Sidekiq support remains a separate optional integration
|
|
60
|
+
Version 0.7 aggregates controller, SQL, and job timings into bounded APM batches. SQL literals and binds remain excluded while a bounded normalized query and fingerprint are produced. Sidekiq support remains a separate optional integration. Version `0.9.0.pre.2` provides bounded Active Job trace/request propagation through a namespaced serialized field; adapters that replace the standard Active Job serialization/execution hooks require separate compatibility evidence.
|
|
@@ -4,7 +4,7 @@ Version `0.6.0.pre.1` starts the legacy jobs line with optional Sidekiq 4 and 5
|
|
|
4
4
|
|
|
5
5
|
```ruby
|
|
6
6
|
gem "sidekiq", "~> 5.0"
|
|
7
|
-
gem "chronos-ruby", "0.
|
|
7
|
+
gem "chronos-ruby", "0.9.0.pre.2", :require => "chronos/sidekiq"
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
`chronos/sidekiq` installs middleware through the public `configure_client` and `configure_server` APIs. It does nothing when Sidekiq is unavailable, and the core gem never requires Sidekiq. Installation adds no Chronos thread or Redis/database connection per job; delivery continues through the agent's existing fixed worker pool.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Framework telemetry events
|
|
2
2
|
|
|
3
|
-
`Chronos::Core::TelemetryEvent` and `Chronos::Core::TelemetrySerializer` extend the v1 envelope to `request`, `query`, `job`, `cache`, `external_http`, `dependencies`, and `metric_batch` events. These values exist so integrations do not model operational telemetry as fake exceptions.
|
|
3
|
+
`Chronos::Core::TelemetryEvent` and `Chronos::Core::TelemetrySerializer` extend the v1 envelope to `request`, `query`, `job`, `cache`, `external_http`, `dependencies`, `deploy`, and `metric_batch` events. These values exist so integrations do not model operational telemetry as fake exceptions.
|
|
4
4
|
|
|
5
5
|
Telemetry has the same `schema_version`, event ID, timestamps, project, environment, service, runtime, context, sanitization, payload-size limit, idempotency header, asynchronous queue, retry, circuit breaker, and memory backlog used by exception events. Unsupported types are rejected locally.
|
|
6
6
|
|
|
7
7
|
`Chronos.record_event` is the narrow integration entry point. Application code should prefer documented higher-level APIs; its payload is allowlisted by each bundled integration and then sanitized. Remote configuration can reduce or disable any locally enabled telemetry type but cannot enable a type excluded by local configuration.
|
|
8
8
|
|
|
9
9
|
Version 0.8 aggregates request, query, job, and enabled external HTTP observations into `metric_batch` events by default. Cache remains an individual event while contributing to an existing traced request breakdown. Dependencies use one separate event per agent. Set `apm_enabled = false` only when individual legacy telemetry is required for diagnostics. Percentiles remain server-side; see [Essential APM aggregation](apm-aggregation.md).
|
|
10
|
+
|
|
11
|
+
Version 0.9 delivers deploy telemetry synchronously because deployment commands are short-lived. Every telemetry and exception envelope contains the fixed correlation fields documented in [Deploy tracking and release correlation](deploy-tracking.md).
|
data/docs/performance.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Performance
|
|
2
2
|
|
|
3
|
-
Performance is a functional requirement, but version 0.
|
|
3
|
+
Performance is a functional requirement, but version 0.9 makes no unverified speed claim.
|
|
4
4
|
|
|
5
5
|
Current controls:
|
|
6
6
|
|
|
@@ -24,9 +24,22 @@ Current controls:
|
|
|
24
24
|
- outbound HTTP instrumentation uses two clock reads and bounded metadata without body/header traversal;
|
|
25
25
|
- cache normalization is bounded and SHA-256 runs only when explicitly enabled;
|
|
26
26
|
- dependency inventory runs at most once per agent and is capped at 200 loaded specs.
|
|
27
|
+
- event correlation copies exactly seven strings with a 128-byte bound each;
|
|
28
|
+
- deploy notification uses the existing synchronous delivery path and creates no timer or worker type.
|
|
27
29
|
|
|
28
30
|
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, `benchmarks/request_overhead.rb` compares Rack-protocol calls, and `benchmarks/rails_notifications.rb` isolates subscriber normalization overhead.
|
|
29
31
|
|
|
32
|
+
## Version 0.9.0.pre.2 release gates
|
|
33
|
+
|
|
34
|
+
`benchmarks/comparative.rb` compares the same successful Rack fixture without and with Chronos instrumentation. It performs configurable warmup, at least three samples, and reports median plus median absolute deviation. `benchmarks/fake_endpoint_load.rb` sends asynchronous exception events to a local TCP endpoint, verifies the v1 schema marker, ensures the secret key is absent from every payload, and fails on loss, rejection, invalid payload, or timeout.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
ITERATIONS=50000 WARMUP=5000 SAMPLES=7 bundle exec ruby benchmarks/comparative.rb
|
|
38
|
+
ITERATIONS=500 bundle exec ruby benchmarks/fake_endpoint_load.rb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Results are environment-specific evidence, not a general speed claim. Record CPU, OS, Ruby, gem commit, and environment variables with any published result. Airbrake comparison remains optional and must use a legally compatible, equivalent sanitized payload on the same supported runtime.
|
|
42
|
+
|
|
30
43
|
## Version 0.5 Rails subscriber benchmark
|
|
31
44
|
|
|
32
45
|
Run:
|
|
@@ -115,3 +128,15 @@ ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/external_http.rb
|
|
|
115
128
|
The fixture compares a no-op Net::HTTP-compatible object with the per-instance wrapper. It includes trace-header injection, bounded outcome capture, and clock reads, but excludes DNS, sockets, TLS, serialization, queueing, and network delivery. Record a controlled result before making a performance claim.
|
|
116
129
|
|
|
117
130
|
A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 and 10,000 calls. It measured approximately 15.155 microseconds of wrapper overhead per call. This single run has no controlled warmup, median, or dispersion and is not a production performance claim.
|
|
131
|
+
|
|
132
|
+
## Version 0.9 correlation benchmark
|
|
133
|
+
|
|
134
|
+
Run:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/correlation.rb
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The fixture builds the seven-field frozen correlation hash from an immutable configuration. It excludes notice/event construction, sanitization, JSON, queueing, retry, and network delivery. Record a controlled result before making a performance claim.
|
|
141
|
+
|
|
142
|
+
A local diagnostic run on 2026-07-21 used Ruby 2.2.10 on macOS arm64 and 10,000 correlations. It measured approximately 20.490 microseconds per correlation. This single run has no controlled warmup, median, or dispersion and is not a production performance claim.
|
data/docs/privacy-lgpd.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Privacy and LGPD
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
3
|
+
Version 0.9 sanitizes exception, framework telemetry, dependency/deploy inventory, correlation, and APM metric batches 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
|
|
|
@@ -19,8 +19,17 @@ Version 0.8 sanitizes exception, framework telemetry, dependency inventory, and
|
|
|
19
19
|
| External HTTP | Host/method/status/timing only; URL path/query, Authorization, bodies, headers, and error messages omitted |
|
|
20
20
|
| Cache key | Omitted by default; optional project-scoped SHA-256 hash; cache value never read |
|
|
21
21
|
| Dependencies | Bounded loaded gem names/versions and detected runtime labels; paths and lockfiles omitted |
|
|
22
|
+
| Deploy correlation | Explicit bounded operational identifiers; no automatic Git or environment scan |
|
|
23
|
+
| Repository | Credentials removed from common HTTP/SCP forms; query/fragment omitted for parsed URLs |
|
|
24
|
+
| Actor | Explicit and sanitized; omit it when personal identity is unnecessary |
|
|
22
25
|
|
|
23
|
-
The retry backlog exists only in process memory, accepts only `SerializedEvent`, has a fixed capacity, and disappears on process exit. Version 0.
|
|
26
|
+
The retry backlog exists only in process memory, accepts only `SerializedEvent`, has a fixed capacity, and disappears on process exit. Version 0.9 does not persist telemetry to disk.
|
|
27
|
+
|
|
28
|
+
## Deploy metadata
|
|
29
|
+
|
|
30
|
+
Release, revision, deploy ID, environment, service, region, and instance are operational identifiers but can become personal or sensitive when applications encode customer, tenant, employee, or infrastructure-secret values in them. Use opaque bounded identifiers. Repository should be an owner/name identifier rather than a credential-bearing clone URL. Actor is optional; prefer a service account label when individual identity is unnecessary.
|
|
31
|
+
|
|
32
|
+
The deploy normalizer removes user information from standard HTTP(S) repository URLs and usernames from SCP-style Git references. This is defensive, not a substitute for secret management. Never pass access tokens in any deploy field. All values still traverse the common sanitizer before transport.
|
|
24
33
|
|
|
25
34
|
## External services, cache, and dependencies
|
|
26
35
|
|
data/docs/protocol-v1.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Protocol v1 stability
|
|
2
|
+
|
|
3
|
+
The schemas under `contracts/` are the source of truth for protocol v1. Version `0.9.0.pre.2` keeps `schema_version: "1.0"` and treats every required field, enum value, privacy exclusion, and maximum as a compatibility contract.
|
|
4
|
+
|
|
5
|
+
Compatible changes may add optional bounded fields or new event types accepted by the server. Removing or renaming a field, changing its type/meaning, weakening a bound, or making an optional field required needs a new protocol major schema. Authentication remains outside the JSON payload. Contract tests and `script/verify_docs` must pass before a release.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Version 1.0 readiness
|
|
2
|
+
|
|
3
|
+
Version `0.9.0.pre.2` is a hardening release, not the stable release. The local suite, lint, documentation verifier, build, load test, and comparative benchmark are required evidence. The release can advance to `1.0.0` only after the GitHub Actions matrix is green for every declared legacy Ruby/framework/job combination.
|
|
4
|
+
|
|
5
|
+
| Gate | State in pre.2 |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Ruby pure, Rack, Rails 4.2/5.2 | Implemented; external legacy matrix must pass |
|
|
8
|
+
| Sidekiq 4/5 real gems | Dedicated Docker matrix added; must pass |
|
|
9
|
+
| Active Job metadata/error/context | Implemented and contract-tested |
|
|
10
|
+
| filters, bounded ignore rules, context, breadcrumbs | Implemented and documented |
|
|
11
|
+
| request/SQL/job APM, deploy, retry/backlog, remote configuration | Implemented and contract-tested |
|
|
12
|
+
| payload fixture privacy | Contract-tested |
|
|
13
|
+
| public API/options/protocol review | Documented; final review required |
|
|
14
|
+
| Airbrake migration, SemVer, deprecation, security review | Added in pre.2 |
|
|
15
|
+
| fake endpoint load and repeatable comparison | Executable gates added |
|
|
16
|
+
| package signing | Not currently feasible; protected secret plus checksum is the interim control |
|
|
17
|
+
|
|
18
|
+
Do not change compatibility status from `Best effort` or create a `v1.0.0` tag until all external jobs and the dependency audit pass without skips.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Security review for 0.9.0.pre.2
|
|
2
|
+
|
|
3
|
+
Review date: 2026-07-21. Scope: capture, serialization, transport, remote configuration, framework/job integrations, release workflow, examples, and fixtures.
|
|
4
|
+
|
|
5
|
+
Verified by contracts and implementation review:
|
|
6
|
+
|
|
7
|
+
- the secret project key is sent only in an authentication header and never in the event body;
|
|
8
|
+
- sanitization and payload limits run before queueing, retry, backlog, or transport;
|
|
9
|
+
- queue, workers, retries, delays, circuit state, backlog, breadcrumbs, APM groups, dependencies, ignore rules, and propagated identifiers are bounded;
|
|
10
|
+
- TLS verification is default and plain HTTP requires explicit `ssl_verify = false`;
|
|
11
|
+
- remote configuration is size-limited, allowlisted, and cannot replace credentials/host or install executable rules;
|
|
12
|
+
- integrations contain agent failures and do not collect bodies, authorization, raw SQL/binds, mail content, or raw cache keys;
|
|
13
|
+
- Active Job propagation uses a namespaced v1 field containing only bounded trace/request identifiers and does not alter job arguments;
|
|
14
|
+
- fixture privacy is enforced by contract tests and dependency advisories are checked by the security workflow.
|
|
15
|
+
|
|
16
|
+
Residual risks: supported Ruby/Rails versions are end-of-life; in-memory backlog is lost at exit; application filters/ignore rules execute application code; project identifiers and documented job IDs may be personal data in some deployments; package signing is not enabled because no trusted certificate/key lifecycle exists. Release artifacts should use protected environments and published SHA-256 checksums until signing can be operated safely.
|
data/docs/semver.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Semantic Versioning policy
|
|
2
|
+
|
|
3
|
+
Chronos follows Semantic Versioning for the gem API and the independently versioned event protocol.
|
|
4
|
+
|
|
5
|
+
- Before `1.0.0`, incompatible changes may occur only in a documented prerelease and must appear in the changelog and migration notes.
|
|
6
|
+
- From `1.0.0`, patch releases contain compatible fixes, minor releases add compatible capability, and major releases may remove or change public behavior.
|
|
7
|
+
- Public API includes documented `Chronos` methods, configuration options, supported require paths, integration entry points, and documented return/error behavior.
|
|
8
|
+
- Internal constants and files explicitly described as internal are not public API.
|
|
9
|
+
- The JSON `schema_version` is independent of the gem version. Additive optional v1 fields are compatible; removing, renaming, or changing required v1 fields requires a new protocol major version.
|
|
10
|
+
|
|
11
|
+
A release is not promoted to stable while its supported matrix or mandatory release gates are incomplete.
|
data/docs/troubleshooting.md
CHANGED
|
@@ -36,6 +36,18 @@ Set `external_http_trace_headers = false` before configuring the agent, or do no
|
|
|
36
36
|
|
|
37
37
|
The event is emitted once per agent and contains only gems loaded at collection time, capped by `dependency_max_items`. It does not parse the lockfile or activate optional frameworks. Call `Chronos.report_dependencies` after application boot if the first event can occur before all integrations load, or set `dependency_reporting = false` to disable collection.
|
|
38
38
|
|
|
39
|
+
## `Chronos.notify_deploy` returns false
|
|
40
|
+
|
|
41
|
+
Confirm Chronos is configured/enabled, environment is present, and at least revision or version is non-empty. Check credentials, TLS, proxy, timeout, retry/circuit state, and endpoint response. The call is synchronous and deliberately returns the delivery result. Repository credentials are removed and do not help authenticate with Chronos.
|
|
42
|
+
|
|
43
|
+
## Events have empty or stale release correlation
|
|
44
|
+
|
|
45
|
+
Set `app_version`, `revision`, `deploy_id`, `environment`, `service_name`, `region`, and `instance_id` before `Chronos.configure` creates its snapshot. `notify_deploy` does not mutate a running agent because that would mix release identities across concurrent events. Restart/configure each deployed process with the new values.
|
|
46
|
+
|
|
47
|
+
## Capistrano task is missing
|
|
48
|
+
|
|
49
|
+
Require `chronos/capistrano` after the Capistrano/Rake task DSL is loaded. The installer registers once per DSL and hooks `chronos:notify_deploy` after `deploy:published`. Confirm the base Chronos configuration runs in the task process and the expected `stage`, `current_revision`, `repo_url`, and `chronos_*` variables exist.
|
|
50
|
+
|
|
39
51
|
## Sidekiq telemetry is missing
|
|
40
52
|
|
|
41
53
|
Require `chronos/sidekiq` after Sidekiq is available and configure Chronos before jobs run. The entry point uses Sidekiq's public client/server middleware configuration and remains optional. Requiring only `chronos` or `chronos/rails` does not load Sidekiq. A job already enqueued before client middleware installation may lack propagated trace context, but server timing and failure capture can still run.
|
data/lib/chronos/agent.rb
CHANGED
|
@@ -37,7 +37,7 @@ module Chronos
|
|
|
37
37
|
pipeline_options
|
|
38
38
|
)
|
|
39
39
|
initialize_capture(options)
|
|
40
|
-
|
|
40
|
+
initialize_observability(options)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def notify(exception, context = {})
|
|
@@ -50,6 +50,10 @@ module Chronos
|
|
|
50
50
|
@capture.call_sync(exception, context_for_capture(context))
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def ignore_if(&block)
|
|
54
|
+
@ignore_policy.add(&block)
|
|
55
|
+
end
|
|
56
|
+
|
|
53
57
|
def with_context(context = {}, &block)
|
|
54
58
|
@context_store.with_context(context, &block)
|
|
55
59
|
end
|
|
@@ -67,6 +71,8 @@ module Chronos
|
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def record_event(event_type, payload = {}, context = {})
|
|
74
|
+
return false if event_type.to_s == "deploy"
|
|
75
|
+
|
|
70
76
|
report_dependencies unless event_type.to_s == "dependencies"
|
|
71
77
|
@telemetry.call(event_type, payload, telemetry_context(context))
|
|
72
78
|
end
|
|
@@ -81,6 +87,18 @@ module Chronos
|
|
|
81
87
|
false
|
|
82
88
|
end
|
|
83
89
|
|
|
90
|
+
def notify_deploy(attributes = {}, timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
91
|
+
payload = @deploy_normalizer.call(attributes)
|
|
92
|
+
return false unless @telemetry.call_sync("deploy", payload, {})
|
|
93
|
+
|
|
94
|
+
@dependency_reporter.reset(payload["version"]) if @dependency_reporter.respond_to?(:reset)
|
|
95
|
+
report_dependencies
|
|
96
|
+
flush(timeout)
|
|
97
|
+
rescue StandardError => error
|
|
98
|
+
@logger.warn("Chronos deploy notification failed: #{error.class}")
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
|
|
84
102
|
def record_event_once(key, event_type, payload = {}, context = {})
|
|
85
103
|
execution = @context_store.get
|
|
86
104
|
captured = execution[:__chronos_captured_events] || {}
|
|
@@ -172,10 +190,20 @@ module Chronos
|
|
|
172
190
|
private
|
|
173
191
|
|
|
174
192
|
def initialize_capture(options)
|
|
175
|
-
@
|
|
193
|
+
@ignore_policy = options[:ignore_policy] || Application::IgnorePolicy.new(
|
|
194
|
+
@config.ignore_rules, @config.max_ignore_rules, @logger
|
|
195
|
+
)
|
|
196
|
+
@capture = options[:capture] || Application::CaptureException.new(
|
|
197
|
+
@config, @delivery_pipeline, @logger, :ignore_policy => @ignore_policy
|
|
198
|
+
)
|
|
176
199
|
@telemetry = options[:telemetry] || Application::CaptureTelemetry.new(@config, @delivery_pipeline, @logger)
|
|
177
200
|
end
|
|
178
201
|
|
|
202
|
+
def initialize_observability(options)
|
|
203
|
+
@dependency_reporter = options[:dependency_reporter] || Application::DependencyReporter.new(@config)
|
|
204
|
+
@deploy_normalizer = options[:deploy_normalizer] || Core::DeployNormalizer.new(@config)
|
|
205
|
+
end
|
|
206
|
+
|
|
179
207
|
def build_context_store(strategy)
|
|
180
208
|
return Adapters::ThreadLocalContextStore.new if strategy == :thread_local
|
|
181
209
|
|
|
@@ -18,6 +18,7 @@ module Chronos
|
|
|
18
18
|
@delivery_pipeline = delivery_pipeline
|
|
19
19
|
@logger = logger || Internal::SafeLogger.new(config.logger)
|
|
20
20
|
@notice_builder = options[:notice_builder] || Core::NoticeBuilder.new(config)
|
|
21
|
+
@ignore_policy = options[:ignore_policy]
|
|
21
22
|
@serializer = options[:serializer] || Core::PayloadSerializer.new(
|
|
22
23
|
config,
|
|
23
24
|
nil,
|
|
@@ -29,6 +30,7 @@ module Chronos
|
|
|
29
30
|
return false unless capture_enabled?
|
|
30
31
|
|
|
31
32
|
notice = build_notice(exception, context)
|
|
33
|
+
return false if ignored?(notice)
|
|
32
34
|
return false unless @delivery_pipeline.capture_allowed?("exception", notice.fingerprint)
|
|
33
35
|
|
|
34
36
|
@delivery_pipeline.enqueue(@serializer.call(notice))
|
|
@@ -41,6 +43,7 @@ module Chronos
|
|
|
41
43
|
return false unless capture_enabled?
|
|
42
44
|
|
|
43
45
|
notice = build_notice(exception, context)
|
|
46
|
+
return false if ignored?(notice)
|
|
44
47
|
return false unless @delivery_pipeline.capture_allowed?("exception", notice.fingerprint)
|
|
45
48
|
|
|
46
49
|
@delivery_pipeline.deliver_sync(@serializer.call(notice))
|
|
@@ -59,6 +62,10 @@ module Chronos
|
|
|
59
62
|
@notice_builder.call(exception, context)
|
|
60
63
|
end
|
|
61
64
|
|
|
65
|
+
def ignored?(notice)
|
|
66
|
+
@ignore_policy && @ignore_policy.ignored?(notice)
|
|
67
|
+
end
|
|
68
|
+
|
|
62
69
|
def diagnose(error)
|
|
63
70
|
@logger.warn("Chronos capture failed: #{error.class}")
|
|
64
71
|
end
|
|
@@ -40,6 +40,17 @@ module Chronos
|
|
|
40
40
|
false
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def call_sync(event_type, payload = {}, context = {})
|
|
44
|
+
return false unless @config.enabled_for_environment?
|
|
45
|
+
return false unless synchronous_capture_allowed?(event_type)
|
|
46
|
+
|
|
47
|
+
event = Core::TelemetryEvent.new(event_type, payload, context)
|
|
48
|
+
@delivery_pipeline.deliver_sync(@serializer.call(event))
|
|
49
|
+
rescue StandardError => error
|
|
50
|
+
@logger.warn("Chronos synchronous telemetry capture failed: #{error.class}")
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
43
54
|
def flush
|
|
44
55
|
enqueue_batches(@aggregator.flush)
|
|
45
56
|
rescue StandardError => error
|
|
@@ -55,6 +66,12 @@ module Chronos
|
|
|
55
66
|
|
|
56
67
|
private
|
|
57
68
|
|
|
69
|
+
def synchronous_capture_allowed?(event_type)
|
|
70
|
+
return @delivery_pipeline.event_enabled?(event_type) if event_type.to_s == "deploy"
|
|
71
|
+
|
|
72
|
+
@delivery_pipeline.capture_allowed?(event_type)
|
|
73
|
+
end
|
|
74
|
+
|
|
58
75
|
def aggregate_type?(event_type)
|
|
59
76
|
ApmAggregator::METRIC_TYPES.include?(event_type.to_s)
|
|
60
77
|
end
|
|
@@ -19,6 +19,17 @@ module Chronos
|
|
|
19
19
|
@constants = options[:constants] || {}
|
|
20
20
|
@mutex = Mutex.new
|
|
21
21
|
@reported = false
|
|
22
|
+
@release_override = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset(release = nil)
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
@release_override = bounded(release, 128)
|
|
28
|
+
@reported = false
|
|
29
|
+
end
|
|
30
|
+
true
|
|
31
|
+
rescue StandardError
|
|
32
|
+
false
|
|
22
33
|
end
|
|
23
34
|
|
|
24
35
|
def call
|
|
@@ -45,7 +56,7 @@ module Chronos
|
|
|
45
56
|
"web_server" => detected("web_server") { web_server },
|
|
46
57
|
"database_adapter" => detected("database_adapter") { database_adapter },
|
|
47
58
|
"sidekiq" => detected("sidekiq") { sidekiq_version },
|
|
48
|
-
"release" => bounded(@config.app_version.to_s, 128)
|
|
59
|
+
"release" => @release_override || bounded(@config.app_version.to_s, 128)
|
|
49
60
|
}
|
|
50
61
|
payload.delete_if do |key, value|
|
|
51
62
|
!["dependencies", "ruby"].include?(key) && value.to_s.empty?
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Application
|
|
3
|
+
# Stores and evaluates bounded application-owned exception ignore rules.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Decide whether a normalized Notice should be discarded locally.
|
|
6
|
+
# @motivation Expected application failures should not consume queue or network capacity.
|
|
7
|
+
# @limits Rules inspect only the bounded Notice API and cannot be installed remotely.
|
|
8
|
+
# @collaborators CaptureException and application-provided callables.
|
|
9
|
+
# @thread_safety Rule registration and snapshots are protected by a mutex.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @example
|
|
12
|
+
# policy.add { |notice| notice.exception_class == "ExpectedError" }
|
|
13
|
+
# @errors A failing rule is contained, logged, and treated as not matching.
|
|
14
|
+
# @performance The number of evaluated rules is capped by configuration.
|
|
15
|
+
class IgnorePolicy
|
|
16
|
+
def initialize(rules, limit, logger)
|
|
17
|
+
@rules = Array(rules).dup
|
|
18
|
+
@limit = limit
|
|
19
|
+
@logger = logger
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add(rule = nil, &block)
|
|
24
|
+
candidate = rule || block
|
|
25
|
+
return false unless candidate.respond_to?(:call)
|
|
26
|
+
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
return false if @rules.length >= @limit
|
|
29
|
+
|
|
30
|
+
@rules << candidate
|
|
31
|
+
end
|
|
32
|
+
true
|
|
33
|
+
rescue StandardError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ignored?(notice)
|
|
38
|
+
rules = @mutex.synchronize { @rules.dup }
|
|
39
|
+
rules.any? { |rule| safely_matches?(rule, notice) }
|
|
40
|
+
rescue StandardError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def safely_matches?(rule, notice)
|
|
47
|
+
rule.call(notice) == true
|
|
48
|
+
rescue StandardError => error
|
|
49
|
+
@logger.warn("Chronos ignore rule failed: #{error.class}")
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -14,7 +14,7 @@ module Chronos
|
|
|
14
14
|
# @performance Validation is bounded by configured document and list limits.
|
|
15
15
|
class RemoteConfiguration
|
|
16
16
|
SUPPORTED_EVENT_TYPES = %w(
|
|
17
|
-
exception request query job cache external_http dependencies metric_batch
|
|
17
|
+
exception request query job cache external_http dependencies deploy metric_batch
|
|
18
18
|
).freeze
|
|
19
19
|
MAX_IGNORED_FINGERPRINTS = 100
|
|
20
20
|
MAX_FINGERPRINT_BYTES = 256
|
|
@@ -65,9 +65,20 @@ module Chronos
|
|
|
65
65
|
unless dependency_max_items.is_a?(Integer) && dependency_max_items >= 1 && dependency_max_items <= 200
|
|
66
66
|
errors << "dependency_max_items must be between 1 and 200"
|
|
67
67
|
end
|
|
68
|
+
correlation_attributes.each do |name, value|
|
|
69
|
+
errors << "#{name} must be a String with at most 128 bytes" unless bounded_optional_string?(value, 128)
|
|
70
|
+
end
|
|
68
71
|
errors
|
|
69
72
|
end
|
|
70
73
|
|
|
74
|
+
def correlation_attributes
|
|
75
|
+
{:revision => revision, :deploy_id => deploy_id, :region => region, :instance_id => instance_id}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def bounded_optional_string?(value, limit)
|
|
79
|
+
value.nil? || (value.is_a?(String) && value.bytesize <= limit)
|
|
80
|
+
end
|
|
81
|
+
|
|
71
82
|
def boolean?(value)
|
|
72
83
|
[true, false].include?(value)
|
|
73
84
|
end
|
|
@@ -9,7 +9,7 @@ module Chronos
|
|
|
9
9
|
# @thread_safety Validation reads one configuration instance without shared state.
|
|
10
10
|
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
11
|
# @errors Invalid values become messages; malformed URLs are contained.
|
|
12
|
-
module ConfigurationValidation
|
|
12
|
+
module ConfigurationValidation # rubocop:disable Metrics/ModuleLength
|
|
13
13
|
private
|
|
14
14
|
|
|
15
15
|
def resilience_errors
|
|
@@ -60,6 +60,7 @@ module Chronos
|
|
|
60
60
|
errors.concat(matcher_errors("allowlist_keys", allowlist_keys))
|
|
61
61
|
errors.concat(matcher_errors("hash_keys", hash_keys))
|
|
62
62
|
errors.concat(filter_errors)
|
|
63
|
+
errors.concat(ignore_rule_errors)
|
|
63
64
|
errors.concat(anonymization_errors)
|
|
64
65
|
errors
|
|
65
66
|
end
|
|
@@ -99,6 +100,20 @@ module Chronos
|
|
|
99
100
|
[true, false].include?(anonymize_ip) ? [] : ["anonymize_ip must be true or false"]
|
|
100
101
|
end
|
|
101
102
|
|
|
103
|
+
def ignore_rule_errors
|
|
104
|
+
errors = []
|
|
105
|
+
unless ignore_rules.is_a?(Array) && ignore_rules.all? { |rule| rule.respond_to?(:call) }
|
|
106
|
+
errors << "ignore_rules must contain only callable objects"
|
|
107
|
+
end
|
|
108
|
+
unless positive_integer?(max_ignore_rules) && max_ignore_rules <= 100
|
|
109
|
+
errors << "max_ignore_rules must be an integer between 1 and 100"
|
|
110
|
+
end
|
|
111
|
+
if ignore_rules.is_a?(Array) && max_ignore_rules.is_a?(Integer) && ignore_rules.length > max_ignore_rules
|
|
112
|
+
errors << "ignore_rules cannot exceed max_ignore_rules"
|
|
113
|
+
end
|
|
114
|
+
errors
|
|
115
|
+
end
|
|
116
|
+
|
|
102
117
|
def matcher_errors(name, values)
|
|
103
118
|
return [] unless values.is_a?(Array)
|
|
104
119
|
return [] if values.all? { |value| value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Regexp) }
|
|
@@ -32,11 +32,12 @@ module Chronos
|
|
|
32
32
|
|
|
33
33
|
ATTRIBUTES = [
|
|
34
34
|
:project_id, :project_key, :host, :environment, :app_version,
|
|
35
|
-
:service_name, :
|
|
35
|
+
:service_name, :revision, :deploy_id, :region, :instance_id,
|
|
36
|
+
:root_directory, :logger, :timeout, :open_timeout,
|
|
36
37
|
:queue_size, :workers, :enabled, :error_notifications,
|
|
37
38
|
:ignored_environments, :proxy, :ssl_verify, :user_agent,
|
|
38
39
|
:max_payload_size, :gzip, :blocklist_keys, :allowlist_keys,
|
|
39
|
-
:filters, :hash_keys, :anonymize_ip, :max_retries,
|
|
40
|
+
:filters, :hash_keys, :anonymize_ip, :ignore_rules, :max_ignore_rules, :max_retries,
|
|
40
41
|
:retry_base_interval, :retry_max_interval, :retry_jitter,
|
|
41
42
|
:backlog_size, :circuit_failure_threshold, :circuit_reset_timeout,
|
|
42
43
|
:remote_configuration, :remote_config_max_bytes, :sampling_rate,
|
|
@@ -59,6 +60,7 @@ module Chronos
|
|
|
59
60
|
initialize_rails_defaults
|
|
60
61
|
initialize_apm_defaults
|
|
61
62
|
initialize_observability_defaults
|
|
63
|
+
initialize_correlation_defaults
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def snapshot
|
|
@@ -120,6 +122,13 @@ module Chronos
|
|
|
120
122
|
@breadcrumb_max_bytes = 2048
|
|
121
123
|
end
|
|
122
124
|
|
|
125
|
+
def initialize_correlation_defaults
|
|
126
|
+
@revision = nil
|
|
127
|
+
@deploy_id = nil
|
|
128
|
+
@region = nil
|
|
129
|
+
@instance_id = nil
|
|
130
|
+
end
|
|
131
|
+
|
|
123
132
|
def initialize_rails_defaults
|
|
124
133
|
@rails_enabled = true
|
|
125
134
|
@rails_capture_in_console = false
|
|
@@ -153,6 +162,8 @@ module Chronos
|
|
|
153
162
|
@filters = []
|
|
154
163
|
@hash_keys = []
|
|
155
164
|
@anonymize_ip = true
|
|
165
|
+
@ignore_rules = []
|
|
166
|
+
@max_ignore_rules = 20
|
|
156
167
|
end
|
|
157
168
|
|
|
158
169
|
def initialize_resilience_defaults
|
|
@@ -167,7 +178,7 @@ module Chronos
|
|
|
167
178
|
@remote_config_max_bytes = 4096
|
|
168
179
|
@sampling_rate = 1.0
|
|
169
180
|
@enabled_event_types = [
|
|
170
|
-
"exception", "request", "query", "job", "cache", "external_http", "dependencies", "metric_batch"
|
|
181
|
+
"exception", "request", "query", "job", "cache", "external_http", "dependencies", "deploy", "metric_batch"
|
|
171
182
|
]
|
|
172
183
|
@max_remote_send_interval = 60.0
|
|
173
184
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Core
|
|
3
|
+
# Builds the bounded release/deploy correlation shared by every event envelope.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Normalize release, revision, deploy, environment, service, region, and instance.
|
|
6
|
+
# @motivation Give the SaaS stable before/after-deploy dimensions without integration-specific fields.
|
|
7
|
+
# @limits Values come only from explicit configuration or caller overrides; no environment is scanned.
|
|
8
|
+
# @collaborators PayloadSerializer, TelemetrySerializer, and Configuration::Snapshot.
|
|
9
|
+
# @thread_safety Instances hold immutable configuration and calls allocate independent frozen hashes.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @example
|
|
12
|
+
# CorrelationContext.new(config).call("revision" => "abc123")
|
|
13
|
+
# @errors Unreadable optional values become nil and do not escape.
|
|
14
|
+
# @performance Seven strings are copied with a fixed 128-byte maximum each.
|
|
15
|
+
class CorrelationContext
|
|
16
|
+
FIELDS = %w(release revision deploy_id environment service region instance).freeze
|
|
17
|
+
|
|
18
|
+
def initialize(config)
|
|
19
|
+
@config = config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(overrides = {})
|
|
23
|
+
values = defaults.merge(string_hash(overrides))
|
|
24
|
+
result = FIELDS.each_with_object({}) do |name, correlation|
|
|
25
|
+
correlation[name.freeze] = bounded(values[name], 128)
|
|
26
|
+
end
|
|
27
|
+
result.each_value { |value| value.freeze if value }
|
|
28
|
+
result.freeze
|
|
29
|
+
rescue StandardError
|
|
30
|
+
FIELDS.each_with_object({}) { |name, fallback| fallback[name] = nil }.freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def defaults
|
|
36
|
+
{
|
|
37
|
+
"release" => @config.app_version, "revision" => @config.revision,
|
|
38
|
+
"deploy_id" => @config.deploy_id, "environment" => @config.environment,
|
|
39
|
+
"service" => @config.service_name, "region" => @config.region,
|
|
40
|
+
"instance" => @config.instance_id
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def string_hash(value)
|
|
45
|
+
return {} unless value.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
value.each_with_object({}) do |(key, child), result|
|
|
48
|
+
result[key.to_s] = child if key.is_a?(String) || key.is_a?(Symbol)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bounded(value, limit)
|
|
53
|
+
return nil if value.nil?
|
|
54
|
+
return nil unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric)
|
|
55
|
+
|
|
56
|
+
text = value.to_s
|
|
57
|
+
text = text.scrub("?") if text.respond_to?(:scrub)
|
|
58
|
+
text = text.byteslice(0, limit) if text.bytesize > limit
|
|
59
|
+
text
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|