chronos-ruby 0.1.0.pre.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +251 -0
- data/SECURITY.md +9 -0
- data/contracts/event-v1.schema.json +51 -0
- data/docs/adr/ADR-001-core-and-integrations.md +25 -0
- data/docs/adr/ADR-002-version-lines.md +25 -0
- data/docs/adr/ADR-003-net-http-legacy-transport.md +25 -0
- data/docs/adr/ADR-004-bounded-queue.md +25 -0
- data/docs/adr/ADR-005-sanitize-before-backlog.md +25 -0
- data/docs/adr/ADR-006-versioned-event-contract.md +25 -0
- data/docs/architecture.md +31 -0
- data/docs/compatibility.md +21 -0
- data/docs/configuration.md +40 -0
- data/docs/data-collected.md +20 -0
- data/docs/examples/plain-ruby.md +12 -0
- data/docs/modules/async-queue.md +17 -0
- data/docs/modules/configuration.md +9 -0
- data/docs/modules/notice-pipeline.md +20 -0
- data/docs/modules/transport.md +9 -0
- data/docs/performance.md +16 -0
- data/docs/privacy-lgpd.md +16 -0
- data/docs/troubleshooting.md +25 -0
- data/lib/chronos/adapters/net_http_transport.rb +132 -0
- data/lib/chronos/adapters.rb +10 -0
- data/lib/chronos/agent.rb +57 -0
- data/lib/chronos/application/capture_exception.rb +61 -0
- data/lib/chronos/application.rb +10 -0
- data/lib/chronos/configuration.rb +139 -0
- data/lib/chronos/core/backtrace_parser.rb +74 -0
- data/lib/chronos/core/exception_cause_collector.rb +63 -0
- data/lib/chronos/core/notice.rb +53 -0
- data/lib/chronos/core/notice_builder.rb +83 -0
- data/lib/chronos/core/payload_serializer.rb +165 -0
- data/lib/chronos/core/runtime_info.rb +42 -0
- data/lib/chronos/core.rb +10 -0
- data/lib/chronos/errors.rb +24 -0
- data/lib/chronos/internal/bounded_queue.rb +79 -0
- data/lib/chronos/internal/safe_logger.rb +55 -0
- data/lib/chronos/internal/worker_pool.rb +155 -0
- data/lib/chronos/internal.rb +10 -0
- data/lib/chronos/ports/transport.rb +45 -0
- data/lib/chronos/ports.rb +10 -0
- data/lib/chronos/ruby/version.rb +1 -0
- data/lib/chronos/ruby.rb +1 -0
- data/lib/chronos/version.rb +4 -0
- data/lib/chronos.rb +98 -0
- metadata +156 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Compatibility
|
|
2
|
+
|
|
3
|
+
Chronos Ruby 0.x is the legacy line. Technical compatibility does not make an end-of-life Ruby or Rails release secure.
|
|
4
|
+
|
|
5
|
+
| Ruby | Rails integration | Status | Evidence |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| 2.2.10 | None in 0.1 | Best effort | Local unit, contract, integration, fork, and fake-server suite |
|
|
8
|
+
| 2.3.8 | None in 0.1 | Best effort | Dedicated CI job pending first successful run |
|
|
9
|
+
| 2.4.10 | None in 0.1 | Best effort | Dedicated CI job pending first successful run |
|
|
10
|
+
| 2.5.9 | None in 0.1 | Best effort | Dedicated CI job pending first successful run |
|
|
11
|
+
| 2.6.10 | None in 0.1 | Best effort | Dedicated CI job pending first successful run |
|
|
12
|
+
| 2.7 and newer | None in 0.x | Unsupported | Belongs to transitional or modern lines |
|
|
13
|
+
|
|
14
|
+
No Rails version is declared supported by version 0.1. Rails applications may call the plain Ruby API manually. Rails support requires a real example application, framework integration tests, and a successful dedicated matrix job.
|
|
15
|
+
|
|
16
|
+
Status meanings:
|
|
17
|
+
|
|
18
|
+
- Supported: the complete required compatibility gate passes.
|
|
19
|
+
- Best effort: intended to work, but the complete gate has not passed yet.
|
|
20
|
+
- Deprecated: still tested while removal is planned.
|
|
21
|
+
- Unsupported: outside this release line.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
`Chronos.configure` creates a mutable configuration, validates it, and gives runtime components an immutable snapshot.
|
|
4
|
+
|
|
5
|
+
| Option | Required | Default | Description |
|
|
6
|
+
|---|---:|---|---|
|
|
7
|
+
| `project_id` | Yes when enabled | `nil` | Public project identifier included in the envelope |
|
|
8
|
+
| `project_key` | Yes when enabled | `nil` | Secret authentication key sent only as an HTTP header |
|
|
9
|
+
| `host` | Yes when enabled | `nil` | Absolute Chronos HTTP endpoint; HTTPS is required by default |
|
|
10
|
+
| `environment` | Recommended | `production` | Application environment |
|
|
11
|
+
| `app_version` | Optional | `nil` | Release or revision identifier |
|
|
12
|
+
| `service_name` | Recommended | `nil` | Logical service name |
|
|
13
|
+
| `root_directory` | Optional | `Dir.pwd` | Used to identify application backtrace frames |
|
|
14
|
+
| `logger` | Optional | `nil` | Logger receiving bounded internal diagnostics |
|
|
15
|
+
| `timeout` | Optional | `5.0` | HTTP read timeout in seconds |
|
|
16
|
+
| `open_timeout` | Optional | `2.0` | HTTP connection timeout in seconds |
|
|
17
|
+
| `queue_size` | Optional | `100` | Maximum queued events |
|
|
18
|
+
| `workers` | Optional | `1` | Fixed positive worker count |
|
|
19
|
+
| `enabled` | Optional | `true` | Enables all capture |
|
|
20
|
+
| `error_notifications` | Optional | `true` | Enables exception events |
|
|
21
|
+
| `ignored_environments` | Optional | `[]` | Environments where capture is disabled |
|
|
22
|
+
| `proxy` | Optional | `nil` | Absolute HTTP proxy URL |
|
|
23
|
+
| `ssl_verify` | Recommended | `true` | Enables TLS peer verification |
|
|
24
|
+
| `user_agent` | Optional | Agent version | HTTP user agent |
|
|
25
|
+
| `max_payload_size` | Optional | `1048576` | Maximum serialized payload bytes |
|
|
26
|
+
| `gzip` | Optional | `false` | Compresses request bodies with gzip |
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
Chronos.configure do |config|
|
|
30
|
+
config.project_id = ENV["CHRONOS_PROJECT_ID"]
|
|
31
|
+
config.project_key = ENV["CHRONOS_PROJECT_KEY"]
|
|
32
|
+
config.host = ENV["CHRONOS_HOST"]
|
|
33
|
+
config.environment = ENV["APP_ENV"] || "production"
|
|
34
|
+
config.service_name = "billing"
|
|
35
|
+
config.queue_size = 100
|
|
36
|
+
config.workers = 1
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The gem never reads all environment variables automatically. The host application decides which values to pass.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Data collected
|
|
2
|
+
|
|
3
|
+
Version 0.1 emits only manual exception events.
|
|
4
|
+
|
|
5
|
+
| Data | Default | Source |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Exception class and message | Collected | Ruby exception |
|
|
8
|
+
| Structured backtrace | Collected when present | Ruby exception |
|
|
9
|
+
| Cause class and message | Collected when present | `Exception#cause` |
|
|
10
|
+
| Ruby version, engine, platform | Collected | Ruby constants |
|
|
11
|
+
| PID and opaque thread ID | Collected | Ruby runtime |
|
|
12
|
+
| Hostname | Collected when available | Standard library |
|
|
13
|
+
| Environment and service | Configuration | Host application |
|
|
14
|
+
| Application version | Optional | Host application |
|
|
15
|
+
| Context, parameters, session, user | Optional | Explicit capture arguments |
|
|
16
|
+
| Tags and fingerprint | Optional | Explicit capture arguments |
|
|
17
|
+
|
|
18
|
+
The gem never collects request bodies, response bodies, cookies, HTTP authorization headers, environment variables in bulk, source code, SQL bind values, database rows, or installed gems in version 0.1.
|
|
19
|
+
|
|
20
|
+
The secret `project_key` is an authentication header and is excluded from the JSON payload and logger diagnostics.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Plain Ruby example
|
|
2
|
+
|
|
3
|
+
Set test project credentials and an endpoint, then run the executable example:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
CHRONOS_PROJECT_ID=project-id \
|
|
7
|
+
CHRONOS_PROJECT_KEY=project-key \
|
|
8
|
+
CHRONOS_HOST=https://chronos.example.com \
|
|
9
|
+
bundle _1.17.3_ exec ruby examples/plain-ruby/example.rb
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The example reports one manual exception and closes the agent with a two-second timeout. Use a local fake endpoint when auditing the payload.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Asynchronous queue module
|
|
2
|
+
|
|
3
|
+
`BoundedQueue` stores at most `queue_size` serialized events. When full, it drops the newest event and increments a counter. Producers do not wait for capacity.
|
|
4
|
+
|
|
5
|
+
`WorkerPool` owns a fixed number of lazy threads. It tracks active deliveries, supports timed flush and close, contains transport errors, and recreates worker references after fork.
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
stateDiagram-v2
|
|
9
|
+
[*] --> Accepted
|
|
10
|
+
Accepted --> Queued
|
|
11
|
+
Queued --> Sending
|
|
12
|
+
Sending --> Sent
|
|
13
|
+
Sending --> Dropped: delivery failure in 0.1
|
|
14
|
+
Accepted --> Dropped: queue full
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Version 0.1 has no disk persistence and no retry. Queue statistics expose current size, capacity, accepted events, and dropped events. Tests cover full queues, waiter wakeup, lazy startup, contained failures, timed shutdown, double close, flush, and fork behavior.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Configuration module
|
|
2
|
+
|
|
3
|
+
Configuration separates mutable setup from immutable runtime state. `Chronos::Configuration` owns defaults and validation; `Snapshot` is shared with runtime components.
|
|
4
|
+
|
|
5
|
+
This module is separate from capture because configuration errors should be found before any event enters the pipeline. It can be extended by adding a validated attribute and immutable snapshot field.
|
|
6
|
+
|
|
7
|
+
Risks include accidentally logging credentials and changing settings while events run. The snapshot prevents mutation, while logger and transport code never interpolate `project_key`.
|
|
8
|
+
|
|
9
|
+
Tests in `spec/unit/configuration_spec.rb` verify required fields, HTTPS defaults, immutable containers, disabled operation, and bounded numeric settings.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Notice pipeline
|
|
2
|
+
|
|
3
|
+
The notice pipeline converts a Ruby `Exception` into a versioned JSON event.
|
|
4
|
+
|
|
5
|
+
```mermaid
|
|
6
|
+
flowchart LR
|
|
7
|
+
Exception --> NoticeBuilder
|
|
8
|
+
NoticeBuilder --> BacktraceParser
|
|
9
|
+
NoticeBuilder --> CauseCollector
|
|
10
|
+
NoticeBuilder --> RuntimeInfo
|
|
11
|
+
NoticeBuilder --> Notice
|
|
12
|
+
Notice --> PayloadSerializer
|
|
13
|
+
PayloadSerializer --> SerializedEvent
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`Notice` is immutable. Parsers are stateless. The serializer accepts only JSON primitives, bounds nested structures, tolerates invalid encoding, and represents unknown objects without arbitrary `to_json` calls.
|
|
17
|
+
|
|
18
|
+
Version 0.1 deliberately excludes the advanced sanitizer, sampler, and fingerprint policy. These can be inserted before serialization without changing transport or queue code.
|
|
19
|
+
|
|
20
|
+
Unit and contract tests verify missing backtraces, cyclic causes, invalid encoding, unsafe objects, payload limits, and the v1 envelope.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Transport module
|
|
2
|
+
|
|
3
|
+
`Chronos::Ports::Transport` defines delivery behavior. `Chronos::Adapters::NetHttpTransport` implements it with the Ruby standard library.
|
|
4
|
+
|
|
5
|
+
Each event uses bounded open and read timeouts. TLS verification is enabled by default. Authentication and idempotency values are headers. Redirects are not followed. Statuses are classified as success, client error, rate limited, server error, network error, or closed.
|
|
6
|
+
|
|
7
|
+
The adapter creates a connection per event in version 0.1. This is conservative for legacy applications and avoids shared socket lifecycle state. Batch optimization and retry may be added behind the same port.
|
|
8
|
+
|
|
9
|
+
Risks include proxy credential exposure, TLS incompatibility on old operating systems, and endpoint latency. Tests use a local fake HTTP server to verify headers, `2xx`, `429`, `500`, timeout, and invalid TLS behavior.
|
data/docs/performance.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Performance
|
|
2
|
+
|
|
3
|
+
Performance is a functional requirement, but version 0.1 makes no unverified speed claim.
|
|
4
|
+
|
|
5
|
+
Current controls:
|
|
6
|
+
|
|
7
|
+
- backtraces are limited to 200 frames;
|
|
8
|
+
- hashes and arrays are bounded during serialization;
|
|
9
|
+
- strings and total payload size are bounded;
|
|
10
|
+
- the asynchronous queue has fixed capacity;
|
|
11
|
+
- worker count is fixed and threads start lazily;
|
|
12
|
+
- producers never wait for queue capacity;
|
|
13
|
+
- HTTP open and read timeouts are explicit;
|
|
14
|
+
- shutdown and flush have caller-controlled timeouts.
|
|
15
|
+
|
|
16
|
+
Run the scripts under `benchmarks/` and record Ruby version, operating system, CPU, warmup, iteration count, median, and dispersion before publishing results. Request overhead is not measured because automatic request integration is outside version 0.1.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Privacy and LGPD
|
|
2
|
+
|
|
3
|
+
Version 0.1 enforces structural limits but does not yet implement the recursive sensitive-data sanitizer planned for version 0.2.
|
|
4
|
+
|
|
5
|
+
Applications must not pass passwords, tokens, authorization headers, cookies, session secrets, private keys, card data, CPF, CNPJ, health data, or other unnecessary personal information.
|
|
6
|
+
|
|
7
|
+
Recommended practice:
|
|
8
|
+
|
|
9
|
+
1. pass opaque internal identifiers instead of names or e-mail addresses;
|
|
10
|
+
2. allowlist context fields in application code;
|
|
11
|
+
3. inspect payloads against a local fake server before production;
|
|
12
|
+
4. keep TLS verification enabled;
|
|
13
|
+
5. document the lawful purpose and retention policy in the SaaS;
|
|
14
|
+
6. disable the agent in environments where collection is not authorized.
|
|
15
|
+
|
|
16
|
+
Version 0.1 limits object depth, hash keys, array items, string bytes, backtrace frames, queue capacity, and total payload size. Unknown Ruby objects are represented by class name without calling their `to_json` implementation.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Configuration raises an error
|
|
4
|
+
|
|
5
|
+
Verify `project_id`, `project_key`, and `host`. HTTPS is required while `ssl_verify` is true. Queue size, worker count, payload size, and timeouts must be positive.
|
|
6
|
+
|
|
7
|
+
## `Chronos.notify` returns false
|
|
8
|
+
|
|
9
|
+
The agent may be unconfigured, disabled, ignored in the current environment, unable to serialize the value, closed, or at queue capacity. Check the configured logger and `agent.diagnostics` when constructing an agent directly in diagnostics code.
|
|
10
|
+
|
|
11
|
+
## `Chronos.notify_sync` returns false
|
|
12
|
+
|
|
13
|
+
Check DNS, TLS certificates, credentials, HTTP status, proxy configuration, and timeout values. Version 0.1 does not retry.
|
|
14
|
+
|
|
15
|
+
## Events disappear during shutdown
|
|
16
|
+
|
|
17
|
+
Call `Chronos.close(timeout)` and inspect its Boolean result. A `false` result means accepted work could not finish before the timeout.
|
|
18
|
+
|
|
19
|
+
## Forking servers
|
|
20
|
+
|
|
21
|
+
Workers are recreated after a process fork. Configure before or after forking, then call capture in the child. Always close each child during shutdown.
|
|
22
|
+
|
|
23
|
+
## Sensitive data
|
|
24
|
+
|
|
25
|
+
Version 0.1 does not redact arbitrary context. Stop sending the affected field, rotate exposed credentials, and follow the incident process for the Chronos SaaS.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "openssl"
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "zlib"
|
|
6
|
+
|
|
7
|
+
module Chronos
|
|
8
|
+
module Adapters
|
|
9
|
+
# Net::HTTP implementation of the Chronos transport port.
|
|
10
|
+
#
|
|
11
|
+
# @responsibility Send serialized events over bounded HTTPS requests.
|
|
12
|
+
# @motivation Use the Ruby standard library to preserve legacy compatibility.
|
|
13
|
+
# @limits It classifies failures but does not retry or persist events in version 0.1.
|
|
14
|
+
# @collaborators Configuration::Snapshot, SerializedEvent, and TransportResult.
|
|
15
|
+
# @thread_safety Creates a new Net::HTTP connection per call and synchronizes health state.
|
|
16
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; no Rails dependency.
|
|
17
|
+
# @example
|
|
18
|
+
# result = transport.send_event(serialized_event)
|
|
19
|
+
# @errors Network, TLS, and HTTP errors are returned, never raised to callers.
|
|
20
|
+
# @performance One bounded network request per event in version 0.1.
|
|
21
|
+
class NetHttpTransport
|
|
22
|
+
EVENT_PATH = "/api/v1/events".freeze
|
|
23
|
+
|
|
24
|
+
def initialize(config, logger = nil)
|
|
25
|
+
@config = config
|
|
26
|
+
@logger = logger || Internal::SafeLogger.new(config.logger)
|
|
27
|
+
@health_mutex = Mutex.new
|
|
28
|
+
@healthy = true
|
|
29
|
+
@closed = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def send_event(event)
|
|
33
|
+
return Ports::TransportResult.new(:closed) if closed?
|
|
34
|
+
|
|
35
|
+
response = perform_request(event)
|
|
36
|
+
result = classify(response)
|
|
37
|
+
update_health(result.success?)
|
|
38
|
+
result
|
|
39
|
+
rescue StandardError => error
|
|
40
|
+
update_health(false)
|
|
41
|
+
@logger.warn("Chronos transport failed: #{error.class}")
|
|
42
|
+
Ports::TransportResult.new(:network_error, :error => error.class.name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def send_batch(events)
|
|
46
|
+
Array(events).map { |event| send_event(event) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def healthy?
|
|
50
|
+
@health_mutex.synchronize { @healthy && !@closed }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def close
|
|
54
|
+
@health_mutex.synchronize { @closed = true }
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def perform_request(event)
|
|
61
|
+
uri = endpoint_uri
|
|
62
|
+
http = build_http(uri)
|
|
63
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
64
|
+
request["Content-Type"] = "application/json"
|
|
65
|
+
request["Accept"] = "application/json"
|
|
66
|
+
request["User-Agent"] = @config.user_agent
|
|
67
|
+
request["X-Chronos-Project-ID"] = @config.project_id.to_s
|
|
68
|
+
request["X-Chronos-Project-Key"] = @config.project_key.to_s
|
|
69
|
+
request["Idempotency-Key"] = event.event_id
|
|
70
|
+
request.body = request_body(event.body)
|
|
71
|
+
request["Content-Encoding"] = "gzip" if @config.gzip
|
|
72
|
+
http.start { |connection| connection.request(request) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def endpoint_uri
|
|
76
|
+
uri = URI.parse(@config.host)
|
|
77
|
+
base_path = uri.path.to_s.sub(%r{/+\z}, "")
|
|
78
|
+
uri.path = base_path + EVENT_PATH
|
|
79
|
+
uri.query = nil
|
|
80
|
+
uri.fragment = nil
|
|
81
|
+
uri
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_http(uri)
|
|
85
|
+
http = if @config.proxy
|
|
86
|
+
proxy = URI.parse(@config.proxy)
|
|
87
|
+
Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password).new(uri.host, uri.port)
|
|
88
|
+
else
|
|
89
|
+
Net::HTTP.new(uri.host, uri.port)
|
|
90
|
+
end
|
|
91
|
+
http.open_timeout = @config.open_timeout
|
|
92
|
+
http.read_timeout = @config.timeout
|
|
93
|
+
if uri.scheme == "https"
|
|
94
|
+
http.use_ssl = true
|
|
95
|
+
http.verify_mode = @config.ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
96
|
+
end
|
|
97
|
+
http
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def classify(response)
|
|
101
|
+
code = response.code.to_i
|
|
102
|
+
options = {:status_code => code}
|
|
103
|
+
return Ports::TransportResult.new(:success, options) if code >= 200 && code < 300
|
|
104
|
+
if code == 429
|
|
105
|
+
options[:retry_after] = response["Retry-After"]
|
|
106
|
+
return Ports::TransportResult.new(:rate_limited, options)
|
|
107
|
+
end
|
|
108
|
+
return Ports::TransportResult.new(:server_error, options) if code >= 500
|
|
109
|
+
|
|
110
|
+
Ports::TransportResult.new(:client_error, options)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def request_body(body)
|
|
114
|
+
return body unless @config.gzip
|
|
115
|
+
|
|
116
|
+
output = StringIO.new
|
|
117
|
+
writer = Zlib::GzipWriter.new(output)
|
|
118
|
+
writer.write(body)
|
|
119
|
+
writer.close
|
|
120
|
+
output.string
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def update_health(value)
|
|
124
|
+
@health_mutex.synchronize { @healthy = value }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def closed?
|
|
128
|
+
@health_mutex.synchronize { @closed }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
# Infrastructure implementations of Chronos ports.
|
|
3
|
+
#
|
|
4
|
+
# @responsibility Connect the agent to HTTP and future external systems.
|
|
5
|
+
# @motivation Isolate standard-library and vendor-specific behavior.
|
|
6
|
+
# @limits Adapters must not add policy to the domain pipeline.
|
|
7
|
+
# @thread_safety Defined by each adapter.
|
|
8
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
9
|
+
module Adapters; end
|
|
10
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
# Runtime composition root for the framework-independent Chronos agent.
|
|
3
|
+
#
|
|
4
|
+
# @responsibility Own transport, queue, workers, and the capture use case.
|
|
5
|
+
# @motivation Keep construction details outside the public module facade.
|
|
6
|
+
# @limits It does not integrate with Rails, Rack, or job systems.
|
|
7
|
+
# @collaborators Configuration snapshot, CaptureException, BoundedQueue, and NetHttpTransport.
|
|
8
|
+
# @thread_safety Runtime collaborators synchronize mutable state.
|
|
9
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
10
|
+
# @example
|
|
11
|
+
# agent.notify(RuntimeError.new("failed"))
|
|
12
|
+
# @errors Capture errors return false; explicit construction requires valid configuration.
|
|
13
|
+
# @performance No worker threads are created until the first asynchronous event.
|
|
14
|
+
class Agent
|
|
15
|
+
DEFAULT_FLUSH_TIMEOUT = 5.0
|
|
16
|
+
|
|
17
|
+
attr_reader :config
|
|
18
|
+
|
|
19
|
+
def initialize(config, options = {})
|
|
20
|
+
@config = config
|
|
21
|
+
@logger = options[:logger] || Internal::SafeLogger.new(config.logger)
|
|
22
|
+
@transport = options[:transport] || Adapters::NetHttpTransport.new(config, @logger)
|
|
23
|
+
unless Ports::Transport.compatible?(@transport)
|
|
24
|
+
raise ArgumentError, "transport does not implement the Chronos transport port"
|
|
25
|
+
end
|
|
26
|
+
@queue = options[:queue] || Internal::BoundedQueue.new(config.queue_size)
|
|
27
|
+
@worker_pool = options[:worker_pool] || Internal::WorkerPool.new(@queue, @transport, config.workers, @logger)
|
|
28
|
+
@capture = options[:capture] || Application::CaptureException.new(config, @worker_pool, @transport, @logger)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def notify(exception, context = {})
|
|
32
|
+
@capture.call(exception, context)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def notify_sync(exception, context = {})
|
|
36
|
+
@capture.call_sync(exception, context)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def flush(timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
40
|
+
@worker_pool.flush(timeout)
|
|
41
|
+
rescue StandardError => error
|
|
42
|
+
@logger.warn("Chronos flush failed: #{error.class}")
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def close(timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
47
|
+
@worker_pool.close(timeout)
|
|
48
|
+
rescue StandardError => error
|
|
49
|
+
@logger.warn("Chronos close failed: #{error.class}")
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def diagnostics
|
|
54
|
+
@queue.stats
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Application
|
|
3
|
+
# Orchestrates exception normalization, serialization, queueing, and delivery.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Execute the version 0.1 exception capture pipeline.
|
|
6
|
+
# @motivation Keep the public facade and transport adapters free of use-case logic.
|
|
7
|
+
# @limits It does not implement advanced sanitization, sampling, retry, or backlog.
|
|
8
|
+
# @collaborators NoticeBuilder, PayloadSerializer, WorkerPool, and Transport.
|
|
9
|
+
# @thread_safety Collaborators are immutable or synchronized; calls may run concurrently.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
|
|
11
|
+
# @example
|
|
12
|
+
# capture.call(exception, :user => {"id" => "42"})
|
|
13
|
+
# @errors All internal StandardError failures are contained and logged.
|
|
14
|
+
# @performance Local work is bounded before asynchronous queue insertion.
|
|
15
|
+
class CaptureException
|
|
16
|
+
def initialize(config, worker_pool, transport, logger = nil, options = {})
|
|
17
|
+
@config = config
|
|
18
|
+
@worker_pool = worker_pool
|
|
19
|
+
@transport = transport
|
|
20
|
+
@logger = logger || Internal::SafeLogger.new(config.logger)
|
|
21
|
+
@notice_builder = options[:notice_builder] || Core::NoticeBuilder.new(config)
|
|
22
|
+
@serializer = options[:serializer] || Core::PayloadSerializer.new(config)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(exception, context = {})
|
|
26
|
+
return false unless capture_enabled?
|
|
27
|
+
|
|
28
|
+
event = build_event(exception, context)
|
|
29
|
+
@worker_pool.enqueue(event)
|
|
30
|
+
rescue StandardError => error
|
|
31
|
+
diagnose(error)
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call_sync(exception, context = {})
|
|
36
|
+
return false unless capture_enabled?
|
|
37
|
+
|
|
38
|
+
event = build_event(exception, context)
|
|
39
|
+
@transport.send_event(event).success?
|
|
40
|
+
rescue StandardError => error
|
|
41
|
+
diagnose(error)
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def capture_enabled?
|
|
48
|
+
@config.enabled_for_environment? && @config.error_notifications
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_event(exception, context)
|
|
52
|
+
notice = @notice_builder.call(exception, context)
|
|
53
|
+
@serializer.call(notice)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def diagnose(error)
|
|
57
|
+
@logger.warn("Chronos capture failed: #{error.class}")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
# Application use cases that coordinate the core and ports.
|
|
3
|
+
#
|
|
4
|
+
# @responsibility Orchestrate capture and delivery workflows.
|
|
5
|
+
# @motivation Keep policy separate from domain values and adapters.
|
|
6
|
+
# @limits Does not implement HTTP or framework hooks.
|
|
7
|
+
# @thread_safety Use cases rely on thread-safe collaborators.
|
|
8
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
9
|
+
module Application; end
|
|
10
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
# Mutable configuration used only while the Chronos agent is being set up.
|
|
5
|
+
#
|
|
6
|
+
# @responsibility Store options, validate them,
|
|
7
|
+
# and create immutable snapshots.
|
|
8
|
+
# @motivation Prevent runtime components
|
|
9
|
+
# from observing partially changed state.
|
|
10
|
+
# @limits It does not send events or read application environment variables.
|
|
11
|
+
# @collaborators Chronos::Configuration::Snapshot.
|
|
12
|
+
# @thread_safety Configure one instance
|
|
13
|
+
# from one thread before sharing its snapshot.
|
|
14
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
15
|
+
# @example
|
|
16
|
+
# config = Chronos::Configuration.new
|
|
17
|
+
# config.project_id = 'project-id'
|
|
18
|
+
# config.project_key = 'project-key'
|
|
19
|
+
# config.host = 'https://chronos.example.com'
|
|
20
|
+
# snapshot = config.snapshot
|
|
21
|
+
class Configuration
|
|
22
|
+
ATTRIBUTES = [
|
|
23
|
+
:project_id, :project_key, :host, :environment, :app_version,
|
|
24
|
+
:service_name, :root_directory, :logger, :timeout, :open_timeout,
|
|
25
|
+
:queue_size, :workers, :enabled, :error_notifications,
|
|
26
|
+
:ignored_environments, :proxy, :ssl_verify, :user_agent,
|
|
27
|
+
:max_payload_size, :gzip
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
attr_accessor(*ATTRIBUTES)
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@project_id = nil
|
|
34
|
+
@project_key = nil
|
|
35
|
+
@host = nil
|
|
36
|
+
@environment = "production"
|
|
37
|
+
@app_version = nil
|
|
38
|
+
@service_name = nil
|
|
39
|
+
@root_directory = Dir.pwd
|
|
40
|
+
@logger = nil
|
|
41
|
+
@timeout = 5.0
|
|
42
|
+
@open_timeout = 2.0
|
|
43
|
+
@queue_size = 100
|
|
44
|
+
@workers = 1
|
|
45
|
+
@enabled = true
|
|
46
|
+
@error_notifications = true
|
|
47
|
+
@ignored_environments = []
|
|
48
|
+
@proxy = nil
|
|
49
|
+
@ssl_verify = true
|
|
50
|
+
@user_agent = "chronos-ruby/#{Chronos::VERSION}"
|
|
51
|
+
@max_payload_size = 1_048_576
|
|
52
|
+
@gzip = false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def snapshot
|
|
56
|
+
errors = validation_errors
|
|
57
|
+
raise ConfigurationError, errors.join(", ") unless errors.empty?
|
|
58
|
+
|
|
59
|
+
Snapshot.new(to_hash)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def valid?
|
|
63
|
+
validation_errors.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validation_errors
|
|
67
|
+
errors = []
|
|
68
|
+
if enabled
|
|
69
|
+
errors << "project_id is required" if blank?(project_id)
|
|
70
|
+
errors << "project_key is required" if blank?(project_key)
|
|
71
|
+
errors.concat(host_errors)
|
|
72
|
+
end
|
|
73
|
+
errors << "timeout must be greater than zero" unless positive_number?(timeout)
|
|
74
|
+
errors << "open_timeout must be greater than zero" unless positive_number?(open_timeout)
|
|
75
|
+
errors << "queue_size must be a positive integer" unless positive_integer?(queue_size)
|
|
76
|
+
errors << "workers must be a positive integer" unless positive_integer?(workers)
|
|
77
|
+
errors << "max_payload_size must be a positive integer" unless positive_integer?(max_payload_size)
|
|
78
|
+
errors
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def to_hash
|
|
84
|
+
ATTRIBUTES.each_with_object({}) do |attribute, values|
|
|
85
|
+
value = public_send(attribute)
|
|
86
|
+
value = value.dup if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String)
|
|
87
|
+
values[attribute] = value
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def host_errors
|
|
92
|
+
return ["host is required"] if blank?(host)
|
|
93
|
+
|
|
94
|
+
uri = URI.parse(host.to_s)
|
|
95
|
+
return ["host must be an absolute HTTP or HTTPS URL"] unless uri.host && %w(http https).include?(uri.scheme)
|
|
96
|
+
return ["host must use HTTPS unless ssl_verify is explicitly disabled"] if uri.scheme != "https" && ssl_verify
|
|
97
|
+
|
|
98
|
+
[]
|
|
99
|
+
rescue URI::InvalidURIError
|
|
100
|
+
["host must be a valid URL"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def blank?(value)
|
|
104
|
+
value.nil? || value.to_s.strip.empty?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def positive_number?(value)
|
|
108
|
+
value.is_a?(Numeric) && value > 0
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def positive_integer?(value)
|
|
112
|
+
value.is_a?(Integer) && value > 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Immutable configuration shared by all runtime components.
|
|
116
|
+
#
|
|
117
|
+
# @responsibility Expose validated settings without mutable containers.
|
|
118
|
+
# @motivation Keep capture behavior stable while multiple threads run.
|
|
119
|
+
# @limits It cannot be edited after creation.
|
|
120
|
+
# @thread_safety Safe to share between threads after construction.
|
|
121
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
122
|
+
class Snapshot
|
|
123
|
+
attr_reader(*ATTRIBUTES)
|
|
124
|
+
|
|
125
|
+
def initialize(values)
|
|
126
|
+
ATTRIBUTES.each do |attribute|
|
|
127
|
+
value = values[attribute]
|
|
128
|
+
value.freeze if value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(String)
|
|
129
|
+
instance_variable_set("@#{attribute}", value)
|
|
130
|
+
end
|
|
131
|
+
freeze
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def enabled_for_environment?
|
|
135
|
+
enabled && !ignored_environments.map(&:to_s).include?(environment.to_s)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|