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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +34 -0
  3. data/CODE_OF_CONDUCT.md +74 -0
  4. data/CONTRIBUTING.md +23 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +251 -0
  7. data/SECURITY.md +9 -0
  8. data/contracts/event-v1.schema.json +51 -0
  9. data/docs/adr/ADR-001-core-and-integrations.md +25 -0
  10. data/docs/adr/ADR-002-version-lines.md +25 -0
  11. data/docs/adr/ADR-003-net-http-legacy-transport.md +25 -0
  12. data/docs/adr/ADR-004-bounded-queue.md +25 -0
  13. data/docs/adr/ADR-005-sanitize-before-backlog.md +25 -0
  14. data/docs/adr/ADR-006-versioned-event-contract.md +25 -0
  15. data/docs/architecture.md +31 -0
  16. data/docs/compatibility.md +21 -0
  17. data/docs/configuration.md +40 -0
  18. data/docs/data-collected.md +20 -0
  19. data/docs/examples/plain-ruby.md +12 -0
  20. data/docs/modules/async-queue.md +17 -0
  21. data/docs/modules/configuration.md +9 -0
  22. data/docs/modules/notice-pipeline.md +20 -0
  23. data/docs/modules/transport.md +9 -0
  24. data/docs/performance.md +16 -0
  25. data/docs/privacy-lgpd.md +16 -0
  26. data/docs/troubleshooting.md +25 -0
  27. data/lib/chronos/adapters/net_http_transport.rb +132 -0
  28. data/lib/chronos/adapters.rb +10 -0
  29. data/lib/chronos/agent.rb +57 -0
  30. data/lib/chronos/application/capture_exception.rb +61 -0
  31. data/lib/chronos/application.rb +10 -0
  32. data/lib/chronos/configuration.rb +139 -0
  33. data/lib/chronos/core/backtrace_parser.rb +74 -0
  34. data/lib/chronos/core/exception_cause_collector.rb +63 -0
  35. data/lib/chronos/core/notice.rb +53 -0
  36. data/lib/chronos/core/notice_builder.rb +83 -0
  37. data/lib/chronos/core/payload_serializer.rb +165 -0
  38. data/lib/chronos/core/runtime_info.rb +42 -0
  39. data/lib/chronos/core.rb +10 -0
  40. data/lib/chronos/errors.rb +24 -0
  41. data/lib/chronos/internal/bounded_queue.rb +79 -0
  42. data/lib/chronos/internal/safe_logger.rb +55 -0
  43. data/lib/chronos/internal/worker_pool.rb +155 -0
  44. data/lib/chronos/internal.rb +10 -0
  45. data/lib/chronos/ports/transport.rb +45 -0
  46. data/lib/chronos/ports.rb +10 -0
  47. data/lib/chronos/ruby/version.rb +1 -0
  48. data/lib/chronos/ruby.rb +1 -0
  49. data/lib/chronos/version.rb +4 -0
  50. data/lib/chronos.rb +98 -0
  51. 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.
@@ -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