chronos-ruby 0.1.0.pre.2 → 0.3.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 +56 -0
- data/README.md +51 -21
- 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/architecture.md +5 -4
- data/docs/compatibility.md +6 -6
- data/docs/configuration.md +27 -1
- data/docs/data-collected.md +10 -7
- data/docs/examples/plain-ruby.md +14 -0
- data/docs/modules/async-queue.md +6 -2
- data/docs/modules/notice-pipeline.md +6 -3
- data/docs/modules/remote-configuration.md +54 -0
- data/docs/modules/retry-backlog.md +60 -0
- data/docs/modules/sanitization.md +19 -0
- data/docs/modules/serialization.md +9 -0
- data/docs/modules/transport.md +5 -3
- data/docs/performance.md +39 -2
- data/docs/privacy-lgpd.md +80 -10
- data/docs/troubleshooting.md +2 -2
- data/lib/chronos/adapters/net_http_transport.rb +21 -3
- data/lib/chronos/agent.rb +16 -8
- 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 +178 -21
- data/lib/chronos/core/notice.rb +1 -1
- data/lib/chronos/core/payload_serializer.rb +39 -89
- data/lib/chronos/core/runtime_info.rb +1 -1
- data/lib/chronos/core/safe_serializer.rb +119 -0
- data/lib/chronos/core/sanitizer.rb +161 -0
- data/lib/chronos/core/sensitive_value_filter.rb +118 -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/transport.rb +4 -3
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +9 -1
- metadata +30 -11
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Core
|
|
5
|
+
# Removes secrets and personal data from event values before serialization.
|
|
6
|
+
#
|
|
7
|
+
# @responsibility Recursively redact configured keys and recognized sensitive strings.
|
|
8
|
+
# @motivation Make privacy protection the default before transport or future persistence.
|
|
9
|
+
# @limits Detection is conservative and cannot replace an application data-governance review.
|
|
10
|
+
# @collaborators Configuration::Snapshot and application-provided filters.
|
|
11
|
+
# @thread_safety Instances are immutable; configured filters must be thread-safe.
|
|
12
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6; independent of frameworks.
|
|
13
|
+
# @example
|
|
14
|
+
# sanitizer.call("password" => "secret") #=> {"password"=>"[FILTERED]"}
|
|
15
|
+
# @errors Unsafe values and failing custom filters become redacted placeholders.
|
|
16
|
+
# @performance Work is linear in the structurally bounded event tree.
|
|
17
|
+
class Sanitizer
|
|
18
|
+
FILTERED = SensitiveValueFilter::FILTERED
|
|
19
|
+
MAX_DEPTH = 10
|
|
20
|
+
MAX_KEYS = 100
|
|
21
|
+
MAX_ITEMS = 100
|
|
22
|
+
MAX_NODES = 2_000
|
|
23
|
+
|
|
24
|
+
def initialize(config)
|
|
25
|
+
@blocklist_names, @blocklist_patterns = compile_matchers(config.blocklist_keys)
|
|
26
|
+
@allowlist_names, @allowlist_patterns = compile_matchers(config.allowlist_keys)
|
|
27
|
+
@hash_names, @hash_patterns = compile_matchers(config.hash_keys)
|
|
28
|
+
@filters = config.filters
|
|
29
|
+
@sensitive_value_filter = SensitiveValueFilter.new(config.anonymize_ip)
|
|
30
|
+
@hash_scope = config.project_id.to_s
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call(value)
|
|
35
|
+
sanitize_value(value, nil, true, 0, :nodes => 0, :seen => {})
|
|
36
|
+
rescue StandardError
|
|
37
|
+
FILTERED
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def sanitize_value(value, key, run_filters, depth, state)
|
|
43
|
+
state[:nodes] += 1
|
|
44
|
+
return "<node limit reached>" if state[:nodes] > MAX_NODES
|
|
45
|
+
return "<maximum depth reached>" if depth >= MAX_DEPTH
|
|
46
|
+
|
|
47
|
+
result = sanitize_type(value, depth, state)
|
|
48
|
+
run_filters && key && !@filters.empty? ? filter_value(key, result, depth, state) : result
|
|
49
|
+
rescue StandardError
|
|
50
|
+
FILTERED
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sanitize_type(value, depth, state)
|
|
54
|
+
return sanitize_hash(value, depth, state) if value.is_a?(Hash)
|
|
55
|
+
return sanitize_array(value, depth, state) if value.is_a?(Array)
|
|
56
|
+
return @sensitive_value_filter.call(value) if value.is_a?(String)
|
|
57
|
+
|
|
58
|
+
value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def sanitize_hash(value, depth, state)
|
|
62
|
+
return "<circular reference>" if state[:seen][value.object_id]
|
|
63
|
+
|
|
64
|
+
state[:seen][value.object_id] = true
|
|
65
|
+
result = {}
|
|
66
|
+
value.to_a.first(MAX_KEYS).each do |key, child|
|
|
67
|
+
result[key] = sanitize_hash_value(key, child, depth, state)
|
|
68
|
+
end
|
|
69
|
+
state[:seen].delete(value.object_id)
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def sanitize_hash_value(key, value, depth, state)
|
|
74
|
+
return FILTERED if blocked_key?(key) && !allowed_key?(key)
|
|
75
|
+
return hash_value(value) if hashed_key?(key)
|
|
76
|
+
|
|
77
|
+
sanitize_value(value, key, true, depth + 1, state)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def sanitize_array(value, depth, state)
|
|
81
|
+
return "<circular reference>" if state[:seen][value.object_id]
|
|
82
|
+
|
|
83
|
+
state[:seen][value.object_id] = true
|
|
84
|
+
result = value.first(MAX_ITEMS).map do |child|
|
|
85
|
+
sanitize_value(child, nil, true, depth + 1, state)
|
|
86
|
+
end
|
|
87
|
+
state[:seen].delete(value.object_id)
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def blocked_key?(key)
|
|
92
|
+
matches_any?(@blocklist_names, @blocklist_patterns, key, true)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def allowed_key?(key)
|
|
96
|
+
matches_any?(@allowlist_names, @allowlist_patterns, key, false)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def hashed_key?(key)
|
|
100
|
+
matches_any?(@hash_names, @hash_patterns, key, false)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def compile_matchers(matchers)
|
|
104
|
+
names = []
|
|
105
|
+
patterns = []
|
|
106
|
+
matchers.each do |matcher|
|
|
107
|
+
matcher.is_a?(Regexp) ? patterns << matcher : names << normalize_key(matcher)
|
|
108
|
+
end
|
|
109
|
+
[names.freeze, patterns.freeze]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def matches_any?(names, patterns, key, fallback)
|
|
113
|
+
candidate = safe_key(key)
|
|
114
|
+
key_name = normalize_key(candidate)
|
|
115
|
+
name_match = names.any? { |name| key_name == name || key_name.end_with?("_#{name}") }
|
|
116
|
+
name_match || patterns.any? { |pattern| pattern =~ candidate }
|
|
117
|
+
rescue StandardError
|
|
118
|
+
fallback
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def normalize_key(value)
|
|
122
|
+
value.to_s.downcase.gsub(/[^a-z0-9]+/, "_").sub(/\A_+/, "").sub(/_+\z/, "")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def safe_key(value)
|
|
126
|
+
return value if value.is_a?(String)
|
|
127
|
+
return value.to_s if value.is_a?(Symbol) || value.is_a?(Numeric)
|
|
128
|
+
|
|
129
|
+
"<#{safe_class_name(value)}>"
|
|
130
|
+
rescue StandardError
|
|
131
|
+
"<unreadable key>"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def hash_value(value)
|
|
135
|
+
scalar = case value
|
|
136
|
+
when String, Symbol, Numeric
|
|
137
|
+
value.to_s
|
|
138
|
+
else
|
|
139
|
+
return FILTERED
|
|
140
|
+
end
|
|
141
|
+
digest = Digest::SHA256.hexdigest("chronos:#{@hash_scope}:#{scalar}")
|
|
142
|
+
"[HASHED_SHA256:#{digest}]"
|
|
143
|
+
rescue StandardError
|
|
144
|
+
FILTERED
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def filter_value(key, value, depth, state)
|
|
148
|
+
filtered = @filters.inject(value) { |current, filter| filter.call(key, current) }
|
|
149
|
+
sanitize_value(filtered, nil, false, depth, state)
|
|
150
|
+
rescue StandardError
|
|
151
|
+
FILTERED
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def safe_class_name(value)
|
|
155
|
+
value.class.name.to_s
|
|
156
|
+
rescue StandardError
|
|
157
|
+
"Object"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Core
|
|
3
|
+
# Redacts recognized secret and personal-data patterns inside strings.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Detect bounded sensitive value formats independently of hash-key policy.
|
|
6
|
+
# @motivation Protect exception messages and allowlisted fields where no sensitive key exists.
|
|
7
|
+
# @limits Detection is conservative, validates documents and cards, and does not cover every format.
|
|
8
|
+
# @collaborators Sanitizer.
|
|
9
|
+
# @thread_safety Immutable after construction and safe to share between capture calls.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @example
|
|
12
|
+
# filter.call("person@example.com") #=> "[FILTERED_EMAIL]"
|
|
13
|
+
# @errors Invalid encodings become a filtered placeholder.
|
|
14
|
+
# @performance Applies a fixed set of regular expressions to each bounded string.
|
|
15
|
+
class SensitiveValueFilter
|
|
16
|
+
FILTERED = "[FILTERED]".freeze
|
|
17
|
+
FILTERED_EMAIL = "[FILTERED_EMAIL]".freeze
|
|
18
|
+
FILTERED_DOCUMENT = "[FILTERED_DOCUMENT]".freeze
|
|
19
|
+
FILTERED_CARD = "[FILTERED_CARD]".freeze
|
|
20
|
+
FILTERED_JWT = "[FILTERED_JWT]".freeze
|
|
21
|
+
|
|
22
|
+
EMAIL_PATTERN = /\b[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}\b/i
|
|
23
|
+
JWT_PATTERN = /\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/
|
|
24
|
+
BEARER_PATTERN = /\bBearer\s+[^\s,;]+/i
|
|
25
|
+
CPF_PATTERN = /\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b/
|
|
26
|
+
CNPJ_PATTERN = %r{\b\d{2}\.?\d{3}\.?\d{3}/?\d{4}-?\d{2}\b}
|
|
27
|
+
IPV4_PATTERN = /\b(?:\d{1,3}\.){3}\d{1,3}\b/
|
|
28
|
+
CARD_PATTERN = /\b(?:\d[ -]?){12,18}\d\b/
|
|
29
|
+
|
|
30
|
+
def initialize(anonymize_ip)
|
|
31
|
+
@anonymize_ip = anonymize_ip
|
|
32
|
+
freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call(value)
|
|
36
|
+
result = safe_string(value)
|
|
37
|
+
result = result.gsub(BEARER_PATTERN, "Bearer #{FILTERED}") if result =~ BEARER_PATTERN
|
|
38
|
+
result = result.gsub(JWT_PATTERN, FILTERED_JWT) if result.count(".") >= 2
|
|
39
|
+
result = result.gsub(EMAIL_PATTERN, FILTERED_EMAIL) if result.include?("@")
|
|
40
|
+
result = redact_numeric_values(result) if result.count("0-9") >= 11
|
|
41
|
+
@anonymize_ip && result.count(".") >= 3 ? anonymize_ipv4(result) : result
|
|
42
|
+
rescue StandardError
|
|
43
|
+
FILTERED
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def redact_numeric_values(value)
|
|
49
|
+
result = redact_documents(value)
|
|
50
|
+
value.count("0-9") >= 13 ? redact_cards(result) : result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def redact_cards(value)
|
|
54
|
+
value.gsub(CARD_PATTERN) do |candidate|
|
|
55
|
+
luhn_valid?(candidate) ? FILTERED_CARD : candidate
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def redact_documents(value)
|
|
60
|
+
result = value.gsub(CNPJ_PATTERN) do |candidate|
|
|
61
|
+
valid_cnpj?(candidate) ? FILTERED_DOCUMENT : candidate
|
|
62
|
+
end
|
|
63
|
+
result.gsub(CPF_PATTERN) do |candidate|
|
|
64
|
+
valid_cpf?(candidate) ? FILTERED_DOCUMENT : candidate
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def valid_cpf?(candidate)
|
|
69
|
+
digits = candidate.gsub(/\D/, "").chars.map(&:to_i)
|
|
70
|
+
return false if digits.uniq.size == 1
|
|
71
|
+
|
|
72
|
+
digits[9] == document_digit(digits.first(9), (2..10).to_a.reverse) &&
|
|
73
|
+
digits[10] == document_digit(digits.first(10), (2..11).to_a.reverse)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def valid_cnpj?(candidate)
|
|
77
|
+
digits = candidate.gsub(/\D/, "").chars.map(&:to_i)
|
|
78
|
+
return false if digits.uniq.size == 1
|
|
79
|
+
|
|
80
|
+
first_weights = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
|
|
81
|
+
second_weights = [6] + first_weights
|
|
82
|
+
digits[12] == document_digit(digits.first(12), first_weights) &&
|
|
83
|
+
digits[13] == document_digit(digits.first(13), second_weights)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def document_digit(digits, weights)
|
|
87
|
+
remainder = digits.each_with_index.inject(0) do |sum, (digit, index)|
|
|
88
|
+
sum + (digit * weights[index])
|
|
89
|
+
end % 11
|
|
90
|
+
remainder < 2 ? 0 : 11 - remainder
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def luhn_valid?(candidate)
|
|
94
|
+
digits = candidate.gsub(/\D/, "").chars.map(&:to_i)
|
|
95
|
+
return false unless digits.length.between?(13, 19)
|
|
96
|
+
|
|
97
|
+
sum = digits.reverse.each_with_index.inject(0) do |total, (digit, index)|
|
|
98
|
+
doubled = index.odd? ? digit * 2 : digit
|
|
99
|
+
total + (doubled > 9 ? doubled - 9 : doubled)
|
|
100
|
+
end
|
|
101
|
+
(sum % 10).zero?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def anonymize_ipv4(value)
|
|
105
|
+
value.gsub(IPV4_PATTERN) do |candidate|
|
|
106
|
+
octets = candidate.split(".").map(&:to_i)
|
|
107
|
+
octets.all? { |octet| octet.between?(0, 255) } ? "#{octets[0, 3].join('.')}.0" : candidate
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def safe_string(value)
|
|
112
|
+
value.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "�")
|
|
113
|
+
rescue StandardError
|
|
114
|
+
FILTERED
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
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
|
|
@@ -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
|
@@ -11,12 +11,20 @@ require "chronos/core/backtrace_parser"
|
|
|
11
11
|
require "chronos/core/exception_cause_collector"
|
|
12
12
|
require "chronos/core/runtime_info"
|
|
13
13
|
require "chronos/core/notice_builder"
|
|
14
|
+
require "chronos/core/sensitive_value_filter"
|
|
15
|
+
require "chronos/core/sanitizer"
|
|
16
|
+
require "chronos/core/safe_serializer"
|
|
14
17
|
require "chronos/core/payload_serializer"
|
|
15
18
|
require "chronos/ports/transport"
|
|
16
19
|
require "chronos/internal/safe_logger"
|
|
17
20
|
require "chronos/internal/bounded_queue"
|
|
21
|
+
require "chronos/internal/memory_backlog"
|
|
18
22
|
require "chronos/internal/worker_pool"
|
|
19
23
|
require "chronos/adapters/net_http_transport"
|
|
24
|
+
require "chronos/application/retry_policy"
|
|
25
|
+
require "chronos/application/circuit_breaker"
|
|
26
|
+
require "chronos/application/remote_configuration"
|
|
27
|
+
require "chronos/application/delivery_pipeline"
|
|
20
28
|
require "chronos/application/capture_exception"
|
|
21
29
|
require "chronos/agent"
|
|
22
30
|
|
|
@@ -24,7 +32,7 @@ require "chronos/agent"
|
|
|
24
32
|
#
|
|
25
33
|
# @responsibility Configure the agent and expose its small lifecycle API.
|
|
26
34
|
# @motivation Give applications a stable entry point while internals evolve.
|
|
27
|
-
# @limits Version 0.
|
|
35
|
+
# @limits Version 0.3 captures Ruby exceptions manually; integrations arrive later.
|
|
28
36
|
# @collaborators Configuration and Agent.
|
|
29
37
|
# @thread_safety Agent replacement and lookup are protected by a mutex.
|
|
30
38
|
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chronos-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0.pre.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antonio Jefferson
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-07-20 00:00:00.000000000 Z
|
|
@@ -30,14 +30,20 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '12.3'
|
|
34
|
+
- - ">="
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: 12.3.3
|
|
34
37
|
type: :development
|
|
35
38
|
prerelease: false
|
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
40
|
requirements:
|
|
38
41
|
- - "~>"
|
|
39
42
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
43
|
+
version: '12.3'
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 12.3.3
|
|
41
47
|
- !ruby/object:Gem::Dependency
|
|
42
48
|
name: rspec
|
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -56,16 +62,16 @@ dependencies:
|
|
|
56
62
|
name: rubocop
|
|
57
63
|
requirement: !ruby/object:Gem::Requirement
|
|
58
64
|
requirements:
|
|
59
|
-
- -
|
|
65
|
+
- - "~>"
|
|
60
66
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.
|
|
67
|
+
version: 0.49.0
|
|
62
68
|
type: :development
|
|
63
69
|
prerelease: false
|
|
64
70
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
71
|
requirements:
|
|
66
|
-
- -
|
|
72
|
+
- - "~>"
|
|
67
73
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.
|
|
74
|
+
version: 0.49.0
|
|
69
75
|
description: Base do cliente Chronos para excecoes, telemetria e observabilidade em
|
|
70
76
|
aplicacoes Ruby legadas.
|
|
71
77
|
email:
|
|
@@ -87,6 +93,7 @@ files:
|
|
|
87
93
|
- docs/adr/ADR-004-bounded-queue.md
|
|
88
94
|
- docs/adr/ADR-005-sanitize-before-backlog.md
|
|
89
95
|
- docs/adr/ADR-006-versioned-event-contract.md
|
|
96
|
+
- docs/adr/ADR-011-bounded-resilience-and-remote-control.md
|
|
90
97
|
- docs/architecture.md
|
|
91
98
|
- docs/compatibility.md
|
|
92
99
|
- docs/configuration.md
|
|
@@ -95,6 +102,10 @@ files:
|
|
|
95
102
|
- docs/modules/async-queue.md
|
|
96
103
|
- docs/modules/configuration.md
|
|
97
104
|
- docs/modules/notice-pipeline.md
|
|
105
|
+
- docs/modules/remote-configuration.md
|
|
106
|
+
- docs/modules/retry-backlog.md
|
|
107
|
+
- docs/modules/sanitization.md
|
|
108
|
+
- docs/modules/serialization.md
|
|
98
109
|
- docs/modules/transport.md
|
|
99
110
|
- docs/performance.md
|
|
100
111
|
- docs/privacy-lgpd.md
|
|
@@ -105,6 +116,10 @@ files:
|
|
|
105
116
|
- lib/chronos/agent.rb
|
|
106
117
|
- lib/chronos/application.rb
|
|
107
118
|
- lib/chronos/application/capture_exception.rb
|
|
119
|
+
- lib/chronos/application/circuit_breaker.rb
|
|
120
|
+
- lib/chronos/application/delivery_pipeline.rb
|
|
121
|
+
- lib/chronos/application/remote_configuration.rb
|
|
122
|
+
- lib/chronos/application/retry_policy.rb
|
|
108
123
|
- lib/chronos/configuration.rb
|
|
109
124
|
- lib/chronos/core.rb
|
|
110
125
|
- lib/chronos/core/backtrace_parser.rb
|
|
@@ -113,9 +128,13 @@ files:
|
|
|
113
128
|
- lib/chronos/core/notice_builder.rb
|
|
114
129
|
- lib/chronos/core/payload_serializer.rb
|
|
115
130
|
- lib/chronos/core/runtime_info.rb
|
|
131
|
+
- lib/chronos/core/safe_serializer.rb
|
|
132
|
+
- lib/chronos/core/sanitizer.rb
|
|
133
|
+
- lib/chronos/core/sensitive_value_filter.rb
|
|
116
134
|
- lib/chronos/errors.rb
|
|
117
135
|
- lib/chronos/internal.rb
|
|
118
136
|
- lib/chronos/internal/bounded_queue.rb
|
|
137
|
+
- lib/chronos/internal/memory_backlog.rb
|
|
119
138
|
- lib/chronos/internal/safe_logger.rb
|
|
120
139
|
- lib/chronos/internal/worker_pool.rb
|
|
121
140
|
- lib/chronos/ports.rb
|
|
@@ -131,7 +150,7 @@ metadata:
|
|
|
131
150
|
homepage_uri: https://github.com/antoniojefferson/chronos-ruby
|
|
132
151
|
source_code_uri: https://github.com/antoniojefferson/chronos-ruby
|
|
133
152
|
changelog_uri: https://github.com/antoniojefferson/chronos-ruby/blob/main/CHANGELOG.md
|
|
134
|
-
post_install_message:
|
|
153
|
+
post_install_message:
|
|
135
154
|
rdoc_options: []
|
|
136
155
|
require_paths:
|
|
137
156
|
- lib
|
|
@@ -149,8 +168,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
149
168
|
- !ruby/object:Gem::Version
|
|
150
169
|
version: 1.3.1
|
|
151
170
|
requirements: []
|
|
152
|
-
rubygems_version: 3.
|
|
153
|
-
signing_key:
|
|
171
|
+
rubygems_version: 3.4.22
|
|
172
|
+
signing_key:
|
|
154
173
|
specification_version: 4
|
|
155
174
|
summary: Cliente Ruby para captura de eventos do Chronos
|
|
156
175
|
test_files: []
|