chronos-ruby 0.7.0.pre.1 → 0.9.0.pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +55 -9
- data/contracts/apm-batch-v1.schema.json +1 -1
- data/contracts/dependencies-v1.schema.json +38 -0
- data/contracts/deploy-v1.schema.json +26 -0
- data/contracts/event-v1.schema.json +15 -1
- data/docs/adr/ADR-016-explicit-observability-integrations.md +25 -0
- data/docs/adr/ADR-017-deploy-events-and-correlation.md +25 -0
- data/docs/architecture.md +12 -1
- data/docs/compatibility.md +4 -0
- data/docs/configuration.md +24 -3
- data/docs/data-collected.md +11 -4
- data/docs/examples/plain-ruby.md +13 -1
- data/docs/modules/apm-aggregation.md +2 -2
- data/docs/modules/cache-observability.md +16 -0
- data/docs/modules/configuration.md +4 -0
- data/docs/modules/dependencies.md +18 -0
- data/docs/modules/deploy-tracking.md +86 -0
- data/docs/modules/external-http.md +27 -0
- data/docs/modules/rails-legacy.md +1 -1
- data/docs/modules/sidekiq-legacy.md +1 -1
- data/docs/modules/telemetry-events.md +4 -2
- data/docs/performance.md +30 -1
- data/docs/privacy-lgpd.md +20 -2
- data/docs/troubleshooting.md +25 -1
- data/lib/chronos/agent.rb +43 -0
- data/lib/chronos/application/apm_aggregator.rb +7 -10
- data/lib/chronos/application/apm_error_classifier.rb +27 -0
- data/lib/chronos/application/capture_telemetry.rb +18 -1
- data/lib/chronos/application/dependency_reporter.rb +140 -0
- data/lib/chronos/application/remote_configuration.rb +3 -1
- data/lib/chronos/capistrano.rb +4 -0
- data/lib/chronos/configuration/apm_validation.rb +28 -1
- data/lib/chronos/configuration.rb +25 -3
- data/lib/chronos/core/cache_normalizer.rb +99 -0
- 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/capistrano.rb +61 -0
- data/lib/chronos/integrations/net_http.rb +165 -0
- data/lib/chronos/net_http.rb +4 -0
- data/lib/chronos/observability_facade.rb +50 -0
- data/lib/chronos/rails/notifications_subscriber.rb +6 -5
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +8 -0
- metadata +19 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Deploy tracking and release correlation
|
|
2
|
+
|
|
3
|
+
Version `0.9.0.pre.1` adds synchronous deploy notifications and a bounded correlation block to every exception and telemetry envelope. The SaaS can compare errors and performance before and after a release using release, revision, deploy ID, environment, service, region, and instance.
|
|
4
|
+
|
|
5
|
+
## Public API
|
|
6
|
+
|
|
7
|
+
Configure the agent, then provide deploy metadata explicitly:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
Chronos.notify_deploy(
|
|
11
|
+
:environment => "production",
|
|
12
|
+
:revision => ENV["GIT_SHA"],
|
|
13
|
+
:version => ENV["APP_VERSION"],
|
|
14
|
+
:repository => "owner/repository",
|
|
15
|
+
:actor => ENV["DEPLOY_USER"],
|
|
16
|
+
:deploy_id => ENV["DEPLOY_ID"],
|
|
17
|
+
:service => "billing",
|
|
18
|
+
:region => "sa-east-1",
|
|
19
|
+
:instance => "web-1"
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Environment is required, and either revision or version must be present. A UUID deploy ID is generated when none is supplied. The call uses synchronous delivery, bypasses ordinary event sampling, then refreshes the bounded dependency inventory for the new release and flushes it before returning. Local/remote event disabling and the kill switch still apply. It returns `false` instead of leaking configuration, normalization, serialization, or transport failures.
|
|
24
|
+
|
|
25
|
+
Fields are limited to 128 bytes, except repository at 512 bytes. HTTP and SCP-style repository credentials are stripped before serialization. The shared sanitizer still filters explicit actor/repository content. Do not send access tokens, e-mail addresses, or unnecessary personal data.
|
|
26
|
+
|
|
27
|
+
## Correlation on application events
|
|
28
|
+
|
|
29
|
+
Set the values that identify the currently running release during normal application configuration:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
Chronos.configure do |config|
|
|
33
|
+
# project and endpoint settings omitted
|
|
34
|
+
config.app_version = ENV["APP_VERSION"]
|
|
35
|
+
config.revision = ENV["GIT_SHA"]
|
|
36
|
+
config.deploy_id = ENV["DEPLOY_ID"]
|
|
37
|
+
config.environment = ENV["APP_ENV"] || "production"
|
|
38
|
+
config.service_name = "billing"
|
|
39
|
+
config.region = ENV["REGION"]
|
|
40
|
+
config.instance_id = ENV["INSTANCE_ID"]
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The gem never reads these variables itself. The host application chooses each source. Configuration snapshots are immutable, so `notify_deploy` does not mutate correlation for an already running process; newly deployed processes must start with their own release values.
|
|
45
|
+
|
|
46
|
+
## Capistrano
|
|
47
|
+
|
|
48
|
+
Require the optional entry point after Capistrano loads:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require "chronos/capistrano"
|
|
52
|
+
|
|
53
|
+
set :chronos_version, ENV["APP_VERSION"]
|
|
54
|
+
set :chronos_actor, ENV["DEPLOY_USER"]
|
|
55
|
+
set :chronos_deploy_id, ENV["DEPLOY_ID"]
|
|
56
|
+
set :chronos_service, "billing"
|
|
57
|
+
set :chronos_region, ENV["DEPLOY_REGION"]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
It registers `chronos:notify_deploy` after `deploy:published`, once per DSL object. Revision, stage, and repository use the public Capistrano variables `current_revision`, `stage`, and `repo_url`; all Chronos-specific values are optional and explicit. The integration adds no Capistrano runtime dependency.
|
|
61
|
+
|
|
62
|
+
## Manual, Kamal, and GitHub Actions
|
|
63
|
+
|
|
64
|
+
[`examples/deploy/notify.rb`](../../examples/deploy/notify.rb) is the shared command. It requires project credentials/host and deploy environment, reads only named variables, notifies synchronously, closes the agent, and exits nonzero on failure.
|
|
65
|
+
|
|
66
|
+
For Kamal, pass the variables to the deployed container or command environment and execute after publication:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
kamal app exec --reuse "bundle exec ruby examples/deploy/notify.rb"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Adapt the command to the application's Kamal hook lifecycle and secret-management policy. The repository also provides a non-active [GitHub Actions workflow example](../../examples/deploy/github-actions.yml) using read-only repository permission and secret-scoped Chronos credentials.
|
|
73
|
+
|
|
74
|
+
For a network-free payload demonstration:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bundle _1.17.3_ exec ruby examples/plain-ruby/deploy_tracking.rb
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Limitations
|
|
81
|
+
|
|
82
|
+
- delivery is process-local and uses the existing finite retry/circuit/backlog policy;
|
|
83
|
+
- an unsuccessful synchronous delivery returns `false`; deployment policy decides whether that blocks publication;
|
|
84
|
+
- Capistrano auto-registration requires its task DSL to be loaded first;
|
|
85
|
+
- Kamal integration is command/documentation based, not a Kamal plugin;
|
|
86
|
+
- repository and actor are explicit metadata and remain subject to application privacy review.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# External HTTP instrumentation
|
|
2
|
+
|
|
3
|
+
Version `0.8.0.pre.1` provides an optional `Net::HTTP` wrapper loaded through `chronos/net_http`. It is disabled by default and prepended only to a connection object explicitly passed to `Chronos.instrument_net_http`; the `Net::HTTP` class and unrelated instances are unchanged.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require "net/http"
|
|
7
|
+
require "chronos"
|
|
8
|
+
|
|
9
|
+
Chronos.configure do |config|
|
|
10
|
+
# connection settings omitted
|
|
11
|
+
config.external_http_enabled = true
|
|
12
|
+
config.external_http_trace_headers = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
http = Net::HTTP.new("payments.example.com", 443)
|
|
16
|
+
http.use_ssl = true
|
|
17
|
+
Chronos.instrument_net_http(http)
|
|
18
|
+
response = http.request(Net::HTTP::Get.new("/health"))
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The event contains a bounded lowercase host, uppercase method, response status, monotonic duration, timeout flag, connection-error flag, and error class. A request made inside a Chronos context receives `X-Chronos-Trace-ID` and `X-Chronos-Request-ID` unless the application already set those headers. Disable propagation with `external_http_trace_headers = false`.
|
|
22
|
+
|
|
23
|
+
The wrapper never reads or records the path, query string, Authorization, other request headers, request body, response headers/body, or exception message. The native streaming block is forwarded and the identical HTTP exception is re-raised. Telemetry failures are contained.
|
|
24
|
+
|
|
25
|
+
Successful and failed calls become bounded `external_http` APM groups keyed only by host and method. A call carrying a trace ID contributes its duration to the enclosing request's `external_http` breakdown. Faraday, HTTP.rb, Excon, and RestClient are outside this release.
|
|
26
|
+
|
|
27
|
+
Installation is idempotent per object. A `false` result means collection is disabled, the object is incompatible or already instrumented, or installation was contained after an internal error.
|
|
@@ -29,7 +29,7 @@ The public `ActiveSupport::Notifications.subscribe` API is used for:
|
|
|
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
31
|
| `perform.active_job` | `job` | job class, queue, and duration, when Active Job is available |
|
|
32
|
-
| cache read/write/hit notifications | `cache` | operation,
|
|
32
|
+
| cache read/write/hit notifications | `cache` | operation, backend, namespace, hit/miss, duration, and optional scoped key hash |
|
|
33
33
|
|
|
34
34
|
Raw SQL, binds, cache keys, mail recipients and bodies, job IDs and arguments, request/response bodies, cookies, and authorization headers are not copied. 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
|
|
|
@@ -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.1", :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`, and `
|
|
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
|
-
Version 0.
|
|
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
|
|
|
@@ -21,6 +21,11 @@ Current controls:
|
|
|
21
21
|
- Rails subscribers copy only small allowlisted field sets and never copy raw SQL or job arguments.
|
|
22
22
|
- Sidekiq middleware creates no per-job thread or connection and bounds arguments, collections, nesting, strings, and tags before telemetry capture.
|
|
23
23
|
- APM group, trace, query-fingerprint, histogram, and batch counts are fixed; no APM timer thread is created.
|
|
24
|
+
- outbound HTTP instrumentation uses two clock reads and bounded metadata without body/header traversal;
|
|
25
|
+
- cache normalization is bounded and SHA-256 runs only when explicitly enabled;
|
|
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.
|
|
24
29
|
|
|
25
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.
|
|
26
31
|
|
|
@@ -100,3 +105,27 @@ ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/apm_aggregation.rb
|
|
|
100
105
|
The fixture repeatedly updates one SQL metric group and one trace-local fingerprint. It excludes ActiveSupport dispatch, serialization, queueing, and network delivery. The benchmark must report one retained group; memory must remain bounded by configuration. Record runtime, hardware, warmup, median, and dispersion before publishing a performance claim.
|
|
101
106
|
|
|
102
107
|
A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 and 10,000 observations. It retained one group and measured approximately 34.972 microseconds per observation. This single run has no controlled warmup, median, or dispersion and is not a production performance claim.
|
|
108
|
+
|
|
109
|
+
## Version 0.8 external HTTP benchmark
|
|
110
|
+
|
|
111
|
+
Run:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/external_http.rb
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
## Version 0.9 correlation benchmark
|
|
122
|
+
|
|
123
|
+
Run:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/correlation.rb
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
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
|
|
|
@@ -16,8 +16,26 @@ Version 0.7 sanitizes exception, framework telemetry, and APM metric batches bef
|
|
|
16
16
|
| Request/response bodies, raw query strings, cookies, authorization headers, raw SQL/binds, cache values, mail bodies, environment variables | Never collected automatically |
|
|
17
17
|
| Sidekiq arguments | Collected automatically, limited before sanitization, then redacted by the common policy |
|
|
18
18
|
| SQL | Comments, quoted/numeric/boolean/null literals removed; binds never read; bounded identifiers remain |
|
|
19
|
+
| External HTTP | Host/method/status/timing only; URL path/query, Authorization, bodies, headers, and error messages omitted |
|
|
20
|
+
| Cache key | Omitted by default; optional project-scoped SHA-256 hash; cache value never read |
|
|
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 |
|
|
19
25
|
|
|
20
|
-
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.
|
|
33
|
+
|
|
34
|
+
## External services, cache, and dependencies
|
|
35
|
+
|
|
36
|
+
Outbound HTTP instrumentation is disabled by default and installed per connection. Trace headers reveal correlation identifiers to the selected destination; enable propagation only for destinations permitted to receive them. The wrapper does not inspect any other header or URL component.
|
|
37
|
+
|
|
38
|
+
Raw cache keys can contain user or business data and are never delivered. Optional hashing is pseudonymization, not anonymization: predictable low-entropy keys can still be guessed. Dependency reporting is a separate once-per-agent event, never copied into exceptions, and can be disabled. It reads loaded gem name/version pairs only, with no filesystem path, source, configuration, or complete lockfile.
|
|
21
39
|
|
|
22
40
|
## Rails telemetry
|
|
23
41
|
|
data/docs/troubleshooting.md
CHANGED
|
@@ -22,7 +22,31 @@ Require `chronos/rails` from the generated initializer and confirm it runs befor
|
|
|
22
22
|
|
|
23
23
|
## Rails telemetry contains no SQL or cache key
|
|
24
24
|
|
|
25
|
-
This is intentional.
|
|
25
|
+
This is intentional. Chronos records bounded SQL and cache operational metadata without raw statements, binds, keys, or values. Set `cache_key_mode = :sha256` before configuration only when a stable pseudonymous key identity is required.
|
|
26
|
+
|
|
27
|
+
## External HTTP telemetry is missing
|
|
28
|
+
|
|
29
|
+
Set `external_http_enabled = true`, then call `Chronos.instrument_net_http(http)` for each `Net::HTTP` instance after Chronos is configured. Existing and future objects are not patched globally. The method returns `false` when disabled, already installed, incompatible, or unconfigured.
|
|
30
|
+
|
|
31
|
+
## Trace headers should not reach a destination
|
|
32
|
+
|
|
33
|
+
Set `external_http_trace_headers = false` before configuring the agent, or do not instrument that connection. Chronos preserves an application-supplied Chronos header instead of overwriting it.
|
|
34
|
+
|
|
35
|
+
## Dependency inventory is missing or incomplete
|
|
36
|
+
|
|
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
|
+
|
|
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.
|
|
26
50
|
|
|
27
51
|
## Sidekiq telemetry is missing
|
|
28
52
|
|
data/lib/chronos/agent.rb
CHANGED
|
@@ -37,13 +37,16 @@ module Chronos
|
|
|
37
37
|
pipeline_options
|
|
38
38
|
)
|
|
39
39
|
initialize_capture(options)
|
|
40
|
+
initialize_observability(options)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def notify(exception, context = {})
|
|
44
|
+
report_dependencies
|
|
43
45
|
@capture.call(exception, context_for_capture(context))
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
def notify_sync(exception, context = {})
|
|
49
|
+
report_dependencies
|
|
47
50
|
@capture.call_sync(exception, context_for_capture(context))
|
|
48
51
|
end
|
|
49
52
|
|
|
@@ -64,9 +67,34 @@ module Chronos
|
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def record_event(event_type, payload = {}, context = {})
|
|
70
|
+
return false if event_type.to_s == "deploy"
|
|
71
|
+
|
|
72
|
+
report_dependencies unless event_type.to_s == "dependencies"
|
|
67
73
|
@telemetry.call(event_type, payload, telemetry_context(context))
|
|
68
74
|
end
|
|
69
75
|
|
|
76
|
+
def report_dependencies
|
|
77
|
+
payload = @dependency_reporter.call
|
|
78
|
+
return false unless payload
|
|
79
|
+
|
|
80
|
+
@telemetry.call("dependencies", payload, {})
|
|
81
|
+
rescue StandardError => error
|
|
82
|
+
@logger.warn("Chronos dependency reporting failed: #{error.class}")
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def notify_deploy(attributes = {}, timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
87
|
+
payload = @deploy_normalizer.call(attributes)
|
|
88
|
+
return false unless @telemetry.call_sync("deploy", payload, {})
|
|
89
|
+
|
|
90
|
+
@dependency_reporter.reset(payload["version"]) if @dependency_reporter.respond_to?(:reset)
|
|
91
|
+
report_dependencies
|
|
92
|
+
flush(timeout)
|
|
93
|
+
rescue StandardError => error
|
|
94
|
+
@logger.warn("Chronos deploy notification failed: #{error.class}")
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
70
98
|
def record_event_once(key, event_type, payload = {}, context = {})
|
|
71
99
|
execution = @context_store.get
|
|
72
100
|
captured = execution[:__chronos_captured_events] || {}
|
|
@@ -87,6 +115,14 @@ module Chronos
|
|
|
87
115
|
}
|
|
88
116
|
end
|
|
89
117
|
|
|
118
|
+
def external_http_integration_options
|
|
119
|
+
{:enabled => @config.external_http_enabled, :trace_headers => @config.external_http_trace_headers}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def cache_integration_options
|
|
123
|
+
{:project_id => @config.project_id, :key_mode => @config.cache_key_mode}
|
|
124
|
+
end
|
|
125
|
+
|
|
90
126
|
# Returns the correlation subset safe for an integration-owned process boundary.
|
|
91
127
|
def propagation_context
|
|
92
128
|
current = context_hash(@context_store.get)
|
|
@@ -124,6 +160,7 @@ module Chronos
|
|
|
124
160
|
end
|
|
125
161
|
|
|
126
162
|
def flush(timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
163
|
+
report_dependencies
|
|
127
164
|
@telemetry.flush
|
|
128
165
|
@delivery_pipeline.flush(timeout)
|
|
129
166
|
rescue StandardError => error
|
|
@@ -132,6 +169,7 @@ module Chronos
|
|
|
132
169
|
end
|
|
133
170
|
|
|
134
171
|
def close(timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
172
|
+
report_dependencies
|
|
135
173
|
@telemetry.flush
|
|
136
174
|
@delivery_pipeline.close(timeout)
|
|
137
175
|
rescue StandardError => error
|
|
@@ -152,6 +190,11 @@ module Chronos
|
|
|
152
190
|
@telemetry = options[:telemetry] || Application::CaptureTelemetry.new(@config, @delivery_pipeline, @logger)
|
|
153
191
|
end
|
|
154
192
|
|
|
193
|
+
def initialize_observability(options)
|
|
194
|
+
@dependency_reporter = options[:dependency_reporter] || Application::DependencyReporter.new(@config)
|
|
195
|
+
@deploy_normalizer = options[:deploy_normalizer] || Core::DeployNormalizer.new(@config)
|
|
196
|
+
end
|
|
197
|
+
|
|
155
198
|
def build_context_store(strategy)
|
|
156
199
|
return Adapters::ThreadLocalContextStore.new if strategy == :thread_local
|
|
157
200
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Chronos
|
|
2
2
|
module Application
|
|
3
|
-
# Aggregates bounded request, query, and
|
|
3
|
+
# Aggregates bounded request, query, job, and external HTTP observations into metric batches.
|
|
4
4
|
#
|
|
5
5
|
# @responsibility Maintain statistics, histograms, breakdown, and local query signals.
|
|
6
6
|
# @motivation Avoid one network event per observation while retaining diagnostic value.
|
|
@@ -14,7 +14,9 @@ module Chronos
|
|
|
14
14
|
# @errors Invalid observations are ignored and never escape to the application.
|
|
15
15
|
# @performance Group, transaction, query, bucket, and batch counts are strictly bounded.
|
|
16
16
|
class ApmAggregator
|
|
17
|
-
|
|
17
|
+
include ApmErrorClassifier
|
|
18
|
+
|
|
19
|
+
METRIC_TYPES = %w(request query job external_http).freeze
|
|
18
20
|
def initialize(config)
|
|
19
21
|
@config = config
|
|
20
22
|
@mutex = Mutex.new
|
|
@@ -77,7 +79,7 @@ module Chronos
|
|
|
77
79
|
@groups[key] = aggregate
|
|
78
80
|
end
|
|
79
81
|
duration = non_negative(payload["duration_ms"] || payload[:duration_ms])
|
|
80
|
-
status =
|
|
82
|
+
status = ["request", "external_http"].include?(type) ? payload["status"] || payload[:status] : nil
|
|
81
83
|
signals = signals_for(type, payload, context, duration)
|
|
82
84
|
breakdown = breakdown_for(type, payload, context, duration)
|
|
83
85
|
aggregate.observe(
|
|
@@ -177,6 +179,7 @@ module Chronos
|
|
|
177
179
|
when "request" then %w(route method)
|
|
178
180
|
when "query" then %w(adapter operation table fingerprint normalized_query name cached role shard source)
|
|
179
181
|
when "job" then %w(kind class queue status)
|
|
182
|
+
when "external_http" then %w(host method)
|
|
180
183
|
else []
|
|
181
184
|
end
|
|
182
185
|
names.each_with_object({}) do |name, result|
|
|
@@ -211,17 +214,11 @@ module Chronos
|
|
|
211
214
|
return "view" if type == "request" && payload["kind"].to_s == "view"
|
|
212
215
|
return "cache" if type == "cache"
|
|
213
216
|
return "queue" if type == "job"
|
|
217
|
+
return "external_http" if type == "external_http"
|
|
214
218
|
|
|
215
219
|
nil
|
|
216
220
|
end
|
|
217
221
|
|
|
218
|
-
def error?(type, payload)
|
|
219
|
-
return (payload["status"] || payload[:status]).to_i >= 500 if type == "request"
|
|
220
|
-
return (payload["status"] || payload[:status]).to_s == "failed" if type == "job"
|
|
221
|
-
|
|
222
|
-
!(payload["error_class"] || payload[:error_class]).to_s.empty?
|
|
223
|
-
end
|
|
224
|
-
|
|
225
222
|
def trace_id(context)
|
|
226
223
|
(context["trace_id"] || context[:trace_id]).to_s
|
|
227
224
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Application
|
|
3
|
+
# Classifies bounded APM observations without inspecting exception messages.
|
|
4
|
+
module ApmErrorClassifier
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def error?(type, payload)
|
|
8
|
+
return status_error?(payload) if type == "request"
|
|
9
|
+
return (payload["status"] || payload[:status]).to_s == "failed" if type == "job"
|
|
10
|
+
return external_http_error?(payload) if type == "external_http"
|
|
11
|
+
|
|
12
|
+
!(payload["error_class"] || payload[:error_class]).to_s.empty?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def external_http_error?(payload)
|
|
16
|
+
return true unless (payload["error_class"] || payload[:error_class]).to_s.empty?
|
|
17
|
+
return true if payload["timeout"] == true || payload[:timeout] == true
|
|
18
|
+
|
|
19
|
+
status_error?(payload)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def status_error?(payload)
|
|
23
|
+
(payload["status"] || payload[:status]).to_i >= 500
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -4,7 +4,7 @@ module Chronos
|
|
|
4
4
|
#
|
|
5
5
|
# @responsibility Aggregate APM observations or serialize and enqueue integration events.
|
|
6
6
|
# @motivation Keep framework policy and local batching outside transport and domain objects.
|
|
7
|
-
# @limits It handles
|
|
7
|
+
# @limits It handles only the event types declared by TelemetryEvent.
|
|
8
8
|
# @collaborators ApmAggregator, TelemetryEvent, TelemetrySerializer, and DeliveryPipeline.
|
|
9
9
|
# @thread_safety Calls own event state and may execute concurrently.
|
|
10
10
|
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
|
|
@@ -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
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Application
|
|
3
|
+
# Builds one bounded inventory from already loaded runtime components.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Report loaded gems and runtime/framework versions once per agent.
|
|
6
|
+
# @motivation Dependency context aids diagnosis without attaching the bundle to every error.
|
|
7
|
+
# @limits It does not read lockfiles, environment variables, gem paths, or open DB connections.
|
|
8
|
+
# @collaborators RubyGems loaded specs and immutable Chronos configuration.
|
|
9
|
+
# @thread_safety A mutex guarantees at-most-once collection across concurrent first events.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; all framework detection is optional.
|
|
11
|
+
# @example
|
|
12
|
+
# payload = DependencyReporter.new(config).call
|
|
13
|
+
# @errors Detection failures yield omitted fields and never escape.
|
|
14
|
+
# @performance Runs once and visits at most the configured number of loaded specs.
|
|
15
|
+
class DependencyReporter
|
|
16
|
+
def initialize(config, options = {})
|
|
17
|
+
@config = config
|
|
18
|
+
@loaded_specs = options[:loaded_specs] || proc { Gem.loaded_specs }
|
|
19
|
+
@constants = options[:constants] || {}
|
|
20
|
+
@mutex = Mutex.new
|
|
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
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
return nil if @reported || !@config.dependency_reporting
|
|
38
|
+
|
|
39
|
+
@reported = true
|
|
40
|
+
build_payload
|
|
41
|
+
end
|
|
42
|
+
rescue StandardError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_payload
|
|
49
|
+
payload = {
|
|
50
|
+
"dependencies" => dependencies,
|
|
51
|
+
"ruby" => {
|
|
52
|
+
"version" => bounded(RUBY_VERSION, 64), "engine" => bounded(ruby_engine, 64),
|
|
53
|
+
"platform" => bounded(RUBY_PLATFORM, 128)
|
|
54
|
+
},
|
|
55
|
+
"rails" => detected("rails") { rails_version },
|
|
56
|
+
"web_server" => detected("web_server") { web_server },
|
|
57
|
+
"database_adapter" => detected("database_adapter") { database_adapter },
|
|
58
|
+
"sidekiq" => detected("sidekiq") { sidekiq_version },
|
|
59
|
+
"release" => @release_override || bounded(@config.app_version.to_s, 128)
|
|
60
|
+
}
|
|
61
|
+
payload.delete_if do |key, value|
|
|
62
|
+
!["dependencies", "ruby"].include?(key) && value.to_s.empty?
|
|
63
|
+
end
|
|
64
|
+
payload
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def dependencies
|
|
68
|
+
specs = @loaded_specs.call
|
|
69
|
+
values = specs.respond_to?(:values) ? specs.values : []
|
|
70
|
+
values.first(@config.dependency_max_items).sort_by { |spec| spec.name.to_s }.map do |spec|
|
|
71
|
+
{"name" => bounded(spec.name.to_s, 128), "version" => bounded(spec.version.to_s, 64)}
|
|
72
|
+
end
|
|
73
|
+
rescue StandardError
|
|
74
|
+
[]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def detected(name)
|
|
78
|
+
explicit = @constants[name]
|
|
79
|
+
return bounded(explicit.to_s, 128) unless explicit.nil?
|
|
80
|
+
|
|
81
|
+
bounded(yield.to_s, 128)
|
|
82
|
+
rescue StandardError
|
|
83
|
+
""
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rails_version
|
|
87
|
+
defined?(::Rails) && ::Rails.respond_to?(:version) ? ::Rails.version.to_s : loaded_version("rails")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def sidekiq_version
|
|
91
|
+
defined?(::Sidekiq::VERSION) ? ::Sidekiq::VERSION.to_s : loaded_version("sidekiq")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def web_server
|
|
95
|
+
return "Puma" if defined?(::Puma)
|
|
96
|
+
return "Unicorn" if defined?(::Unicorn)
|
|
97
|
+
return "Passenger" if defined?(::PhusionPassenger)
|
|
98
|
+
return "WEBrick" if defined?(::WEBrick)
|
|
99
|
+
|
|
100
|
+
""
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def database_adapter
|
|
104
|
+
names = loaded_spec_names
|
|
105
|
+
return "PostgreSQL" if names.include?("pg")
|
|
106
|
+
return "MySQL" if names.include?("mysql2")
|
|
107
|
+
return "SQLite" if names.include?("sqlite3")
|
|
108
|
+
|
|
109
|
+
""
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def loaded_version(name)
|
|
113
|
+
specs = @loaded_specs.call
|
|
114
|
+
spec = specs[name] || specs[name.to_sym] if specs.respond_to?(:[])
|
|
115
|
+
spec ? spec.version.to_s : ""
|
|
116
|
+
rescue StandardError
|
|
117
|
+
""
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def loaded_spec_names
|
|
121
|
+
specs = @loaded_specs.call
|
|
122
|
+
specs.respond_to?(:keys) ? specs.keys.map(&:to_s) : []
|
|
123
|
+
rescue StandardError
|
|
124
|
+
[]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def ruby_engine
|
|
128
|
+
defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def bounded(value, limit)
|
|
132
|
+
text = value.to_s
|
|
133
|
+
text = text.scrub("?") if text.respond_to?(:scrub)
|
|
134
|
+
text.bytesize > limit ? text.byteslice(0, limit) : text
|
|
135
|
+
rescue StandardError
|
|
136
|
+
""
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -13,7 +13,9 @@ module Chronos
|
|
|
13
13
|
# @errors Invalid documents are ignored and return false.
|
|
14
14
|
# @performance Validation is bounded by configured document and list limits.
|
|
15
15
|
class RemoteConfiguration
|
|
16
|
-
SUPPORTED_EVENT_TYPES =
|
|
16
|
+
SUPPORTED_EVENT_TYPES = %w(
|
|
17
|
+
exception request query job cache external_http dependencies deploy metric_batch
|
|
18
|
+
).freeze
|
|
17
19
|
MAX_IGNORED_FINGERPRINTS = 100
|
|
18
20
|
MAX_FINGERPRINT_BYTES = 256
|
|
19
21
|
MIN_PAYLOAD_SIZE = 256
|