chronos-ruby 0.2.0.pre.1 → 0.4.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 +48 -0
- data/README.md +60 -19
- data/contracts/rack-context-v1.schema.json +43 -0
- data/docs/adr/ADR-005-sanitize-before-backlog.md +2 -2
- data/docs/adr/ADR-011-bounded-resilience-and-remote-control.md +27 -0
- data/docs/adr/ADR-012-rack-context-isolation.md +27 -0
- data/docs/architecture.md +15 -6
- data/docs/compatibility.md +6 -6
- data/docs/configuration.md +22 -0
- data/docs/data-collected.md +8 -2
- data/docs/examples/plain-ruby.md +6 -0
- data/docs/modules/async-queue.md +6 -2
- data/docs/modules/notice-pipeline.md +2 -1
- data/docs/modules/rack-context.md +59 -0
- data/docs/modules/remote-configuration.md +54 -0
- data/docs/modules/retry-backlog.md +60 -0
- data/docs/modules/transport.md +5 -3
- data/docs/performance.md +39 -2
- data/docs/privacy-lgpd.md +10 -2
- data/docs/troubleshooting.md +10 -2
- data/lib/chronos/adapters/net_http_transport.rb +21 -3
- data/lib/chronos/adapters/thread_local_context_store.rb +52 -0
- data/lib/chronos/agent.rb +68 -11
- data/lib/chronos/application/capture_exception.rb +20 -14
- data/lib/chronos/application/circuit_breaker.rb +70 -0
- data/lib/chronos/application/delivery_pipeline.rb +248 -0
- data/lib/chronos/application/remote_configuration.rb +173 -0
- data/lib/chronos/application/retry_policy.rb +57 -0
- data/lib/chronos/configuration.rb +128 -20
- data/lib/chronos/core/breadcrumb.rb +126 -0
- data/lib/chronos/core/payload_serializer.rb +4 -2
- data/lib/chronos/integrations/rack/middleware.rb +152 -0
- data/lib/chronos/integrations/rack.rb +12 -0
- data/lib/chronos/integrations.rb +10 -0
- data/lib/chronos/internal/bounded_queue.rb +1 -1
- data/lib/chronos/internal/memory_backlog.rb +65 -0
- data/lib/chronos/internal/worker_pool.rb +5 -6
- data/lib/chronos/ports/context_store.rb +22 -0
- data/lib/chronos/ports/transport.rb +4 -3
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +26 -1
- metadata +18 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "time"
|
|
3
|
+
|
|
4
|
+
module Chronos
|
|
5
|
+
module Core
|
|
6
|
+
# Immutable, bounded diagnostic marker attached to an execution.
|
|
7
|
+
#
|
|
8
|
+
# @responsibility Normalize one breadcrumb without retaining raw application objects.
|
|
9
|
+
# @motivation Preserve useful events leading to an exception with predictable memory use.
|
|
10
|
+
# @limits It does not capture logs, SQL, bodies, or HTTP calls automatically.
|
|
11
|
+
# @collaborators SafeSerializer and BreadcrumbBuffer.
|
|
12
|
+
# @thread_safety Immutable after construction.
|
|
13
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
14
|
+
# @example
|
|
15
|
+
# breadcrumb.to_h #=> {"category"=>"custom", ...}
|
|
16
|
+
# @errors Unserializable metadata is replaced by SafeSerializer placeholders.
|
|
17
|
+
# @performance Metadata traversal has strict depth, node, item, and byte limits.
|
|
18
|
+
class Breadcrumb
|
|
19
|
+
CATEGORIES = %w(custom log request query external_http cache job).freeze
|
|
20
|
+
|
|
21
|
+
def initialize(attributes, clock = nil, max_bytes = 2048)
|
|
22
|
+
attributes = {} unless attributes.is_a?(Hash)
|
|
23
|
+
clock ||= proc { Time.now }
|
|
24
|
+
serializer = SafeSerializer.new(
|
|
25
|
+
:max_depth => 5, :max_keys => 20, :max_items => 20,
|
|
26
|
+
:max_string_bytes => 512, :max_nodes => 100
|
|
27
|
+
)
|
|
28
|
+
@data = serializer.call(build_data(attributes, clock))
|
|
29
|
+
@data = compact_data(@data, max_bytes) if JSON.generate(@data).bytesize > max_bytes
|
|
30
|
+
deep_freeze(@data)
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
@data
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_data(attributes, clock)
|
|
41
|
+
category = value(attributes, :category).to_s
|
|
42
|
+
category = "custom" unless CATEGORIES.include?(category)
|
|
43
|
+
{
|
|
44
|
+
"category" => category,
|
|
45
|
+
"message" => value(attributes, :message).to_s,
|
|
46
|
+
"metadata" => value(attributes, :metadata) || {},
|
|
47
|
+
"timestamp" => clock.call.utc.iso8601(6)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def compact_data(data, max_bytes)
|
|
52
|
+
message_limit = [max_bytes / 4, 32].max
|
|
53
|
+
compacted = {
|
|
54
|
+
"category" => data["category"],
|
|
55
|
+
"message" => SafeSerializer.new.call(data["message"], :max_string_bytes => message_limit),
|
|
56
|
+
"metadata" => {"_truncated" => true},
|
|
57
|
+
"timestamp" => data["timestamp"]
|
|
58
|
+
}
|
|
59
|
+
trim_compacted_message(compacted, max_bytes)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def trim_compacted_message(compacted, max_bytes)
|
|
63
|
+
while JSON.generate(compacted).bytesize > max_bytes && !compacted["message"].empty?
|
|
64
|
+
length = [compacted["message"].bytesize - 16, 0].max
|
|
65
|
+
compacted["message"] = SafeSerializer.new.call(
|
|
66
|
+
compacted["message"].byteslice(0, length).to_s,
|
|
67
|
+
:max_string_bytes => length
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
compacted
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def value(attributes, key)
|
|
74
|
+
attributes.key?(key) ? attributes[key] : attributes[key.to_s]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def deep_freeze(value)
|
|
78
|
+
if value.is_a?(Hash)
|
|
79
|
+
value.each do |key, child|
|
|
80
|
+
deep_freeze(key)
|
|
81
|
+
deep_freeze(child)
|
|
82
|
+
end
|
|
83
|
+
elsif value.is_a?(Array)
|
|
84
|
+
value.each { |child| deep_freeze(child) }
|
|
85
|
+
end
|
|
86
|
+
value.freeze
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Fixed-size circular collection of execution breadcrumbs.
|
|
91
|
+
#
|
|
92
|
+
# @responsibility Retain only the newest bounded breadcrumbs for one execution.
|
|
93
|
+
# @motivation Prevent long requests or noisy instrumentation from growing memory indefinitely.
|
|
94
|
+
# @limits It is process memory only and does not collect events by itself.
|
|
95
|
+
# @collaborators Breadcrumb and Agent.
|
|
96
|
+
# @thread_safety Intended for one execution thread; snapshots are immutable values.
|
|
97
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
98
|
+
# @example
|
|
99
|
+
# buffer.add(:category => "custom", :message => "started")
|
|
100
|
+
# @errors Invalid attributes are normalized into a safe breadcrumb.
|
|
101
|
+
# @performance Add is constant time and storage never exceeds capacity.
|
|
102
|
+
class BreadcrumbBuffer
|
|
103
|
+
def initialize(capacity, max_bytes = 2048)
|
|
104
|
+
raise ArgumentError, "capacity must be a positive integer" unless capacity.is_a?(Integer) && capacity > 0
|
|
105
|
+
|
|
106
|
+
@capacity = capacity
|
|
107
|
+
@max_bytes = max_bytes
|
|
108
|
+
@items = []
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def add(attributes)
|
|
112
|
+
@items.shift if @items.length >= @capacity
|
|
113
|
+
@items << Breadcrumb.new(attributes, nil, @max_bytes)
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def to_a
|
|
118
|
+
@items.map(&:to_h)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def size
|
|
122
|
+
@items.size
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -43,13 +43,15 @@ module Chronos
|
|
|
43
43
|
@clock = clock || proc { Time.now }
|
|
44
44
|
@sanitizer = options[:sanitizer] || Sanitizer.new(config)
|
|
45
45
|
@safe_serializer = options[:safe_serializer] || SafeSerializer.new
|
|
46
|
+
@max_payload_size = options[:max_payload_size] || proc { @config.max_payload_size }
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def call(notice)
|
|
49
50
|
envelope = @safe_serializer.call(@sanitizer.call(build_envelope(notice)))
|
|
50
51
|
body = JSON.generate(envelope)
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
limit = @max_payload_size.call
|
|
53
|
+
body = JSON.generate(compact_envelope(envelope)) if body.bytesize > limit
|
|
54
|
+
raise Error, "event exceeds max_payload_size" if body.bytesize > limit
|
|
53
55
|
|
|
54
56
|
SerializedEvent.new(notice.event_id, body)
|
|
55
57
|
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Integrations
|
|
5
|
+
module Rack
|
|
6
|
+
# Captures unhandled Rack exceptions while preserving Rack semantics.
|
|
7
|
+
#
|
|
8
|
+
# @responsibility Build bounded request context, notify Chronos, and re-raise the original exception.
|
|
9
|
+
# @motivation Provide automatic error capture without coupling the core to Rack.
|
|
10
|
+
# @limits It never reads rack.input, enumerates response bodies, or infers Rails routes.
|
|
11
|
+
# @collaborators Rack application and the Chronos facade or Agent.
|
|
12
|
+
# @thread_safety Shared instances keep no per-request mutable state.
|
|
13
|
+
# @compatibility Rack 1.x/2.x protocol; Ruby 2.2.10 through Ruby 2.6.
|
|
14
|
+
# @example
|
|
15
|
+
# use Chronos::Integrations::Rack::Middleware
|
|
16
|
+
# @errors Notification failures are contained; the application exception is always re-raised.
|
|
17
|
+
# @performance Adds bounded hash construction and monotonic-clock reads per request.
|
|
18
|
+
class Middleware
|
|
19
|
+
def initialize(app, options = {})
|
|
20
|
+
@app = app
|
|
21
|
+
@notifier = options[:notifier] || Chronos
|
|
22
|
+
@include_user_agent = options[:include_user_agent] || false
|
|
23
|
+
@clock = options[:clock] || proc { monotonic_time }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(env)
|
|
27
|
+
started_at = @clock.call
|
|
28
|
+
base = request_capture_context(env)
|
|
29
|
+
@notifier.with_context(base) do
|
|
30
|
+
add_request_breadcrumb("request started", base)
|
|
31
|
+
call_application(env, base, started_at)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def call_application(env, base, started_at)
|
|
38
|
+
response = @app.call(env)
|
|
39
|
+
status = response[0]
|
|
40
|
+
headers = response[1]
|
|
41
|
+
add_request_breadcrumb("request completed", dynamic_request_context(base, status, headers, started_at))
|
|
42
|
+
response
|
|
43
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
44
|
+
context = dynamic_request_context(base, 500, nil, started_at)
|
|
45
|
+
notify_safely(error, context)
|
|
46
|
+
raise
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def notify_safely(error, context)
|
|
50
|
+
@notifier.notify(error, context)
|
|
51
|
+
rescue StandardError
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def request_capture_context(env)
|
|
56
|
+
request = request_values(env)
|
|
57
|
+
{
|
|
58
|
+
:context => {"request" => request, "trace_id" => trace_id(env)},
|
|
59
|
+
:parameters => parameters(env),
|
|
60
|
+
:user => hash_value(env["chronos.user"])
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def request_values(env)
|
|
65
|
+
values = {
|
|
66
|
+
"method" => env["REQUEST_METHOD"].to_s,
|
|
67
|
+
"route" => normalized_route(env),
|
|
68
|
+
"request_id" => request_id(env),
|
|
69
|
+
"host" => (env["HTTP_HOST"] || env["SERVER_NAME"]).to_s,
|
|
70
|
+
"path" => env["PATH_INFO"].to_s,
|
|
71
|
+
"controller" => controller_action(env, "controller"),
|
|
72
|
+
"action" => controller_action(env, "action")
|
|
73
|
+
}
|
|
74
|
+
values["user_agent"] = env["HTTP_USER_AGENT"].to_s if @include_user_agent
|
|
75
|
+
values
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def dynamic_request_context(base, status, headers, started_at)
|
|
79
|
+
request = base[:context]["request"].merge(
|
|
80
|
+
"status" => status.to_i,
|
|
81
|
+
"duration_ms" => ((@clock.call - started_at) * 1000.0).round(3),
|
|
82
|
+
"response_size" => response_size(headers)
|
|
83
|
+
)
|
|
84
|
+
{:context => base[:context].merge("request" => request)}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parameters(env)
|
|
88
|
+
result = {}
|
|
89
|
+
[env["rack.request.query_hash"], env["action_dispatch.request.query_parameters"],
|
|
90
|
+
env["action_dispatch.request.path_parameters"], env["chronos.parameters"]].each do |candidate|
|
|
91
|
+
result.merge!(candidate) if candidate.is_a?(Hash)
|
|
92
|
+
end
|
|
93
|
+
result
|
|
94
|
+
rescue StandardError
|
|
95
|
+
{}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def normalized_route(env)
|
|
99
|
+
explicit = env["chronos.route"] || env["action_dispatch.route_uri_pattern"]
|
|
100
|
+
return explicit.to_s unless explicit.to_s.empty?
|
|
101
|
+
|
|
102
|
+
env["PATH_INFO"].to_s.split("/").map { |part| dynamic_segment?(part) ? ":id" : part }.join("/")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def dynamic_segment?(segment)
|
|
106
|
+
segment =~ /\A\d+\z/ || segment =~ /\A[0-9a-f]{8}-[0-9a-f-]{27,}\z/i
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def controller_action(env, key)
|
|
110
|
+
explicit = env["chronos.#{key}"]
|
|
111
|
+
paths = env["action_dispatch.request.path_parameters"]
|
|
112
|
+
explicit || (paths[key] if paths.is_a?(Hash)) || (paths[key.to_sym] if paths.is_a?(Hash))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def request_id(env)
|
|
116
|
+
env["chronos.request_id"] || env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def trace_id(env)
|
|
120
|
+
env["chronos.trace_id"] || SecureRandom.uuid
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def response_size(headers)
|
|
124
|
+
return nil unless headers.respond_to?(:each)
|
|
125
|
+
|
|
126
|
+
pair = headers.find { |key, _value| key.to_s.casecmp("content-length").zero? }
|
|
127
|
+
pair ? pair[1].to_i : nil
|
|
128
|
+
rescue StandardError
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def add_request_breadcrumb(message, context)
|
|
133
|
+
request = context[:context]["request"]
|
|
134
|
+
@notifier.add_breadcrumb(
|
|
135
|
+
:category => "request", :message => message,
|
|
136
|
+
:metadata => {"method" => request["method"], "route" => request["route"], "status" => request["status"]}
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def hash_value(value)
|
|
141
|
+
value.is_a?(Hash) ? value : {}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def monotonic_time
|
|
145
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
146
|
+
rescue StandardError
|
|
147
|
+
Time.now.to_f
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Integrations
|
|
3
|
+
# Rack protocol integrations that do not require Rack at gem load time.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Namespace Rack-compatible middleware.
|
|
6
|
+
# @motivation Permit optional use without adding a runtime Rack dependency.
|
|
7
|
+
# @limits It does not provide Rails-specific route discovery.
|
|
8
|
+
# @thread_safety Middleware instances may be shared by concurrent Rack threads.
|
|
9
|
+
# @compatibility Rack 1.x and 2.x protocol shapes on Ruby 2.2.10 through Ruby 2.6.
|
|
10
|
+
module Rack; end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
# Optional framework entry points kept outside the framework-independent core.
|
|
3
|
+
#
|
|
4
|
+
# @responsibility Namespace integration adapters such as Rack middleware.
|
|
5
|
+
# @motivation Keep framework loading optional for plain Ruby applications.
|
|
6
|
+
# @limits Integrations may depend only on documented public agent behavior.
|
|
7
|
+
# @thread_safety Each integration documents its own guarantees.
|
|
8
|
+
# @compatibility Version 0.4 supports Rack protocol behavior on Ruby 2.2.10 through 2.6.
|
|
9
|
+
module Integrations; end
|
|
10
|
+
end
|
|
@@ -4,7 +4,7 @@ module Chronos
|
|
|
4
4
|
#
|
|
5
5
|
# @responsibility Accept events up to a limit and count accepted and dropped items.
|
|
6
6
|
# @motivation Prevent telemetry bursts from growing application memory indefinitely.
|
|
7
|
-
# @limits Version 0.
|
|
7
|
+
# @limits Version 0.3 drops the newest item when full and does not persist to disk.
|
|
8
8
|
# @collaborators WorkerPool.
|
|
9
9
|
# @thread_safety Mutex and condition variable protect all mutable state.
|
|
10
10
|
# @compatibility Ruby 2.2.10 through Ruby 2.6 and fork-aware callers.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Internal
|
|
3
|
+
# Fixed-capacity retry storage for sanitized serialized events.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Retain failed deliveries in memory without unbounded growth.
|
|
6
|
+
# @motivation Allow later recovery while preserving the host application's memory limit.
|
|
7
|
+
# @limits It never writes to disk and accepts only SerializedEvent instances.
|
|
8
|
+
# @collaborators DeliveryPipeline and SerializedEvent.
|
|
9
|
+
# @thread_safety A mutex protects storage and counters.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @example
|
|
12
|
+
# backlog.push(serialized_event)
|
|
13
|
+
# @errors Invalid event types raise before entering storage.
|
|
14
|
+
# @performance Push and shift are bounded by the configured capacity.
|
|
15
|
+
class MemoryBacklog
|
|
16
|
+
attr_reader :capacity
|
|
17
|
+
|
|
18
|
+
def initialize(capacity)
|
|
19
|
+
unless capacity.is_a?(Integer) && capacity >= 0
|
|
20
|
+
raise ArgumentError, "capacity must be a non-negative integer"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@capacity = capacity
|
|
24
|
+
@items = []
|
|
25
|
+
@accepted = 0
|
|
26
|
+
@dropped = 0
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def push(event)
|
|
31
|
+
unless event.is_a?(Core::SerializedEvent)
|
|
32
|
+
raise ArgumentError, "backlog accepts only sanitized serialized events"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
if @items.length >= capacity
|
|
37
|
+
@dropped += 1
|
|
38
|
+
return false
|
|
39
|
+
end
|
|
40
|
+
@items << event
|
|
41
|
+
@accepted += 1
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def shift
|
|
47
|
+
@mutex.synchronize { @items.shift }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def size
|
|
51
|
+
@mutex.synchronize { @items.size }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def empty?
|
|
55
|
+
size.zero?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def stats
|
|
59
|
+
@mutex.synchronize do
|
|
60
|
+
{:size => @items.size, :capacity => capacity, :accepted => @accepted, :dropped => @dropped}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -4,8 +4,8 @@ module Chronos
|
|
|
4
4
|
#
|
|
5
5
|
# @responsibility Start workers on first use, deliver events, flush, and shut down predictably.
|
|
6
6
|
# @motivation Keep serialization and network delivery outside the caller's critical path.
|
|
7
|
-
# @limits
|
|
8
|
-
# @collaborators BoundedQueue,
|
|
7
|
+
# @limits Retry policy belongs to the delivery collaborator.
|
|
8
|
+
# @collaborators BoundedQueue, DeliveryPipeline, and SafeLogger.
|
|
9
9
|
# @thread_safety Internal state is synchronized and active delivery is counted.
|
|
10
10
|
# @compatibility Ruby 2.2.10 through Ruby 2.6; workers are recreated after fork.
|
|
11
11
|
# @example
|
|
@@ -16,9 +16,9 @@ module Chronos
|
|
|
16
16
|
class WorkerPool
|
|
17
17
|
POLL_INTERVAL = 0.05
|
|
18
18
|
|
|
19
|
-
def initialize(queue,
|
|
19
|
+
def initialize(queue, delivery, worker_count, logger = nil)
|
|
20
20
|
@queue = queue
|
|
21
|
-
@
|
|
21
|
+
@delivery = delivery
|
|
22
22
|
@worker_count = worker_count
|
|
23
23
|
@logger = logger || SafeLogger.new(nil)
|
|
24
24
|
@mutex = Mutex.new
|
|
@@ -59,7 +59,6 @@ module Chronos
|
|
|
59
59
|
flushed = flush_without_reopening(timeout)
|
|
60
60
|
@queue.close
|
|
61
61
|
join_workers(timeout)
|
|
62
|
-
@transport.close
|
|
63
62
|
flushed
|
|
64
63
|
rescue StandardError => error
|
|
65
64
|
@logger.warn("Chronos worker shutdown failed: #{error.class}")
|
|
@@ -87,7 +86,7 @@ module Chronos
|
|
|
87
86
|
|
|
88
87
|
increment_active
|
|
89
88
|
begin
|
|
90
|
-
@
|
|
89
|
+
@delivery.send_event(event)
|
|
91
90
|
rescue StandardError => error
|
|
92
91
|
@logger.warn("Chronos worker contained #{error.class}")
|
|
93
92
|
ensure
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Ports
|
|
3
|
+
# Conceptual storage port for execution-scoped context.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Define get, set, clear, and scoped context behavior.
|
|
6
|
+
# @motivation Let integrations isolate request state without depending on a thread implementation.
|
|
7
|
+
# @limits The port does not prescribe thread, fiber, or distributed propagation semantics.
|
|
8
|
+
# @collaborators Agent and context-store adapters.
|
|
9
|
+
# @thread_safety Implementations must isolate concurrent executions.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @example
|
|
12
|
+
# Chronos::Ports::ContextStore.compatible?(store) #=> true
|
|
13
|
+
# @errors Compatibility checks never invoke application methods.
|
|
14
|
+
module ContextStore
|
|
15
|
+
REQUIRED_METHODS = [:get, :set, :clear, :with_context].freeze
|
|
16
|
+
|
|
17
|
+
def self.compatible?(object)
|
|
18
|
+
REQUIRED_METHODS.all? { |method_name| object.respond_to?(method_name) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -4,17 +4,18 @@ module Chronos
|
|
|
4
4
|
#
|
|
5
5
|
# @responsibility Describe delivery outcome and retry classification.
|
|
6
6
|
# @motivation Keep HTTP implementation details outside the application layer.
|
|
7
|
-
# @limits It does not schedule retries
|
|
7
|
+
# @limits It classifies outcomes but does not schedule retries.
|
|
8
8
|
# @thread_safety Immutable after construction.
|
|
9
9
|
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
10
10
|
class TransportResult
|
|
11
|
-
attr_reader :status, :status_code, :retry_after, :error
|
|
11
|
+
attr_reader :status, :status_code, :retry_after, :error, :remote_configuration
|
|
12
12
|
|
|
13
13
|
def initialize(status, options = {})
|
|
14
14
|
@status = status
|
|
15
15
|
@status_code = options[:status_code]
|
|
16
16
|
@retry_after = options[:retry_after]
|
|
17
17
|
@error = options[:error]
|
|
18
|
+
@remote_configuration = options[:remote_configuration]
|
|
18
19
|
freeze
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -23,7 +24,7 @@ module Chronos
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def retryable?
|
|
26
|
-
[:rate_limited, :server_error, :network_error].include?(status)
|
|
27
|
+
[:request_timeout, :rate_limited, :server_error, :network_error].include?(status)
|
|
27
28
|
end
|
|
28
29
|
end
|
|
29
30
|
|
data/lib/chronos/version.rb
CHANGED
data/lib/chronos.rb
CHANGED
|
@@ -16,18 +16,29 @@ require "chronos/core/sanitizer"
|
|
|
16
16
|
require "chronos/core/safe_serializer"
|
|
17
17
|
require "chronos/core/payload_serializer"
|
|
18
18
|
require "chronos/ports/transport"
|
|
19
|
+
require "chronos/ports/context_store"
|
|
19
20
|
require "chronos/internal/safe_logger"
|
|
20
21
|
require "chronos/internal/bounded_queue"
|
|
22
|
+
require "chronos/internal/memory_backlog"
|
|
21
23
|
require "chronos/internal/worker_pool"
|
|
22
24
|
require "chronos/adapters/net_http_transport"
|
|
25
|
+
require "chronos/adapters/thread_local_context_store"
|
|
26
|
+
require "chronos/core/breadcrumb"
|
|
27
|
+
require "chronos/application/retry_policy"
|
|
28
|
+
require "chronos/application/circuit_breaker"
|
|
29
|
+
require "chronos/application/remote_configuration"
|
|
30
|
+
require "chronos/application/delivery_pipeline"
|
|
23
31
|
require "chronos/application/capture_exception"
|
|
24
32
|
require "chronos/agent"
|
|
33
|
+
require "chronos/integrations"
|
|
34
|
+
require "chronos/integrations/rack"
|
|
35
|
+
require "chronos/integrations/rack/middleware"
|
|
25
36
|
|
|
26
37
|
# Framework-independent public facade for the Chronos Ruby agent.
|
|
27
38
|
#
|
|
28
39
|
# @responsibility Configure the agent and expose its small lifecycle API.
|
|
29
40
|
# @motivation Give applications a stable entry point while internals evolve.
|
|
30
|
-
# @limits Version 0.
|
|
41
|
+
# @limits Version 0.4 captures Rack failures but does not install middleware automatically.
|
|
31
42
|
# @collaborators Configuration and Agent.
|
|
32
43
|
# @thread_safety Agent replacement and lookup are protected by a mutex.
|
|
33
44
|
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
@@ -70,6 +81,20 @@ module Chronos
|
|
|
70
81
|
false
|
|
71
82
|
end
|
|
72
83
|
|
|
84
|
+
def with_context(context = {})
|
|
85
|
+
agent = current_agent
|
|
86
|
+
return yield unless agent
|
|
87
|
+
|
|
88
|
+
agent.with_context(context) { yield }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_breadcrumb(attributes = {})
|
|
92
|
+
agent = current_agent
|
|
93
|
+
agent ? agent.add_breadcrumb(attributes) : false
|
|
94
|
+
rescue StandardError
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
73
98
|
def configured?
|
|
74
99
|
!current_agent.nil?
|
|
75
100
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chronos-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0.pre.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antonio Jefferson
|
|
@@ -87,12 +87,15 @@ files:
|
|
|
87
87
|
- README.md
|
|
88
88
|
- SECURITY.md
|
|
89
89
|
- contracts/event-v1.schema.json
|
|
90
|
+
- contracts/rack-context-v1.schema.json
|
|
90
91
|
- docs/adr/ADR-001-core-and-integrations.md
|
|
91
92
|
- docs/adr/ADR-002-version-lines.md
|
|
92
93
|
- docs/adr/ADR-003-net-http-legacy-transport.md
|
|
93
94
|
- docs/adr/ADR-004-bounded-queue.md
|
|
94
95
|
- docs/adr/ADR-005-sanitize-before-backlog.md
|
|
95
96
|
- docs/adr/ADR-006-versioned-event-contract.md
|
|
97
|
+
- docs/adr/ADR-011-bounded-resilience-and-remote-control.md
|
|
98
|
+
- docs/adr/ADR-012-rack-context-isolation.md
|
|
96
99
|
- docs/architecture.md
|
|
97
100
|
- docs/compatibility.md
|
|
98
101
|
- docs/configuration.md
|
|
@@ -101,6 +104,9 @@ files:
|
|
|
101
104
|
- docs/modules/async-queue.md
|
|
102
105
|
- docs/modules/configuration.md
|
|
103
106
|
- docs/modules/notice-pipeline.md
|
|
107
|
+
- docs/modules/rack-context.md
|
|
108
|
+
- docs/modules/remote-configuration.md
|
|
109
|
+
- docs/modules/retry-backlog.md
|
|
104
110
|
- docs/modules/sanitization.md
|
|
105
111
|
- docs/modules/serialization.md
|
|
106
112
|
- docs/modules/transport.md
|
|
@@ -110,12 +116,18 @@ files:
|
|
|
110
116
|
- lib/chronos.rb
|
|
111
117
|
- lib/chronos/adapters.rb
|
|
112
118
|
- lib/chronos/adapters/net_http_transport.rb
|
|
119
|
+
- lib/chronos/adapters/thread_local_context_store.rb
|
|
113
120
|
- lib/chronos/agent.rb
|
|
114
121
|
- lib/chronos/application.rb
|
|
115
122
|
- lib/chronos/application/capture_exception.rb
|
|
123
|
+
- lib/chronos/application/circuit_breaker.rb
|
|
124
|
+
- lib/chronos/application/delivery_pipeline.rb
|
|
125
|
+
- lib/chronos/application/remote_configuration.rb
|
|
126
|
+
- lib/chronos/application/retry_policy.rb
|
|
116
127
|
- lib/chronos/configuration.rb
|
|
117
128
|
- lib/chronos/core.rb
|
|
118
129
|
- lib/chronos/core/backtrace_parser.rb
|
|
130
|
+
- lib/chronos/core/breadcrumb.rb
|
|
119
131
|
- lib/chronos/core/exception_cause_collector.rb
|
|
120
132
|
- lib/chronos/core/notice.rb
|
|
121
133
|
- lib/chronos/core/notice_builder.rb
|
|
@@ -125,11 +137,16 @@ files:
|
|
|
125
137
|
- lib/chronos/core/sanitizer.rb
|
|
126
138
|
- lib/chronos/core/sensitive_value_filter.rb
|
|
127
139
|
- lib/chronos/errors.rb
|
|
140
|
+
- lib/chronos/integrations.rb
|
|
141
|
+
- lib/chronos/integrations/rack.rb
|
|
142
|
+
- lib/chronos/integrations/rack/middleware.rb
|
|
128
143
|
- lib/chronos/internal.rb
|
|
129
144
|
- lib/chronos/internal/bounded_queue.rb
|
|
145
|
+
- lib/chronos/internal/memory_backlog.rb
|
|
130
146
|
- lib/chronos/internal/safe_logger.rb
|
|
131
147
|
- lib/chronos/internal/worker_pool.rb
|
|
132
148
|
- lib/chronos/ports.rb
|
|
149
|
+
- lib/chronos/ports/context_store.rb
|
|
133
150
|
- lib/chronos/ports/transport.rb
|
|
134
151
|
- lib/chronos/ruby.rb
|
|
135
152
|
- lib/chronos/ruby/version.rb
|