chronos-ruby 0.9.0.pre.3 → 0.9.0.pre.4
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 +16 -0
- data/README.md +24 -3
- data/contracts/integration-verification-response-v1.schema.json +76 -0
- data/docs/data-collected.md +3 -0
- data/docs/modules/integration-verification.md +65 -0
- data/docs/modules/sidekiq-legacy.md +1 -1
- data/docs/performance.md +1 -1
- data/docs/protocol-v1.md +3 -1
- data/docs/release-1.0-readiness.md +2 -2
- data/docs/security-review.md +3 -2
- data/docs/troubleshooting.md +6 -0
- data/lib/chronos/adapters/net_http_transport.rb +36 -2
- data/lib/chronos/agent.rb +21 -0
- data/lib/chronos/application/delivery_pipeline.rb +6 -3
- data/lib/chronos/application/verify_integration.rb +262 -0
- data/lib/chronos/core/integration_verification_result.rb +108 -0
- data/lib/chronos/errors.rb +11 -0
- data/lib/chronos/ports/transport.rb +18 -1
- data/lib/chronos/rails/railtie.rb +5 -0
- data/lib/chronos/rake_tasks.rb +41 -0
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +26 -0
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84266b3c93fc92c15268d44e3625762f471c9cf99fa0523b6f5d883f846906e4
|
|
4
|
+
data.tar.gz: bf3d2c641bbb7f117a1c9c76519fa756057f72108d899cb64d86e2eeff305423
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b54a3aef38f706677794b39740558586d9c69f083a3c2b3f2dc6ea20e3b60f2fe84765eb61a44fe2f230a03c03dcb27047cdd7a1853b14fc805fcb914d7cfb8
|
|
7
|
+
data.tar.gz: 2f915f461e658f93269d35e52f6d728ff32ef90a4a6ac765a6b92194a1fc53b06ca5350b2bc34c8d84197bf2263d523ead9a62ada46edbd9ae46363d3de27e05
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes are documented here. The project follows Semantic Versioning
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.0.pre.4] - 2026-07-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `Chronos.verify_integration` and `chronos:verify_integration` send a correlated synthetic exception and report credential, project, receiver, and receipt status as bounded JSON;
|
|
12
|
+
- strict integration-verification response contract v1, Rails task registration, failure classification, security limits, and end-to-end tests.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- version advanced to `0.9.0.pre.4` because `0.9.0.pre.3` had already been published before integration verification was added.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- legacy dependency resolution pins `parallel` to Ruby 2.2-compatible `1.19.2` and the Sidekiq 5 smoke pins `rack-protection` to Ruby 2.5-compatible `2.2.4`.
|
|
21
|
+
- the Rails verification task now always depends on `environment`, even when Rails defines that Rake task after the Chronos Railtie hook runs.
|
|
22
|
+
|
|
7
23
|
## [0.9.0.pre.3] - 2026-07-21
|
|
8
24
|
|
|
9
25
|
### Changed
|
data/README.md
CHANGED
|
@@ -33,7 +33,7 @@ See [Compatibility](docs/compatibility.md).
|
|
|
33
33
|
The current public build is a pre-release. Add its exact version to the application's `Gemfile`:
|
|
34
34
|
|
|
35
35
|
```ruby
|
|
36
|
-
gem "chronos-ruby", "0.9.0.pre.
|
|
36
|
+
gem "chronos-ruby", "0.9.0.pre.4"
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Install with a Bundler version compatible with the application. For the oldest supported runtime:
|
|
@@ -54,7 +54,7 @@ gem install chronos-ruby --pre
|
|
|
54
54
|
Version 0.5 exposes Rails support explicitly, keeping Rails and ActiveSupport out of plain Ruby applications:
|
|
55
55
|
|
|
56
56
|
```ruby
|
|
57
|
-
gem "chronos-ruby", "0.9.0.pre.
|
|
57
|
+
gem "chronos-ruby", "0.9.0.pre.4", :require => "chronos/rails"
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Generate the initializer with:
|
|
@@ -84,6 +84,27 @@ end
|
|
|
84
84
|
|
|
85
85
|
HTTPS verification is enabled by default. HTTP requires explicitly setting `ssl_verify = false` and should only be used with a local test server.
|
|
86
86
|
|
|
87
|
+
## Verify the Chronos integration
|
|
88
|
+
|
|
89
|
+
After configuring the application, Rails projects can verify credentials and end-to-end ingestion with:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
bundle exec rake chronos:verify_integration
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The task sends one synthetic exception identified as an integration verification and prints one JSON object. It exits with status `0` only when Chronos confirms valid credentials and receipt of the correlated event; configuration, authentication, inactive-project, network, protocol, rate-limit, and receiver failures exit nonzero with safe guidance.
|
|
96
|
+
|
|
97
|
+
Plain Ruby Rakefiles can install the same task explicitly after loading their configuration:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
require "chronos"
|
|
101
|
+
# Chronos.configure { |config| ... }
|
|
102
|
+
require "chronos/rake_tasks"
|
|
103
|
+
Chronos::RakeTasks.install
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Ruby code may call `result = Chronos.verify_integration`, inspect `result.success?`, and serialize `result.to_h` or `result.to_json`. The result contains only bounded contract fields; it never exposes `project_key`, raw server responses, stack traces, source paths, or receiver internals. See [Integration verification](docs/modules/integration-verification.md).
|
|
107
|
+
|
|
87
108
|
## Automatic capture
|
|
88
109
|
|
|
89
110
|
Rack applications can capture unhandled exceptions automatically and preserve the application error semantics:
|
|
@@ -201,7 +222,7 @@ Version `0.6.0.pre.1` adds optional Sidekiq 4/5 middleware:
|
|
|
201
222
|
|
|
202
223
|
```ruby
|
|
203
224
|
gem "sidekiq", "~> 5.0"
|
|
204
|
-
gem "chronos-ruby", "0.9.0.pre.
|
|
225
|
+
gem "chronos-ruby", "0.9.0.pre.4", :require => "chronos/sidekiq"
|
|
205
226
|
```
|
|
206
227
|
|
|
207
228
|
The client middleware propagates only trace/request identifiers in a versioned Sidekiq-envelope field and never changes worker arguments. The server records class, queue, JID, retry count, duration, calculable queue latency, bounded arguments/tags, status, and error class. Values pass through the shared sanitizer before delivery. Failed jobs are notified once and the original exception is re-raised. See [Sidekiq 4/5 legacy integration](docs/modules/sidekiq-legacy.md).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://chronos.example/schemas/integration-verification-response-v1.schema.json",
|
|
4
|
+
"title": "Chronos integration verification response v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"success",
|
|
10
|
+
"status",
|
|
11
|
+
"verification_id",
|
|
12
|
+
"credentials_valid",
|
|
13
|
+
"event_received",
|
|
14
|
+
"event",
|
|
15
|
+
"project",
|
|
16
|
+
"receiver",
|
|
17
|
+
"error"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"schema_version": {"const": "1.0"},
|
|
21
|
+
"success": {"type": "boolean"},
|
|
22
|
+
"status": {
|
|
23
|
+
"enum": [
|
|
24
|
+
"accepted",
|
|
25
|
+
"invalid_credentials",
|
|
26
|
+
"project_inactive",
|
|
27
|
+
"receiver_unavailable",
|
|
28
|
+
"receiver_internal_error",
|
|
29
|
+
"request_rejected",
|
|
30
|
+
"rate_limited"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"verification_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
|
34
|
+
"credentials_valid": {"type": ["boolean", "null"]},
|
|
35
|
+
"event_received": {"type": "boolean"},
|
|
36
|
+
"event": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"required": ["id"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"id": {"type": ["string", "null"], "maxLength": 128}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"project": {
|
|
45
|
+
"type": ["object", "null"],
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"required": ["id", "name", "status", "environment"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"id": {"type": ["string", "null"], "maxLength": 128},
|
|
50
|
+
"name": {"type": ["string", "null"], "maxLength": 128},
|
|
51
|
+
"status": {"type": ["string", "null"], "maxLength": 32},
|
|
52
|
+
"environment": {"type": ["string", "null"], "maxLength": 128}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"receiver": {
|
|
56
|
+
"type": ["object", "null"],
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"required": ["name", "status", "received_at"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"name": {"type": ["string", "null"], "maxLength": 64},
|
|
61
|
+
"status": {"type": ["string", "null"], "maxLength": 32},
|
|
62
|
+
"received_at": {"type": ["string", "null"], "maxLength": 64}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"error": {
|
|
66
|
+
"type": ["object", "null"],
|
|
67
|
+
"additionalProperties": false,
|
|
68
|
+
"required": ["code", "message", "guidance"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"code": {"type": "string", "maxLength": 64},
|
|
71
|
+
"message": {"type": "string", "maxLength": 512},
|
|
72
|
+
"guidance": {"type": "string", "maxLength": 512}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
data/docs/data-collected.md
CHANGED
|
@@ -48,6 +48,7 @@ Version 0.9 emits exceptions, cache telemetry, dependency/deploy events, and agg
|
|
|
48
48
|
| Rails, web server, database adapter, Sidekiq, release | Included when safely detectable/configured | Loaded constants/specs and `app_version` |
|
|
49
49
|
| Event release, revision, deploy ID, environment, service, region, instance | Present as bounded correlation; values optional | Explicit immutable configuration or normalized deploy payload |
|
|
50
50
|
| Deploy environment, revision, version, repository, actor, deploy ID, service, region, instance | Explicit synchronous deploy API | Application/deployment integration arguments |
|
|
51
|
+
| Integration verification ID, fixed test marker, synthetic exception class/message, and receipt correlation | Only when `Chronos.verify_integration` or its Rake task is invoked | Locally generated bounded values |
|
|
51
52
|
|
|
52
53
|
The gem never collects request bodies, response bodies, raw query strings, cookies, HTTP authorization headers, environment variables in bulk, Git state, source code, raw SQL, SQL bind values, database rows, raw cache keys/values, mail bodies/recipients, Active Job arguments, gem paths, or lockfile contents. Active Job IDs, Sidekiq JIDs/arguments, loaded gem names/versions, and deploy fields are documented integration data.
|
|
53
54
|
|
|
@@ -55,4 +56,6 @@ APM dimensions never include user ID, job ID, raw URL, exception message, bind v
|
|
|
55
56
|
|
|
56
57
|
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.
|
|
57
58
|
|
|
59
|
+
The integration check uses the fixed class `Chronos::IntegrationVerificationError`, tag/fingerprint `chronos-integration-verification`, and `context.integration_verification.test: true`. Its returned object allowlists only project identity/status/environment, receiver name/status/receipt time, correlation IDs, booleans, and safe guidance. Raw response bodies and Chronos implementation details are discarded.
|
|
60
|
+
|
|
58
61
|
See [Privacy and LGPD](privacy-lgpd.md) for redaction rules and the payload audit procedure.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Integration verification
|
|
2
|
+
|
|
3
|
+
`Chronos.verify_integration` performs an explicit end-to-end check of configuration, credentials, receiver availability, and ingestion acknowledgement. Rails registers `chronos:verify_integration` automatically through the Railtie. The command prints exactly one JSON object and exits nonzero unless verification succeeds.
|
|
4
|
+
|
|
5
|
+
## Flow and ownership
|
|
6
|
+
|
|
7
|
+
`Chronos::Application::VerifyIntegration` creates one normal v1 exception envelope, delivers it synchronously through `DeliveryPipeline`, and validates the response. `IntegrationVerificationResult` exposes the immutable, bounded public outcome. `RakeTasks` only loads the application environment, calls the facade, prints JSON, and selects the exit status.
|
|
8
|
+
|
|
9
|
+
The synthetic event is recognizable without changing the event protocol:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"event_type": "exception",
|
|
14
|
+
"payload": {
|
|
15
|
+
"exception": {"class": "Chronos::IntegrationVerificationError"},
|
|
16
|
+
"tags": ["chronos-integration-verification"],
|
|
17
|
+
"fingerprint": "chronos-integration-verification"
|
|
18
|
+
},
|
|
19
|
+
"context": {
|
|
20
|
+
"integration_verification": {
|
|
21
|
+
"schema_version": "1.0",
|
|
22
|
+
"verification_id": "generated-uuid",
|
|
23
|
+
"kind": "integration_verification",
|
|
24
|
+
"test": true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Chronos must authenticate before acknowledging the event and must correlate both `verification_id` and the envelope event ID. A `2xx` response alone is insufficient. The accepted response must conform exactly to [`integration-verification-response-v1.schema.json`](../../contracts/integration-verification-response-v1.schema.json):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"schema_version": "1.0",
|
|
35
|
+
"success": true,
|
|
36
|
+
"status": "accepted",
|
|
37
|
+
"verification_id": "generated-uuid",
|
|
38
|
+
"credentials_valid": true,
|
|
39
|
+
"event_received": true,
|
|
40
|
+
"event": {"id": "event-uuid"},
|
|
41
|
+
"project": {"id": "project-id", "name": "Project", "status": "active", "environment": "production"},
|
|
42
|
+
"receiver": {"name": "chronos", "status": "operational", "received_at": "2026-07-22T12:00:00Z"},
|
|
43
|
+
"error": null
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The client rejects missing, mismatched, or additional fields. It copies only the allowlisted project and receiver values and never returns the raw body.
|
|
48
|
+
|
|
49
|
+
## Result and failure classification
|
|
50
|
+
|
|
51
|
+
The Ruby result supports `success?`, `to_h`, and `to_json`. Its status is `verified` only after the correlated acknowledgement. Failures use `configuration_invalid`, `invalid_credentials`, `project_inactive`, `receiver_unavailable`, `receiver_internal_error`, `rate_limited`, `request_rejected`, `invalid_response`, or `verification_failed`.
|
|
52
|
+
|
|
53
|
+
- `401` and untrusted `403` responses become `invalid_credentials` with instructions to create an active project API key and verify the configured identifiers.
|
|
54
|
+
- A contractual authenticated `403` response may become `project_inactive`.
|
|
55
|
+
- network errors, timeouts, an open circuit, and `502`/`503`/`504` become `receiver_unavailable`.
|
|
56
|
+
- other `5xx` responses become `receiver_internal_error`.
|
|
57
|
+
- malformed or uncorrelated success responses become `invalid_response`.
|
|
58
|
+
|
|
59
|
+
Failure output uses local messages and guidance. Server exception messages, stack traces, SQL, paths, classes, credentials, response headers, and architecture details are never copied.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
In Rails, configure Chronos normally and run `bundle exec rake chronos:verify_integration`. In plain Ruby, require `chronos/rake_tasks` and call `Chronos::RakeTasks.install` from the Rakefile. Programmatic callers can use `Chronos.verify_integration` directly.
|
|
64
|
+
|
|
65
|
+
Verification is an explicit synchronous diagnostic operation. It bypasses sampling and ignore rules so an operator-requested check cannot silently disappear, but it still uses the configured bounded retry, timeout, TLS, circuit, serializer, and transport protections. The receiver should record it as a verification/audit receipt rather than a production application incident.
|
|
@@ -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.9.0.pre.
|
|
7
|
+
gem "chronos-ruby", "0.9.0.pre.4", :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.
|
data/docs/performance.md
CHANGED
|
@@ -29,7 +29,7 @@ Current controls:
|
|
|
29
29
|
|
|
30
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.
|
|
31
31
|
|
|
32
|
-
## Version 0.9.0.pre.
|
|
32
|
+
## Version 0.9.0.pre.4 release gates
|
|
33
33
|
|
|
34
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
35
|
|
data/docs/protocol-v1.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Protocol v1 stability
|
|
2
2
|
|
|
3
|
-
The schemas under `contracts/` are the source of truth for protocol v1. Version `0.9.0.pre.
|
|
3
|
+
The schemas under `contracts/` are the source of truth for protocol v1. Version `0.9.0.pre.4` keeps `schema_version: "1.0"` and treats every required field, enum value, privacy exclusion, and maximum as a compatibility contract.
|
|
4
4
|
|
|
5
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.
|
|
6
|
+
|
|
7
|
+
The explicit integration check uses the normal exception envelope and identifies itself through `context.integration_verification`. Its receiver acknowledgement is governed separately by [`integration-verification-response-v1.schema.json`](../contracts/integration-verification-response-v1.schema.json). A successful HTTP status without a complete, correlated response is not proof of authentication or ingestion.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Version 1.0 readiness
|
|
2
2
|
|
|
3
|
-
Version `0.9.0.pre.
|
|
3
|
+
Version `0.9.0.pre.4` 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
4
|
|
|
5
|
-
| Gate | State in pre.
|
|
5
|
+
| Gate | State in pre.4 |
|
|
6
6
|
|---|---|
|
|
7
7
|
| Ruby pure, Rack, Rails 4.2/5.2 | Implemented; external legacy matrix must pass |
|
|
8
8
|
| Sidekiq 4/5 real gems | Dedicated Docker matrix added; must pass |
|
data/docs/security-review.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Security review for 0.9.0.pre.
|
|
1
|
+
# Security review for 0.9.0.pre.4
|
|
2
2
|
|
|
3
|
-
Review date: 2026-07-
|
|
3
|
+
Review date: 2026-07-22. Scope: capture, serialization, transport, integration verification, remote configuration, framework/job integrations, release workflow, examples, and fixtures.
|
|
4
4
|
|
|
5
5
|
Verified by contracts and implementation review:
|
|
6
6
|
|
|
@@ -12,5 +12,6 @@ Verified by contracts and implementation review:
|
|
|
12
12
|
- integrations contain agent failures and do not collect bodies, authorization, raw SQL/binds, mail content, or raw cache keys;
|
|
13
13
|
- Active Job propagation uses a namespaced v1 field containing only bounded trace/request identifiers and does not alter job arguments;
|
|
14
14
|
- fixture privacy is enforced by contract tests and dependency advisories are checked by the security workflow.
|
|
15
|
+
- integration verification accepts only a strict correlated response and never exposes raw receiver bodies, credentials, stack traces, paths, SQL, or internal architecture.
|
|
15
16
|
|
|
16
17
|
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/troubleshooting.md
CHANGED
|
@@ -12,6 +12,12 @@ The agent may be unconfigured, disabled, ignored in the current environment, una
|
|
|
12
12
|
|
|
13
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
14
|
|
|
15
|
+
## `chronos:verify_integration` fails
|
|
16
|
+
|
|
17
|
+
Read the single JSON object printed by the task and use `status` and `error.guidance`. `invalid_credentials` means an active project API key must be created or the configured `project_id`/`project_key` corrected. `project_inactive` means authentication succeeded but the selected project must be activated. `receiver_unavailable` covers DNS, TLS, network, timeout, circuit-open, and gateway/service-unavailable failures. `receiver_internal_error` means Chronos reached its own safe internal-error boundary. `invalid_response` means the receiver did not implement the correlated response v1 contract. Every failure exits nonzero and deliberately omits raw receiver messages and implementation details.
|
|
18
|
+
|
|
19
|
+
If Rails does not list the task, require `chronos/rails` and confirm the application initializer loads. A `not_configured` result means the current Rake process did not execute `Chronos.configure`; update to a release containing the Rails environment prerequisite fix and confirm the variables are exported into that process. In plain Ruby, require `chronos/rake_tasks` and call `Chronos::RakeTasks.install` from the Rakefile after loading configuration.
|
|
20
|
+
|
|
15
21
|
## Rack exception is not captured
|
|
16
22
|
|
|
17
23
|
Confirm that `Chronos.configure` runs before the middleware handles requests and that the middleware wraps the application component that raises. Version 0.5 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.
|
|
@@ -22,6 +22,7 @@ module Chronos
|
|
|
22
22
|
class NetHttpTransport
|
|
23
23
|
EVENT_PATH = "/api/v1/events".freeze
|
|
24
24
|
REMOTE_CONFIGURATION_HEADER = "X-Chronos-Remote-Configuration".freeze
|
|
25
|
+
RESPONSE_BODY_MAX_BYTES = 8192
|
|
25
26
|
|
|
26
27
|
def initialize(config, logger = nil)
|
|
27
28
|
@config = config
|
|
@@ -71,7 +72,11 @@ module Chronos
|
|
|
71
72
|
request["Idempotency-Key"] = event.event_id
|
|
72
73
|
request.body = request_body(event.body)
|
|
73
74
|
request["Content-Encoding"] = "gzip" if @config.gzip
|
|
74
|
-
http.start
|
|
75
|
+
http.start do |connection|
|
|
76
|
+
connection.request(request) do |response|
|
|
77
|
+
read_bounded_response(response)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
75
80
|
end
|
|
76
81
|
|
|
77
82
|
def endpoint_uri
|
|
@@ -101,7 +106,7 @@ module Chronos
|
|
|
101
106
|
|
|
102
107
|
def classify(response)
|
|
103
108
|
code = response.code.to_i
|
|
104
|
-
options = {:status_code => code}
|
|
109
|
+
options = {:status_code => code, :response => parse_response(response)}
|
|
105
110
|
if code >= 200 && code < 300
|
|
106
111
|
options[:remote_configuration] = parse_remote_configuration(response)
|
|
107
112
|
return Ports::TransportResult.new(:success, options)
|
|
@@ -116,6 +121,35 @@ module Chronos
|
|
|
116
121
|
Ports::TransportResult.new(:client_error, options)
|
|
117
122
|
end
|
|
118
123
|
|
|
124
|
+
def read_bounded_response(response)
|
|
125
|
+
body = ""
|
|
126
|
+
overflow = false
|
|
127
|
+
response.read_body do |chunk|
|
|
128
|
+
next if overflow
|
|
129
|
+
|
|
130
|
+
remaining = RESPONSE_BODY_MAX_BYTES - body.bytesize
|
|
131
|
+
if chunk.bytesize > remaining
|
|
132
|
+
overflow = true
|
|
133
|
+
else
|
|
134
|
+
body << chunk
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
response.instance_variable_set(:@chronos_response_body, overflow ? nil : body)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_response(response)
|
|
141
|
+
content_type = response["Content-Type"].to_s.split(";", 2).first
|
|
142
|
+
return nil unless content_type == "application/json"
|
|
143
|
+
|
|
144
|
+
body = response.instance_variable_get(:@chronos_response_body)
|
|
145
|
+
return nil if body.nil? || body.empty?
|
|
146
|
+
|
|
147
|
+
parsed = JSON.parse(body)
|
|
148
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
149
|
+
rescue JSON::ParserError, EncodingError
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
119
153
|
def parse_remote_configuration(response)
|
|
120
154
|
return nil unless @config.remote_configuration
|
|
121
155
|
|
data/lib/chronos/agent.rb
CHANGED
|
@@ -50,6 +50,24 @@ module Chronos
|
|
|
50
50
|
@capture.call_sync(exception, context_for_capture(context))
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def verify_integration
|
|
54
|
+
@verification.call
|
|
55
|
+
rescue StandardError => error
|
|
56
|
+
@logger.warn("Chronos integration verification failed: #{error.class}")
|
|
57
|
+
Core::IntegrationVerificationResult.new(
|
|
58
|
+
:success => false,
|
|
59
|
+
:status => "verification_failed",
|
|
60
|
+
:credentials_valid => nil,
|
|
61
|
+
:event => {"id" => nil, "received" => false},
|
|
62
|
+
:receiver => {"name" => "chronos", "status" => "not_checked", "received_at" => nil},
|
|
63
|
+
:error => {
|
|
64
|
+
"code" => "verification_failed",
|
|
65
|
+
"message" => "Chronos integration verification failed locally.",
|
|
66
|
+
"guidance" => "Review the Chronos configuration and retry."
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
53
71
|
def ignore_if(&block)
|
|
54
72
|
@ignore_policy.add(&block)
|
|
55
73
|
end
|
|
@@ -202,6 +220,9 @@ module Chronos
|
|
|
202
220
|
def initialize_observability(options)
|
|
203
221
|
@dependency_reporter = options[:dependency_reporter] || Application::DependencyReporter.new(@config)
|
|
204
222
|
@deploy_normalizer = options[:deploy_normalizer] || Core::DeployNormalizer.new(@config)
|
|
223
|
+
@verification = options[:verification] || Application::VerifyIntegration.new(
|
|
224
|
+
@config, @delivery_pipeline, @logger
|
|
225
|
+
)
|
|
205
226
|
end
|
|
206
227
|
|
|
207
228
|
def build_context_store(strategy)
|
|
@@ -42,13 +42,16 @@ module Chronos
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def deliver_sync(event)
|
|
45
|
+
deliver_sync_result(event).success?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def deliver_sync_result(event)
|
|
45
49
|
record_pre_delivery(event)
|
|
46
|
-
|
|
47
|
-
result && result.success?
|
|
50
|
+
deliver_with_backlog(event)
|
|
48
51
|
rescue StandardError => error
|
|
49
52
|
diagnose(error)
|
|
50
53
|
store_in_backlog(event)
|
|
51
|
-
|
|
54
|
+
Ports::TransportResult.new(:network_error, :error => error.class.name)
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
# WorkerPool delivery entry point. Events were counted when accepted into the queue.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Application
|
|
5
|
+
# Sends and verifies one synthetic exception against the configured Chronos receiver.
|
|
6
|
+
#
|
|
7
|
+
# @responsibility Build an identified test notice, deliver it synchronously, and validate its acknowledgement.
|
|
8
|
+
# @motivation Verify credentials and end-to-end ingestion without mistaking an empty 2xx for confirmation.
|
|
9
|
+
# @limits It exposes only allowlisted project/receiver fields and never returns raw server responses.
|
|
10
|
+
# @collaborators NoticeBuilder, PayloadSerializer, DeliveryPipeline, and IntegrationVerificationResult.
|
|
11
|
+
# @thread_safety Calls allocate independent IDs and immutable results; collaborators synchronize delivery.
|
|
12
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
|
|
13
|
+
# @example
|
|
14
|
+
# result = verifier.call
|
|
15
|
+
# result.success? #=> true or false
|
|
16
|
+
# @errors Network, protocol, configuration, and receiver failures become structured results.
|
|
17
|
+
# @performance Performs one bounded synchronous verification plus configured bounded retries.
|
|
18
|
+
class VerifyIntegration
|
|
19
|
+
SCHEMA_VERSION = "1.0".freeze
|
|
20
|
+
VERIFICATION_KIND = "integration_verification".freeze
|
|
21
|
+
VERIFICATION_TAG = "chronos-integration-verification".freeze
|
|
22
|
+
|
|
23
|
+
def initialize(config, delivery_pipeline, logger = nil, options = {})
|
|
24
|
+
@config = config
|
|
25
|
+
@delivery_pipeline = delivery_pipeline
|
|
26
|
+
@logger = logger || Internal::SafeLogger.new(config.logger)
|
|
27
|
+
@uuid_generator = options[:uuid_generator] || proc { SecureRandom.uuid }
|
|
28
|
+
@notice_builder = options[:notice_builder] || Core::NoticeBuilder.new(config)
|
|
29
|
+
@serializer = options[:serializer] || Core::PayloadSerializer.new(
|
|
30
|
+
config,
|
|
31
|
+
nil,
|
|
32
|
+
:max_payload_size => proc { @delivery_pipeline.max_payload_size }
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call
|
|
37
|
+
verification_id = bounded(@uuid_generator.call, 128)
|
|
38
|
+
return local_failure("configuration_invalid", verification_id, configuration_guidance) unless configured?
|
|
39
|
+
|
|
40
|
+
event = build_event(verification_id)
|
|
41
|
+
transport_result = @delivery_pipeline.deliver_sync_result(event)
|
|
42
|
+
classify(transport_result, verification_id, event.event_id)
|
|
43
|
+
rescue StandardError => error
|
|
44
|
+
@logger.warn("Chronos integration verification failed: #{error.class}")
|
|
45
|
+
local_failure("verification_failed", verification_id, "Review the Chronos configuration and retry.")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def configured?
|
|
51
|
+
!@config.project_id.to_s.empty? && !@config.project_key.to_s.empty? && !@config.host.to_s.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_event(verification_id)
|
|
55
|
+
marker = {
|
|
56
|
+
"schema_version" => SCHEMA_VERSION,
|
|
57
|
+
"verification_id" => verification_id,
|
|
58
|
+
"kind" => VERIFICATION_KIND,
|
|
59
|
+
"test" => true
|
|
60
|
+
}
|
|
61
|
+
notice = @notice_builder.call(
|
|
62
|
+
IntegrationVerificationError.new("Chronos integration verification test"),
|
|
63
|
+
:severity => "info",
|
|
64
|
+
:context => {"integration_verification" => marker},
|
|
65
|
+
:tags => [VERIFICATION_TAG],
|
|
66
|
+
:fingerprint => VERIFICATION_TAG
|
|
67
|
+
)
|
|
68
|
+
@serializer.call(notice)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def classify(result, verification_id, event_id)
|
|
72
|
+
return classify_success(result.response, verification_id, event_id) if result.success?
|
|
73
|
+
if inactive_response?(result, verification_id, event_id)
|
|
74
|
+
return project_inactive(result.response, verification_id, event_id)
|
|
75
|
+
end
|
|
76
|
+
return invalid_credentials(verification_id, event_id) if [401, 403].include?(result.status_code)
|
|
77
|
+
return rate_limited(verification_id, event_id) if result.status == :rate_limited
|
|
78
|
+
return receiver_unavailable(verification_id, event_id) if unavailable?(result)
|
|
79
|
+
return receiver_internal_error(verification_id, event_id) if result.status == :server_error
|
|
80
|
+
|
|
81
|
+
request_rejected(verification_id, event_id)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def classify_success(response, verification_id, event_id)
|
|
85
|
+
unless valid_acknowledgement?(response, verification_id, event_id)
|
|
86
|
+
return failure("invalid_response", verification_id, event_id,
|
|
87
|
+
:receiver_status => "reachable",
|
|
88
|
+
:message => "Chronos returned a response outside the verification contract.",
|
|
89
|
+
:guidance => "Update Chronos to the integration verification response v1 contract.")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Core::IntegrationVerificationResult.new(
|
|
93
|
+
:success => true,
|
|
94
|
+
:status => "verified",
|
|
95
|
+
:verification_id => verification_id,
|
|
96
|
+
:credentials_valid => true,
|
|
97
|
+
:event => {"id" => event_id, "received" => true},
|
|
98
|
+
:project => safe_project(response["project"]),
|
|
99
|
+
:receiver => safe_receiver(response["receiver"]),
|
|
100
|
+
:error => nil
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def valid_acknowledgement?(response, verification_id, event_id)
|
|
105
|
+
valid_response_structure?(response) &&
|
|
106
|
+
acknowledgement_matches?(response, verification_id, event_id) &&
|
|
107
|
+
active_project?(response["project"]) && operational_receiver?(response["receiver"])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def valid_response_structure?(response)
|
|
111
|
+
exact_keys?(response, %w(
|
|
112
|
+
schema_version success status verification_id credentials_valid event_received
|
|
113
|
+
event project receiver error
|
|
114
|
+
)) &&
|
|
115
|
+
exact_keys?(response["event"], %w(id)) &&
|
|
116
|
+
exact_keys?(response["project"], %w(id name status environment)) &&
|
|
117
|
+
exact_keys?(response["receiver"], %w(name status received_at))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def acknowledgement_matches?(response, verification_id, event_id)
|
|
121
|
+
response["schema_version"] == SCHEMA_VERSION && response["verification_id"] == verification_id &&
|
|
122
|
+
response["status"] == "accepted" && response["success"] == true &&
|
|
123
|
+
response["credentials_valid"] == true && response["event_received"] == true &&
|
|
124
|
+
hash_value(response["event"])["id"] == event_id && response["error"].nil?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def active_project?(project)
|
|
128
|
+
hash_value(project)["id"] == @config.project_id.to_s && hash_value(project)["status"] == "active"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def operational_receiver?(receiver)
|
|
132
|
+
hash_value(receiver)["status"] == "operational"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def inactive_response?(result, verification_id, event_id)
|
|
136
|
+
response = result.response
|
|
137
|
+
result.status_code == 403 && valid_response_structure?(response) &&
|
|
138
|
+
inactive_state?(response) && inactive_correlation?(response, verification_id, event_id)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def inactive_state?(response)
|
|
142
|
+
response["schema_version"] == SCHEMA_VERSION && response["success"] == false &&
|
|
143
|
+
response["status"] == "project_inactive" && response["credentials_valid"] == true &&
|
|
144
|
+
response["event_received"] == false && hash_value(response["project"])["status"] == "inactive" &&
|
|
145
|
+
valid_error?(response["error"])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def inactive_correlation?(response, verification_id, event_id)
|
|
149
|
+
response["verification_id"] == verification_id && hash_value(response["event"])["id"] == event_id &&
|
|
150
|
+
hash_value(response["project"])["id"] == @config.project_id.to_s
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def valid_error?(error)
|
|
154
|
+
exact_keys?(error, %w(code message guidance)) && error["code"] == "project_inactive"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def project_inactive(response, verification_id, event_id)
|
|
158
|
+
failure("project_inactive", verification_id, event_id,
|
|
159
|
+
:credentials_valid => true, :receiver_status => "reachable",
|
|
160
|
+
:message => "The Chronos project is inactive and did not accept the verification event.",
|
|
161
|
+
:guidance => "Activate the project in Chronos or select an active project, then retry.",
|
|
162
|
+
:project => safe_project(response["project"]))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def invalid_credentials(verification_id, event_id)
|
|
166
|
+
failure("invalid_credentials", verification_id, event_id,
|
|
167
|
+
:credentials_valid => false, :receiver_status => "reachable",
|
|
168
|
+
:message => "Chronos rejected the project credentials.",
|
|
169
|
+
:guidance => "Create an active project API key, then confirm project_id and project_key.")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def rate_limited(verification_id, event_id)
|
|
173
|
+
failure("rate_limited", verification_id, event_id,
|
|
174
|
+
:receiver_status => "reachable",
|
|
175
|
+
:message => "Chronos temporarily rate limited the verification request.",
|
|
176
|
+
:guidance => "Wait before retrying and review the project ingestion limits.")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def receiver_internal_error(verification_id, event_id)
|
|
180
|
+
failure("receiver_internal_error", verification_id, event_id,
|
|
181
|
+
:receiver_status => "error",
|
|
182
|
+
:message => "Chronos encountered an internal error while processing the verification.",
|
|
183
|
+
:guidance => "Retry later or contact the Chronos operator with the verification_id.")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def receiver_unavailable(verification_id, event_id)
|
|
187
|
+
failure("receiver_unavailable", verification_id, event_id,
|
|
188
|
+
:receiver_status => "unavailable",
|
|
189
|
+
:message => "The Chronos receiver is unavailable or could not be reached.",
|
|
190
|
+
:guidance => "Check the host, DNS, TLS, network access, and Chronos service status.")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def request_rejected(verification_id, event_id)
|
|
194
|
+
failure("request_rejected", verification_id, event_id,
|
|
195
|
+
:receiver_status => "reachable",
|
|
196
|
+
:message => "Chronos rejected the integration verification request.",
|
|
197
|
+
:guidance => "Confirm that the gem and Chronos support the verification v1 contract.")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def unavailable?(result)
|
|
201
|
+
[:network_error, :request_timeout, :circuit_open].include?(result.status) ||
|
|
202
|
+
[502, 503, 504].include?(result.status_code)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def failure(status, verification_id, event_id, options = {})
|
|
206
|
+
Core::IntegrationVerificationResult.new(
|
|
207
|
+
:success => false,
|
|
208
|
+
:status => status,
|
|
209
|
+
:verification_id => verification_id,
|
|
210
|
+
:credentials_valid => options[:credentials_valid],
|
|
211
|
+
:event => {"id" => event_id, "received" => false},
|
|
212
|
+
:project => options[:project],
|
|
213
|
+
:receiver => {"name" => "chronos", "status" => options[:receiver_status], "received_at" => nil},
|
|
214
|
+
:error => {"code" => status, "message" => options[:message], "guidance" => options[:guidance]}
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def local_failure(status, verification_id, guidance)
|
|
219
|
+
failure(status, verification_id, nil,
|
|
220
|
+
:receiver_status => "not_checked",
|
|
221
|
+
:message => "Chronos integration verification could not be started.",
|
|
222
|
+
:guidance => guidance)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def configuration_guidance
|
|
226
|
+
"Configure a non-empty project_id, project_key, and HTTPS host before running verification."
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def safe_project(value)
|
|
230
|
+
safe_fields(value, "id" => 128, "name" => 128, "status" => 32, "environment" => 128)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def safe_receiver(value)
|
|
234
|
+
safe_fields(value, "name" => 64, "status" => 32, "received_at" => 64)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def safe_fields(value, fields)
|
|
238
|
+
source = hash_value(value)
|
|
239
|
+
fields.each_with_object({}) do |(name, limit), result|
|
|
240
|
+
result[name] = source[name].nil? ? nil : bounded(source[name], limit)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def hash_value(value)
|
|
245
|
+
value.is_a?(Hash) ? value : {}
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def exact_keys?(value, keys)
|
|
249
|
+
value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def bounded(value, limit)
|
|
253
|
+
text = value.to_s.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "?")
|
|
254
|
+
return text if text.bytesize <= limit
|
|
255
|
+
|
|
256
|
+
text.byteslice(0, limit).to_s.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "?")
|
|
257
|
+
rescue StandardError
|
|
258
|
+
""
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Core
|
|
5
|
+
# Immutable result returned by an explicit Chronos integration verification.
|
|
6
|
+
#
|
|
7
|
+
# @responsibility Expose a bounded, JSON-safe verification outcome to Ruby and Rake callers.
|
|
8
|
+
# @motivation Report credential, receiver, and acknowledgement state without exposing raw responses.
|
|
9
|
+
# @limits It never includes project keys, response bodies, stack traces, or receiver internals.
|
|
10
|
+
# @collaborators VerifyIntegration and Chronos::RakeTasks.
|
|
11
|
+
# @thread_safety The object and all nested values are immutable after construction.
|
|
12
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
|
|
13
|
+
# @example
|
|
14
|
+
# result = Chronos.verify_integration
|
|
15
|
+
# puts result.to_json
|
|
16
|
+
# @errors Invalid input is normalized to safe empty values.
|
|
17
|
+
# @performance Construction visits only the small allowlisted verification response.
|
|
18
|
+
class IntegrationVerificationResult
|
|
19
|
+
SCHEMA_VERSION = "1.0".freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :status, :verification_id, :credentials_valid, :event,
|
|
22
|
+
:project, :receiver, :error
|
|
23
|
+
|
|
24
|
+
def initialize(attributes = {})
|
|
25
|
+
@success = attributes[:success] == true
|
|
26
|
+
@status = safe_string(attributes[:status], 64)
|
|
27
|
+
@verification_id = optional_string(attributes[:verification_id], 128)
|
|
28
|
+
@credentials_valid = boolean_or_nil(attributes[:credentials_valid])
|
|
29
|
+
@event = immutable_copy(hash_value(attributes[:event]))
|
|
30
|
+
@project = optional_hash(attributes[:project])
|
|
31
|
+
@receiver = optional_hash(attributes[:receiver])
|
|
32
|
+
@error = optional_hash(attributes[:error])
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def success?
|
|
37
|
+
@success
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
"schema_version" => SCHEMA_VERSION,
|
|
43
|
+
"success" => success?,
|
|
44
|
+
"status" => status,
|
|
45
|
+
"verification_id" => verification_id,
|
|
46
|
+
"credentials_valid" => credentials_valid,
|
|
47
|
+
"event_received" => event["received"] == true,
|
|
48
|
+
"event" => event,
|
|
49
|
+
"project" => project,
|
|
50
|
+
"receiver" => receiver,
|
|
51
|
+
"error" => error
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_json(*arguments)
|
|
56
|
+
JSON.generate(to_h, *arguments)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def optional_hash(value)
|
|
62
|
+
value.is_a?(Hash) ? immutable_copy(value) : nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def hash_value(value)
|
|
66
|
+
value.is_a?(Hash) ? value : {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def boolean_or_nil(value)
|
|
70
|
+
[true, false].include?(value) ? value : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def optional_string(value, limit)
|
|
74
|
+
return nil if value.nil?
|
|
75
|
+
|
|
76
|
+
safe_string(value, limit)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def safe_string(value, limit)
|
|
80
|
+
text = value.is_a?(String) || value.is_a?(Symbol) ? value.to_s : ""
|
|
81
|
+
text = text.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "?")
|
|
82
|
+
return text if text.bytesize <= limit
|
|
83
|
+
|
|
84
|
+
text.byteslice(0, limit).to_s.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "?")
|
|
85
|
+
rescue StandardError
|
|
86
|
+
""
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def immutable_copy(value)
|
|
90
|
+
copy = case value
|
|
91
|
+
when Hash
|
|
92
|
+
value.each_with_object({}) do |(key, child), result|
|
|
93
|
+
result[safe_string(key, 64)] = immutable_copy(child)
|
|
94
|
+
end
|
|
95
|
+
when Array
|
|
96
|
+
value.first(20).map { |child| immutable_copy(child) }
|
|
97
|
+
when String
|
|
98
|
+
safe_string(value, 512)
|
|
99
|
+
when NilClass, TrueClass, FalseClass, Numeric
|
|
100
|
+
value
|
|
101
|
+
else
|
|
102
|
+
""
|
|
103
|
+
end
|
|
104
|
+
copy.freeze
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/chronos/errors.rb
CHANGED
|
@@ -21,4 +21,15 @@ module Chronos
|
|
|
21
21
|
# @example
|
|
22
22
|
# Chronos.configure { |config| config.project_key = nil }
|
|
23
23
|
class ConfigurationError < Error; end
|
|
24
|
+
|
|
25
|
+
# Synthetic exception sent only by an explicit integration verification.
|
|
26
|
+
#
|
|
27
|
+
# @responsibility Give verification notices a stable, recognizable exception class.
|
|
28
|
+
# @motivation Let the Chronos receiver distinguish an integration check from an application failure.
|
|
29
|
+
# @limits It is never raised into the host application and carries no application data.
|
|
30
|
+
# @thread_safety A fresh instance is created for each verification.
|
|
31
|
+
# @compatibility Ruby 2.2.10 and newer legacy runtimes.
|
|
32
|
+
# @example
|
|
33
|
+
# Chronos.verify_integration
|
|
34
|
+
class IntegrationVerificationError < Error; end
|
|
24
35
|
end
|
|
@@ -8,7 +8,7 @@ module Chronos
|
|
|
8
8
|
# @thread_safety Immutable after construction.
|
|
9
9
|
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
10
10
|
class TransportResult
|
|
11
|
-
attr_reader :status, :status_code, :retry_after, :error, :remote_configuration
|
|
11
|
+
attr_reader :status, :status_code, :retry_after, :error, :remote_configuration, :response
|
|
12
12
|
|
|
13
13
|
def initialize(status, options = {})
|
|
14
14
|
@status = status
|
|
@@ -16,6 +16,7 @@ module Chronos
|
|
|
16
16
|
@retry_after = options[:retry_after]
|
|
17
17
|
@error = options[:error]
|
|
18
18
|
@remote_configuration = options[:remote_configuration]
|
|
19
|
+
@response = deep_freeze(options[:response])
|
|
19
20
|
freeze
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -26,6 +27,22 @@ module Chronos
|
|
|
26
27
|
def retryable?
|
|
27
28
|
[:request_timeout, :rate_limited, :server_error, :network_error].include?(status)
|
|
28
29
|
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def deep_freeze(value)
|
|
34
|
+
if value.is_a?(Hash)
|
|
35
|
+
value.each do |key, child|
|
|
36
|
+
deep_freeze(key)
|
|
37
|
+
deep_freeze(child)
|
|
38
|
+
end
|
|
39
|
+
elsif value.is_a?(Array)
|
|
40
|
+
value.each { |child| deep_freeze(child) }
|
|
41
|
+
end
|
|
42
|
+
value.freeze
|
|
43
|
+
rescue StandardError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
29
46
|
end
|
|
30
47
|
|
|
31
48
|
# Conceptual transport port implemented by delivery adapters.
|
|
@@ -16,6 +16,11 @@ module Chronos
|
|
|
16
16
|
initializer "chronos.install", :after => :load_config_initializers do |application|
|
|
17
17
|
Chronos::Rails::Installer.new.install(application)
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
rake_tasks do
|
|
21
|
+
require "chronos/rake_tasks"
|
|
22
|
+
Chronos::RakeTasks.install(:load_environment => true)
|
|
23
|
+
end
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
26
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "rake"
|
|
3
|
+
require "chronos"
|
|
4
|
+
|
|
5
|
+
module Chronos
|
|
6
|
+
# Installs explicit Rake commands supplied by the Chronos gem.
|
|
7
|
+
#
|
|
8
|
+
# @responsibility Register the integration verification task with deterministic JSON output.
|
|
9
|
+
# @motivation Give Rails and plain Ruby operators one repeatable end-to-end credential check.
|
|
10
|
+
# @limits It never reads environment variables, prints credentials, or modifies application data.
|
|
11
|
+
# @collaborators Rake and Chronos.verify_integration.
|
|
12
|
+
# @thread_safety Installation checks the process-global Rake registry and is intended for boot time.
|
|
13
|
+
# @compatibility Rake versions compatible with Ruby 2.2.10 through Ruby 2.6.
|
|
14
|
+
# @example
|
|
15
|
+
# require "chronos/rake_tasks"
|
|
16
|
+
# Chronos::RakeTasks.install
|
|
17
|
+
# @errors Verification failures produce JSON and a nonzero process status.
|
|
18
|
+
# @performance The task performs one explicit synchronous verification when invoked.
|
|
19
|
+
module RakeTasks
|
|
20
|
+
extend ::Rake::DSL
|
|
21
|
+
|
|
22
|
+
TASK_NAME = "chronos:verify_integration".freeze
|
|
23
|
+
|
|
24
|
+
def self.install(options = {})
|
|
25
|
+
return false if ::Rake::Task.task_defined?(TASK_NAME)
|
|
26
|
+
|
|
27
|
+
output = options[:output] || $stdout
|
|
28
|
+
exiter = options[:exit] || proc { |status| exit(status) }
|
|
29
|
+
load_environment = options[:load_environment] || ::Rake::Task.task_defined?("environment")
|
|
30
|
+
prerequisites = load_environment ? ["environment"] : []
|
|
31
|
+
|
|
32
|
+
desc "Send an identified fake error and verify Chronos credentials and ingestion"
|
|
33
|
+
task TASK_NAME => prerequisites do
|
|
34
|
+
result = Chronos.verify_integration
|
|
35
|
+
output.puts(JSON.generate(result.to_h))
|
|
36
|
+
exiter.call(1) unless result.success?
|
|
37
|
+
end
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/chronos/version.rb
CHANGED
data/lib/chronos.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "chronos/ports"
|
|
|
7
7
|
require "chronos/adapters"
|
|
8
8
|
require "chronos/internal"
|
|
9
9
|
require "chronos/core/notice"
|
|
10
|
+
require "chronos/core/integration_verification_result"
|
|
10
11
|
require "chronos/core/backtrace_parser"
|
|
11
12
|
require "chronos/core/exception_cause_collector"
|
|
12
13
|
require "chronos/core/runtime_info"
|
|
@@ -36,6 +37,7 @@ require "chronos/application/remote_configuration"
|
|
|
36
37
|
require "chronos/application/ignore_policy"
|
|
37
38
|
require "chronos/application/delivery_pipeline"
|
|
38
39
|
require "chronos/application/capture_exception"
|
|
40
|
+
require "chronos/application/verify_integration"
|
|
39
41
|
require "chronos/application/apm_error_classifier"
|
|
40
42
|
require "chronos/application/apm_aggregator"
|
|
41
43
|
require "chronos/application/dependency_reporter"
|
|
@@ -95,6 +97,21 @@ module Chronos # rubocop:disable Metrics/ModuleLength
|
|
|
95
97
|
false
|
|
96
98
|
end
|
|
97
99
|
|
|
100
|
+
def verify_integration
|
|
101
|
+
agent = current_agent
|
|
102
|
+
return agent.verify_integration if agent
|
|
103
|
+
|
|
104
|
+
verification_failure(
|
|
105
|
+
"not_configured", "Chronos is not configured.",
|
|
106
|
+
"Run Chronos.configure before verifying the integration."
|
|
107
|
+
)
|
|
108
|
+
rescue StandardError
|
|
109
|
+
verification_failure(
|
|
110
|
+
"verification_failed", "Chronos integration verification failed locally.",
|
|
111
|
+
"Review the Chronos configuration and retry."
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
98
115
|
def with_context(context = {})
|
|
99
116
|
agent = current_agent
|
|
100
117
|
return yield unless agent
|
|
@@ -186,6 +203,15 @@ module Chronos # rubocop:disable Metrics/ModuleLength
|
|
|
186
203
|
def current_agent
|
|
187
204
|
@mutex.synchronize { @agent }
|
|
188
205
|
end
|
|
206
|
+
|
|
207
|
+
def verification_failure(status, message, guidance)
|
|
208
|
+
Core::IntegrationVerificationResult.new(
|
|
209
|
+
:success => false, :status => status, :credentials_valid => nil,
|
|
210
|
+
:event => {"id" => nil, "received" => false},
|
|
211
|
+
:receiver => {"name" => "chronos", "status" => "not_checked", "received_at" => nil},
|
|
212
|
+
:error => {"code" => status, "message" => message, "guidance" => guidance}
|
|
213
|
+
)
|
|
214
|
+
end
|
|
189
215
|
end
|
|
190
216
|
end
|
|
191
217
|
|
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.9.0.pre.
|
|
4
|
+
version: 0.9.0.pre.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antonio Jefferson
|
|
@@ -72,6 +72,20 @@ dependencies:
|
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
74
|
version: 0.49.0
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: parallel
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - '='
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 1.19.2
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - '='
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 1.19.2
|
|
75
89
|
description: Base do cliente Chronos para excecoes, telemetria e observabilidade em
|
|
76
90
|
aplicacoes Ruby legadas.
|
|
77
91
|
email:
|
|
@@ -90,6 +104,7 @@ files:
|
|
|
90
104
|
- contracts/dependencies-v1.schema.json
|
|
91
105
|
- contracts/deploy-v1.schema.json
|
|
92
106
|
- contracts/event-v1.schema.json
|
|
107
|
+
- contracts/integration-verification-response-v1.schema.json
|
|
93
108
|
- contracts/rack-context-v1.schema.json
|
|
94
109
|
- contracts/sidekiq-job-v1.schema.json
|
|
95
110
|
- docs/adr/ADR-001-core-and-integrations.md
|
|
@@ -122,6 +137,7 @@ files:
|
|
|
122
137
|
- docs/modules/deploy-tracking.md
|
|
123
138
|
- docs/modules/external-http.md
|
|
124
139
|
- docs/modules/ignore-rules.md
|
|
140
|
+
- docs/modules/integration-verification.md
|
|
125
141
|
- docs/modules/notice-pipeline.md
|
|
126
142
|
- docs/modules/rack-context.md
|
|
127
143
|
- docs/modules/rails-legacy.md
|
|
@@ -155,6 +171,7 @@ files:
|
|
|
155
171
|
- lib/chronos/application/ignore_policy.rb
|
|
156
172
|
- lib/chronos/application/remote_configuration.rb
|
|
157
173
|
- lib/chronos/application/retry_policy.rb
|
|
174
|
+
- lib/chronos/application/verify_integration.rb
|
|
158
175
|
- lib/chronos/capistrano.rb
|
|
159
176
|
- lib/chronos/configuration.rb
|
|
160
177
|
- lib/chronos/configuration/apm_validation.rb
|
|
@@ -167,6 +184,7 @@ files:
|
|
|
167
184
|
- lib/chronos/core/correlation_context.rb
|
|
168
185
|
- lib/chronos/core/deploy_normalizer.rb
|
|
169
186
|
- lib/chronos/core/exception_cause_collector.rb
|
|
187
|
+
- lib/chronos/core/integration_verification_result.rb
|
|
170
188
|
- lib/chronos/core/metric_aggregate.rb
|
|
171
189
|
- lib/chronos/core/notice.rb
|
|
172
190
|
- lib/chronos/core/notice_builder.rb
|
|
@@ -200,6 +218,7 @@ files:
|
|
|
200
218
|
- lib/chronos/rails/installer.rb
|
|
201
219
|
- lib/chronos/rails/notifications_subscriber.rb
|
|
202
220
|
- lib/chronos/rails/railtie.rb
|
|
221
|
+
- lib/chronos/rake_tasks.rb
|
|
203
222
|
- lib/chronos/ruby.rb
|
|
204
223
|
- lib/chronos/ruby/version.rb
|
|
205
224
|
- lib/chronos/sidekiq.rb
|