chronos-ruby 0.3.0.pre.1 → 0.5.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 +23 -0
- data/README.md +55 -17
- data/contracts/event-v1.schema.json +25 -11
- data/contracts/rack-context-v1.schema.json +43 -0
- data/docs/adr/ADR-012-rack-context-isolation.md +27 -0
- data/docs/adr/ADR-013-legacy-rails-notifications.md +27 -0
- data/docs/architecture.md +17 -3
- data/docs/compatibility.md +6 -6
- data/docs/configuration.md +13 -2
- data/docs/data-collected.md +15 -2
- data/docs/modules/rack-context.md +59 -0
- data/docs/modules/rails-legacy.md +60 -0
- data/docs/modules/telemetry-events.md +9 -0
- data/docs/performance.md +28 -3
- data/docs/privacy-lgpd.md +13 -3
- data/docs/troubleshooting.md +18 -2
- data/lib/chronos/adapters/thread_local_context_store.rb +52 -0
- data/lib/chronos/agent.rb +97 -6
- data/lib/chronos/application/capture_exception.rb +2 -2
- data/lib/chronos/application/capture_telemetry.rb +37 -0
- data/lib/chronos/application/remote_configuration.rb +1 -1
- data/lib/chronos/configuration/snapshot.rb +53 -0
- data/lib/chronos/configuration/validation.rb +122 -0
- data/lib/chronos/configuration.rb +22 -134
- data/lib/chronos/core/breadcrumb.rb +126 -0
- data/lib/chronos/core/telemetry_event.rb +106 -0
- data/lib/chronos/integrations/rack/middleware.rb +156 -0
- data/lib/chronos/integrations/rack.rb +12 -0
- data/lib/chronos/integrations.rb +10 -0
- data/lib/chronos/ports/context_store.rb +22 -0
- data/lib/chronos/rails/installer.rb +70 -0
- data/lib/chronos/rails/notifications_subscriber.rb +203 -0
- data/lib/chronos/rails/railtie.rb +21 -0
- data/lib/chronos/rails.rb +16 -0
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +46 -1
- data/lib/generators/chronos/install/install_generator.rb +25 -0
- data/lib/generators/chronos/install/templates/chronos.rb +20 -0
- metadata +23 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Framework telemetry events
|
|
2
|
+
|
|
3
|
+
`Chronos::Core::TelemetryEvent` and `Chronos::Core::TelemetrySerializer` extend the v1 envelope to `request`, `query`, `job`, and `cache` events. These values exist so framework adapters do not model operational timings as fake exceptions.
|
|
4
|
+
|
|
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
|
+
|
|
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
|
+
|
|
9
|
+
Version 0.5 intentionally sends individual framework events. Aggregation, percentiles, slow-query analysis, and transaction breakdowns remain part of version 0.7.
|
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.5 makes no unverified speed claim.
|
|
4
4
|
|
|
5
5
|
Current controls:
|
|
6
6
|
|
|
@@ -15,9 +15,24 @@ Current controls:
|
|
|
15
15
|
- retry count, delay, jitter, and `Retry-After` are capped;
|
|
16
16
|
- the circuit breaker suppresses requests during sustained failure;
|
|
17
17
|
- retry backlog capacity is fixed and may be disabled;
|
|
18
|
-
- shutdown and flush have caller-controlled timeouts
|
|
18
|
+
- shutdown and flush have caller-controlled timeouts;
|
|
19
|
+
- request context and breadcrumbs have fixed structural and byte limits;
|
|
20
|
+
- Rack middleware never consumes request or response bodies.
|
|
21
|
+
- Rails subscribers copy only small allowlisted field sets and never copy raw SQL or job arguments.
|
|
19
22
|
|
|
20
|
-
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,
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
## Version 0.5 Rails subscriber benchmark
|
|
26
|
+
|
|
27
|
+
Run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/rails_notifications.rb
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This synthetic benchmark invokes the SQL notification normalizer with a no-op notifier. It verifies that raw SQL and binds are not copied and measures local allowlist normalization only; it does not include Rails dispatch, serialization, queueing, or network delivery.
|
|
34
|
+
|
|
35
|
+
A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 and 10,000 iterations. It completed in 0.041232 seconds, or approximately 4.123 microseconds per normalization. This single run has no warmup, median, or dispersion and is not a production performance claim.
|
|
21
36
|
|
|
22
37
|
## Version 0.2 development measurement
|
|
23
38
|
|
|
@@ -51,3 +66,13 @@ A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 with 1,000
|
|
|
51
66
|
| Open-circuit backlog handling | 88,922 operations/second |
|
|
52
67
|
|
|
53
68
|
The outage run retained exactly 100 events and rejected 9,900 additional events without growing the backlog. These are single development runs without median or dispersion and are not comparative performance claims.
|
|
69
|
+
|
|
70
|
+
## Version 0.4 Rack middleware benchmark
|
|
71
|
+
|
|
72
|
+
Run:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/request_overhead.rb
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 and 10,000 successful Rack-protocol calls. The direct application calls took 0.013272 seconds and middleware calls took 0.539758 seconds, for an estimated 52.649 microseconds of middleware work per request. The fixture uses no network, error capture, or response-body enumeration. This is a single development measurement, not a production latency claim; controlled warmup, median, and dispersion remain required before publishing a 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.5 sanitizes exception and framework telemetry 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
|
|
|
@@ -13,9 +13,19 @@ Version 0.3 sanitizes every exception event before JSON serialization, queueing,
|
|
|
13
13
|
| Payment-card candidates that pass the Luhn check | Replaced with `[FILTERED_CARD]` |
|
|
14
14
|
| IPv4 addresses | Last octet replaced with `0` |
|
|
15
15
|
| Unknown Ruby objects | Represented by class name without calling application serialization |
|
|
16
|
-
| Request/response bodies, cookies,
|
|
16
|
+
| Request/response bodies, raw query strings, cookies, authorization headers, raw SQL/binds, cache values, mail bodies, job arguments, environment variables | Never collected automatically in version 0.5 |
|
|
17
17
|
|
|
18
|
-
The retry backlog exists only in process memory, accepts only `SerializedEvent`, has a fixed capacity, and disappears on process exit. Version 0.
|
|
18
|
+
The retry backlog exists only in process memory, accepts only `SerializedEvent`, has a fixed capacity, and disappears on process exit. Version 0.5 does not persist telemetry to disk.
|
|
19
|
+
|
|
20
|
+
## Rails telemetry
|
|
21
|
+
|
|
22
|
+
Rails subscribers use per-notification allowlists. SQL events retain only the operation name, cached flag, and duration; cache events omit key and value; mailer events omit addresses and content; Active Job events omit job ID and arguments; view identifiers are reduced to basenames. Controller parameters are sanitized by the normal payload pipeline before queueing.
|
|
23
|
+
|
|
24
|
+
## Rack context and breadcrumbs
|
|
25
|
+
|
|
26
|
+
The Rack middleware copies only bounded operational fields and parameter hashes that another component already parsed. It does not read `rack.input` or copy `QUERY_STRING`. User context is opt-in through `chronos.user`, and user agent collection is disabled unless the middleware option enables it.
|
|
27
|
+
|
|
28
|
+
Breadcrumbs contain category, bounded message, bounded metadata, and timestamp. No raw request, response, SQL, cache, job, log, or external HTTP payload is added automatically. Rack context and breadcrumbs pass through the same sanitizer as manual context before queueing.
|
|
19
29
|
|
|
20
30
|
Blocklist matching accepts `String`, `Symbol`, and `Regexp`. String and Symbol matching is case-insensitive after punctuation normalization and also protects namespaced keys such as `user_password`.
|
|
21
31
|
|
data/docs/troubleshooting.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Configuration raises an error
|
|
4
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.
|
|
5
|
+
Verify `project_id`, `project_key`, and `host`. HTTPS is required while `ssl_verify` is true. Queue size, worker count, payload size, breadcrumb capacity, and timeouts must be positive. `breadcrumb_max_bytes` must be at least 128.
|
|
6
6
|
|
|
7
7
|
## `Chronos.notify` returns false
|
|
8
8
|
|
|
@@ -10,7 +10,23 @@ The agent may be unconfigured, disabled, ignored in the current environment, una
|
|
|
10
10
|
|
|
11
11
|
## `Chronos.notify_sync` returns false
|
|
12
12
|
|
|
13
|
-
Check DNS, TLS certificates, credentials, HTTP status, proxy configuration, and timeout values.
|
|
13
|
+
Check DNS, TLS certificates, credentials, HTTP status, proxy configuration, and timeout values. The resilience layer retries only network errors, HTTP `408`, `429`, and `5xx` responses. Inspect `agent.diagnostics` when constructing an agent directly to see retry state, backlog usage, and the circuit state.
|
|
14
|
+
|
|
15
|
+
## Rack exception is not captured
|
|
16
|
+
|
|
17
|
+
Confirm that `Chronos.configure` runs before the middleware handles requests and that the middleware wraps the application component that raises. Version 0.5 captures exceptions raised by the initial downstream Rack call; an exception raised later while a server enumerates a streaming response body is outside this release. The original exception is always re-raised, so the server log should still show it.
|
|
18
|
+
|
|
19
|
+
## Rails middleware or telemetry is missing
|
|
20
|
+
|
|
21
|
+
Require `chronos/rails` from the generated initializer and confirm it runs before requests. Automatic integration is disabled by default in Rails test and console; set `rails_capture_in_test` or `rails_capture_in_console` explicitly when validating those environments. `rails_enabled = false` disables all automatic Rails hooks. Active Job is subscribed only when its constant is available during installation.
|
|
22
|
+
|
|
23
|
+
## Rails telemetry contains no SQL or cache key
|
|
24
|
+
|
|
25
|
+
This is intentional. Version 0.5 records SQL operation name/duration and cache operation/store/hit status without raw statements, binds, keys, or values. These omissions are privacy and cardinality boundaries, not capture failures.
|
|
26
|
+
|
|
27
|
+
## Context appears missing
|
|
28
|
+
|
|
29
|
+
The legacy context store is thread-local. A new application-created thread does not inherit context. Establish a new `Chronos.with_context` scope inside that thread, and issue manual notification before the scope exits. For Rack capture, supply user and explicit parameters through the documented environment keys.
|
|
14
30
|
|
|
15
31
|
## Events disappear during shutdown
|
|
16
32
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Adapters
|
|
3
|
+
# Stores execution context in the current Ruby thread.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Isolate request context and restore nested scopes reliably.
|
|
6
|
+
# @motivation Legacy Ruby does not provide a portable fiber-local storage API.
|
|
7
|
+
# @limits Context does not propagate to new threads or across processes.
|
|
8
|
+
# @collaborators ContextStore port and Rack middleware.
|
|
9
|
+
# @thread_safety Each thread owns an independent value; one instance may be shared.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @example
|
|
12
|
+
# store.with_context(:request_id => "r1") { store.get }
|
|
13
|
+
# @errors The previous value is restored in ensure even when the block raises.
|
|
14
|
+
# @performance Reads and writes are constant-time thread-local operations.
|
|
15
|
+
class ThreadLocalContextStore
|
|
16
|
+
def initialize
|
|
17
|
+
@key = "chronos_context_#{object_id}".freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get
|
|
21
|
+
Thread.current[@key] || {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def set(context)
|
|
25
|
+
raise ArgumentError, "context must be a Hash" unless context.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
Thread.current[@key] = context
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def clear
|
|
31
|
+
Thread.current[@key] = nil
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def with_context(context)
|
|
36
|
+
previous = Thread.current[@key]
|
|
37
|
+
set(merge_context(previous || {}, context))
|
|
38
|
+
yield
|
|
39
|
+
ensure
|
|
40
|
+
previous ? Thread.current[@key] = previous : clear
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def merge_context(current, additional)
|
|
46
|
+
raise ArgumentError, "context must be a Hash" unless additional.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
current.merge(additional)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/chronos/agent.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
module Chronos
|
|
2
2
|
# Runtime composition root for the framework-independent Chronos agent.
|
|
3
3
|
#
|
|
4
|
-
# @responsibility Own
|
|
4
|
+
# @responsibility Own delivery, capture, execution context, and breadcrumb collaborators.
|
|
5
5
|
# @motivation Keep construction details outside the public module facade.
|
|
6
|
-
# @limits It does not
|
|
7
|
-
# @collaborators
|
|
6
|
+
# @limits It does not install Rack, Rails, or job integrations automatically.
|
|
7
|
+
# @collaborators CaptureException, DeliveryPipeline, ContextStore, and BreadcrumbBuffer.
|
|
8
8
|
# @thread_safety Runtime collaborators synchronize mutable state.
|
|
9
9
|
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
10
10
|
# @example
|
|
@@ -19,6 +19,10 @@ module Chronos
|
|
|
19
19
|
def initialize(config, options = {})
|
|
20
20
|
@config = config
|
|
21
21
|
@logger = options[:logger] || Internal::SafeLogger.new(config.logger)
|
|
22
|
+
@context_store = options[:context_store] || build_context_store(config.context_store)
|
|
23
|
+
unless Ports::ContextStore.compatible?(@context_store)
|
|
24
|
+
raise ArgumentError, "context store does not implement the Chronos context-store port"
|
|
25
|
+
end
|
|
22
26
|
@transport = options[:transport] || Adapters::NetHttpTransport.new(config, @logger)
|
|
23
27
|
unless Ports::Transport.compatible?(@transport)
|
|
24
28
|
raise ArgumentError, "transport does not implement the Chronos transport port"
|
|
@@ -32,15 +36,56 @@ module Chronos
|
|
|
32
36
|
@logger,
|
|
33
37
|
pipeline_options
|
|
34
38
|
)
|
|
35
|
-
|
|
39
|
+
initialize_capture(options)
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
def notify(exception, context = {})
|
|
39
|
-
@capture.call(exception, context)
|
|
43
|
+
@capture.call(exception, context_for_capture(context))
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
def notify_sync(exception, context = {})
|
|
43
|
-
@capture.call_sync(exception, context)
|
|
47
|
+
@capture.call_sync(exception, context_for_capture(context))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_context(context = {}, &block)
|
|
51
|
+
@context_store.with_context(context, &block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_breadcrumb(attributes = {})
|
|
55
|
+
context = @context_store.get
|
|
56
|
+
buffer = context[:__chronos_breadcrumbs]
|
|
57
|
+
buffer ||= Core::BreadcrumbBuffer.new(@config.breadcrumb_capacity, @config.breadcrumb_max_bytes)
|
|
58
|
+
buffer.add(attributes)
|
|
59
|
+
@context_store.set(context.merge(:__chronos_breadcrumbs => buffer))
|
|
60
|
+
true
|
|
61
|
+
rescue StandardError => error
|
|
62
|
+
@logger.warn("Chronos breadcrumb failed: #{error.class}")
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def record_event(event_type, payload = {}, context = {})
|
|
67
|
+
@telemetry.call(event_type, payload, telemetry_context(context))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def notify_once(exception, context = {})
|
|
71
|
+
execution = @context_store.get
|
|
72
|
+
captured = execution[:__chronos_captured_exceptions] || {}
|
|
73
|
+
keys = [[:object, exception.object_id], [:message, exception.message.to_s]]
|
|
74
|
+
return false if keys.any? { |key| captured[key] }
|
|
75
|
+
|
|
76
|
+
keys.each { |key| captured[key] = true }
|
|
77
|
+
@context_store.set(execution.merge(:__chronos_captured_exceptions => captured))
|
|
78
|
+
notify(exception, context)
|
|
79
|
+
rescue StandardError
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def rails_integration_options(environment = nil, console = false)
|
|
84
|
+
current_environment = (environment || @config.environment).to_s
|
|
85
|
+
enabled = @config.rails_enabled
|
|
86
|
+
enabled = false if console && !@config.rails_capture_in_console
|
|
87
|
+
enabled = false if current_environment == "test" && !@config.rails_capture_in_test
|
|
88
|
+
{:enabled => enabled, :include_user_agent => @config.rails_capture_user_agent}
|
|
44
89
|
end
|
|
45
90
|
|
|
46
91
|
def flush(timeout = DEFAULT_FLUSH_TIMEOUT)
|
|
@@ -61,5 +106,51 @@ module Chronos
|
|
|
61
106
|
details = @delivery_pipeline.diagnostics
|
|
62
107
|
details[:queue].merge(details)
|
|
63
108
|
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def initialize_capture(options)
|
|
113
|
+
@capture = options[:capture] || Application::CaptureException.new(@config, @delivery_pipeline, @logger)
|
|
114
|
+
@telemetry = options[:telemetry] || Application::CaptureTelemetry.new(@config, @delivery_pipeline, @logger)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_context_store(strategy)
|
|
118
|
+
return Adapters::ThreadLocalContextStore.new if strategy == :thread_local
|
|
119
|
+
|
|
120
|
+
strategy
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def context_for_capture(additional)
|
|
124
|
+
merged = deep_merge(context_hash(@context_store.get), context_hash(additional))
|
|
125
|
+
merged.delete(:__chronos_captured_exceptions)
|
|
126
|
+
merged.delete("__chronos_captured_exceptions")
|
|
127
|
+
buffer = merged.delete(:__chronos_breadcrumbs) || merged.delete("__chronos_breadcrumbs")
|
|
128
|
+
if buffer.respond_to?(:to_a)
|
|
129
|
+
merged[:context] = context_hash(merged[:context]).merge("breadcrumbs" => buffer.to_a)
|
|
130
|
+
end
|
|
131
|
+
merged
|
|
132
|
+
rescue StandardError
|
|
133
|
+
context_hash(additional)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def telemetry_context(additional)
|
|
137
|
+
merged = context_for_capture(additional)
|
|
138
|
+
context = context_hash(merged.delete(:context) || merged.delete("context"))
|
|
139
|
+
parameters = merged.delete(:parameters) || merged.delete("parameters")
|
|
140
|
+
user = merged.delete(:user) || merged.delete("user")
|
|
141
|
+
context["parameters"] = parameters if parameters.is_a?(Hash) && !parameters.empty?
|
|
142
|
+
context["user"] = user if user.is_a?(Hash) && !user.empty?
|
|
143
|
+
context.merge(merged)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def deep_merge(left, right)
|
|
147
|
+
left.merge(right) do |_key, old_value, new_value|
|
|
148
|
+
old_value.is_a?(Hash) && new_value.is_a?(Hash) ? deep_merge(old_value, new_value) : new_value
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def context_hash(value)
|
|
153
|
+
value.is_a?(Hash) ? value : {}
|
|
154
|
+
end
|
|
64
155
|
end
|
|
65
156
|
end
|
|
@@ -2,9 +2,9 @@ module Chronos
|
|
|
2
2
|
module Application
|
|
3
3
|
# Orchestrates exception normalization, serialization, queueing, and delivery.
|
|
4
4
|
#
|
|
5
|
-
# @responsibility Execute the
|
|
5
|
+
# @responsibility Execute the exception capture pipeline for manual and integration callers.
|
|
6
6
|
# @motivation Keep the public facade and transport adapters free of use-case logic.
|
|
7
|
-
# @limits
|
|
7
|
+
# @limits Framework hooks remain separate integration adapters.
|
|
8
8
|
# @collaborators NoticeBuilder, PayloadSerializer, and DeliveryPipeline.
|
|
9
9
|
# @thread_safety Collaborators are immutable or synchronized; calls may run concurrently.
|
|
10
10
|
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Application
|
|
3
|
+
# Coordinates bounded non-exception telemetry capture.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Build, sanitize, serialize, and enqueue integration events.
|
|
6
|
+
# @motivation Keep Rails notification policy outside transport and domain objects.
|
|
7
|
+
# @limits It handles request, query, job, and cache events only.
|
|
8
|
+
# @collaborators TelemetryEvent, TelemetrySerializer, and DeliveryPipeline.
|
|
9
|
+
# @thread_safety Calls own event state and may execute concurrently.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of Rails.
|
|
11
|
+
# @example
|
|
12
|
+
# capture.call("query", {"duration_ms" => 3.2})
|
|
13
|
+
# @errors Internal StandardError failures are contained and logged.
|
|
14
|
+
# @performance Local normalization is bounded before asynchronous queue insertion.
|
|
15
|
+
class CaptureTelemetry
|
|
16
|
+
def initialize(config, delivery_pipeline, logger = nil, options = {})
|
|
17
|
+
@config = config
|
|
18
|
+
@delivery_pipeline = delivery_pipeline
|
|
19
|
+
@logger = logger || Internal::SafeLogger.new(config.logger)
|
|
20
|
+
@serializer = options[:serializer] || Core::TelemetrySerializer.new(
|
|
21
|
+
config, nil, :max_payload_size => proc { @delivery_pipeline.max_payload_size }
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(event_type, payload = {}, context = {})
|
|
26
|
+
return false unless @config.enabled_for_environment?
|
|
27
|
+
return false unless @delivery_pipeline.capture_allowed?(event_type)
|
|
28
|
+
|
|
29
|
+
event = Core::TelemetryEvent.new(event_type, payload, context)
|
|
30
|
+
@delivery_pipeline.enqueue(@serializer.call(event))
|
|
31
|
+
rescue StandardError => error
|
|
32
|
+
@logger.warn("Chronos telemetry capture failed: #{error.class}")
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -13,7 +13,7 @@ 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 = ["exception"].freeze
|
|
16
|
+
SUPPORTED_EVENT_TYPES = ["exception", "request", "query", "job", "cache"].freeze
|
|
17
17
|
MAX_IGNORED_FINGERPRINTS = 100
|
|
18
18
|
MAX_FINGERPRINT_BYTES = 256
|
|
19
19
|
MIN_PAYLOAD_SIZE = 256
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
# Immutable configuration shared by all runtime components.
|
|
3
|
+
#
|
|
4
|
+
# @responsibility Expose validated settings without mutable containers.
|
|
5
|
+
# @motivation Keep capture behavior stable while multiple threads run.
|
|
6
|
+
# @limits It cannot be edited after creation.
|
|
7
|
+
# @collaborators Configuration and runtime services.
|
|
8
|
+
# @thread_safety Safe to share between threads after construction.
|
|
9
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
10
|
+
# @example
|
|
11
|
+
# snapshot.enabled_for_environment? #=> true
|
|
12
|
+
# @errors Construction occurs only after Configuration validation.
|
|
13
|
+
# @performance Deep freezing is paid once during configuration.
|
|
14
|
+
Configuration::Snapshot = Class.new do
|
|
15
|
+
attr_reader(*Configuration::ATTRIBUTES)
|
|
16
|
+
|
|
17
|
+
def initialize(values)
|
|
18
|
+
Configuration::ATTRIBUTES.each do |attribute|
|
|
19
|
+
value = values[attribute]
|
|
20
|
+
deep_freeze(value)
|
|
21
|
+
instance_variable_set("@#{attribute}", value)
|
|
22
|
+
end
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def enabled_for_environment?
|
|
27
|
+
enabled && !ignored_environments.map(&:to_s).include?(environment.to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private # rubocop:disable Layout/AccessModifierIndentation
|
|
31
|
+
|
|
32
|
+
def deep_freeze(value)
|
|
33
|
+
return value if value.respond_to?(:call) || context_store?(value)
|
|
34
|
+
|
|
35
|
+
case value
|
|
36
|
+
when Hash
|
|
37
|
+
value.each do |key, child|
|
|
38
|
+
deep_freeze(key)
|
|
39
|
+
deep_freeze(child)
|
|
40
|
+
end
|
|
41
|
+
when Array
|
|
42
|
+
value.each { |child| deep_freeze(child) }
|
|
43
|
+
end
|
|
44
|
+
value.freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def context_store?(value)
|
|
48
|
+
Configuration::CONTEXT_STORE_METHODS.all? { |method_name| value.respond_to?(method_name) }
|
|
49
|
+
rescue StandardError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Internal
|
|
3
|
+
# Shared validation predicates for mutable Chronos configuration.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Validate resilience, privacy, context, Rails, and endpoint settings.
|
|
6
|
+
# @motivation Keep Configuration focused on option storage and snapshot creation.
|
|
7
|
+
# @limits It is mixed into Configuration and is not a public extension API.
|
|
8
|
+
# @collaborators Chronos::Configuration values.
|
|
9
|
+
# @thread_safety Validation reads one configuration instance without shared state.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @errors Invalid values become messages; malformed URLs are contained.
|
|
12
|
+
module ConfigurationValidation
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def resilience_errors
|
|
16
|
+
retry_errors + backlog_and_circuit_errors + remote_policy_errors
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def retry_errors
|
|
20
|
+
errors = []
|
|
21
|
+
errors << "max_retries must be a non-negative integer" unless non_negative_integer?(max_retries)
|
|
22
|
+
errors << "retry_base_interval must be greater than zero" unless positive_number?(retry_base_interval)
|
|
23
|
+
errors << "retry_max_interval must be greater than zero" unless positive_number?(retry_max_interval)
|
|
24
|
+
if positive_number?(retry_base_interval) && positive_number?(retry_max_interval) &&
|
|
25
|
+
retry_max_interval < retry_base_interval
|
|
26
|
+
errors << "retry_max_interval must be greater than or equal to retry_base_interval"
|
|
27
|
+
end
|
|
28
|
+
errors << "retry_jitter must be between zero and one" unless rate?(retry_jitter)
|
|
29
|
+
errors
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def backlog_and_circuit_errors
|
|
33
|
+
errors = []
|
|
34
|
+
errors << "backlog_size must be a non-negative integer" unless non_negative_integer?(backlog_size)
|
|
35
|
+
unless positive_integer?(circuit_failure_threshold)
|
|
36
|
+
errors << "circuit_failure_threshold must be a positive integer"
|
|
37
|
+
end
|
|
38
|
+
errors << "circuit_reset_timeout must be greater than zero" unless positive_number?(circuit_reset_timeout)
|
|
39
|
+
errors
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def remote_policy_errors
|
|
43
|
+
errors = []
|
|
44
|
+
errors << "remote_configuration must be true or false" unless [true, false].include?(remote_configuration)
|
|
45
|
+
errors << "remote_config_max_bytes must be a positive integer" unless positive_integer?(remote_config_max_bytes)
|
|
46
|
+
errors << "sampling_rate must be between zero and one" unless rate?(sampling_rate)
|
|
47
|
+
unless enabled_event_types.is_a?(Array) && enabled_event_types.all? { |value| value.is_a?(String) }
|
|
48
|
+
errors << "enabled_event_types must contain only String values"
|
|
49
|
+
end
|
|
50
|
+
errors << "max_remote_send_interval must be greater than zero" unless positive_number?(max_remote_send_interval)
|
|
51
|
+
errors
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def privacy_errors
|
|
55
|
+
errors = []
|
|
56
|
+
errors << "blocklist_keys must be an array" unless blocklist_keys.is_a?(Array)
|
|
57
|
+
errors << "allowlist_keys must be an array" unless allowlist_keys.is_a?(Array)
|
|
58
|
+
errors << "hash_keys must be an array" unless hash_keys.is_a?(Array)
|
|
59
|
+
errors.concat(matcher_errors("blocklist_keys", blocklist_keys))
|
|
60
|
+
errors.concat(matcher_errors("allowlist_keys", allowlist_keys))
|
|
61
|
+
errors.concat(matcher_errors("hash_keys", hash_keys))
|
|
62
|
+
errors.concat(filter_errors)
|
|
63
|
+
errors.concat(anonymization_errors)
|
|
64
|
+
errors
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def context_errors
|
|
68
|
+
errors = []
|
|
69
|
+
unless context_store == :thread_local || compatible_context_store?
|
|
70
|
+
errors << "context_store must be :thread_local or implement get, set, clear, and with_context"
|
|
71
|
+
end
|
|
72
|
+
errors << "breadcrumb_capacity must be a positive integer" unless positive_integer?(breadcrumb_capacity)
|
|
73
|
+
unless breadcrumb_max_bytes.is_a?(Integer) && breadcrumb_max_bytes >= 128
|
|
74
|
+
errors << "breadcrumb_max_bytes must be an integer greater than or equal to 128"
|
|
75
|
+
end
|
|
76
|
+
rails_boolean_errors.each { |error| errors << error }
|
|
77
|
+
errors
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def compatible_context_store?
|
|
81
|
+
self.class::CONTEXT_STORE_METHODS.all? { |method_name| context_store.respond_to?(method_name) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def rails_boolean_errors
|
|
85
|
+
names = [:rails_enabled, :rails_capture_in_console, :rails_capture_in_test, :rails_capture_user_agent]
|
|
86
|
+
names.reject { |name| [true, false].include?(public_send(name)) }.map do |name|
|
|
87
|
+
"#{name} must be true or false"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def filter_errors
|
|
92
|
+
return ["filters must be an array"] unless filters.is_a?(Array)
|
|
93
|
+
return [] if filters.all? { |filter| filter.respond_to?(:call) }
|
|
94
|
+
|
|
95
|
+
["filters must contain only callable objects"]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def anonymization_errors
|
|
99
|
+
[true, false].include?(anonymize_ip) ? [] : ["anonymize_ip must be true or false"]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def matcher_errors(name, values)
|
|
103
|
+
return [] unless values.is_a?(Array)
|
|
104
|
+
return [] if values.all? { |value| value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Regexp) }
|
|
105
|
+
|
|
106
|
+
["#{name} must contain only String, Symbol, or Regexp values"]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def host_errors
|
|
110
|
+
return ["host is required"] if blank?(host)
|
|
111
|
+
|
|
112
|
+
uri = URI.parse(host.to_s)
|
|
113
|
+
return ["host must be an absolute HTTP or HTTPS URL"] unless uri.host && %w(http https).include?(uri.scheme)
|
|
114
|
+
return ["host must use HTTPS unless ssl_verify is explicitly disabled"] if uri.scheme != "https" && ssl_verify
|
|
115
|
+
|
|
116
|
+
[]
|
|
117
|
+
rescue URI::InvalidURIError
|
|
118
|
+
["host must be a valid URL"]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|