julewire-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/docs/advanced-configuration.md +66 -0
- data/docs/attribute-keys.md +74 -0
- data/docs/configuration.md +327 -0
- data/docs/context-and-propagation.md +353 -0
- data/docs/contracts.md +211 -0
- data/docs/development.md +49 -0
- data/docs/extensions-and-api.md +567 -0
- data/docs/health-schema.md +104 -0
- data/docs/instrumentation-cheatsheet.md +29 -0
- data/docs/internals.md +135 -0
- data/docs/outputs-and-lifecycle.md +206 -0
- data/docs/quickstart.md +133 -0
- data/docs/record-sources.md +17 -0
- data/docs/records-and-data-policy.md +230 -0
- data/docs/security-and-wire.md +45 -0
- data/docs/tail.md +91 -0
- data/exe/julewire +6 -0
- data/julewire-core.gemspec +41 -0
- data/lib/julewire/core/cli/doctor.rb +143 -0
- data/lib/julewire/core/cli/line_helpers.rb +77 -0
- data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
- data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
- data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
- data/lib/julewire/core/cli/log_formats.rb +123 -0
- data/lib/julewire/core/cli/tail.rb +153 -0
- data/lib/julewire/core/cli/transcode.rb +105 -0
- data/lib/julewire/core/cli.rb +73 -0
- data/lib/julewire/core/configuration.rb +99 -0
- data/lib/julewire/core/context_store.rb +384 -0
- data/lib/julewire/core/destinations/chaos_output.rb +91 -0
- data/lib/julewire/core/destinations/collection.rb +177 -0
- data/lib/julewire/core/destinations/definition.rb +125 -0
- data/lib/julewire/core/destinations/destination.rb +268 -0
- data/lib/julewire/core/destinations/registry.rb +81 -0
- data/lib/julewire/core/destinations/sink.rb +35 -0
- data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
- data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
- data/lib/julewire/core/destinations/write_step.rb +119 -0
- data/lib/julewire/core/destinations.rb +33 -0
- data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
- data/lib/julewire/core/diagnostics/doctor.rb +114 -0
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
- data/lib/julewire/core/diagnostics/health.rb +144 -0
- data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
- data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
- data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
- data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
- data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
- data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
- data/lib/julewire/core/diagnostics/tail.rb +168 -0
- data/lib/julewire/core/diagnostics.rb +8 -0
- data/lib/julewire/core/error.rb +7 -0
- data/lib/julewire/core/execution/boundary.rb +106 -0
- data/lib/julewire/core/execution/handle.rb +77 -0
- data/lib/julewire/core/execution/lineage.rb +192 -0
- data/lib/julewire/core/execution/measurement_handle.rb +28 -0
- data/lib/julewire/core/execution/no_current_error.rb +9 -0
- data/lib/julewire/core/execution/scope.rb +246 -0
- data/lib/julewire/core/execution/scope_fields.rb +76 -0
- data/lib/julewire/core/execution/scope_identity.rb +71 -0
- data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
- data/lib/julewire/core/execution/summary_state.rb +206 -0
- data/lib/julewire/core/execution/view.rb +56 -0
- data/lib/julewire/core/facade_methods.rb +181 -0
- data/lib/julewire/core/fields/attribute_keys.rb +54 -0
- data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
- data/lib/julewire/core/fields/bags.rb +123 -0
- data/lib/julewire/core/fields/carry_proxy.rb +22 -0
- data/lib/julewire/core/fields/context_proxy.rb +11 -0
- data/lib/julewire/core/fields/field_set.rb +78 -0
- data/lib/julewire/core/fields/field_stack.rb +269 -0
- data/lib/julewire/core/fields/internal/deletion.rb +68 -0
- data/lib/julewire/core/fields/internal.rb +87 -0
- data/lib/julewire/core/fields/lookup.rb +35 -0
- data/lib/julewire/core/fields/section_proxy.rb +88 -0
- data/lib/julewire/core/fields/stack_set.rb +69 -0
- data/lib/julewire/core/fields/static_labels.rb +43 -0
- data/lib/julewire/core/fields/summary_proxy.rb +62 -0
- data/lib/julewire/core/integration/configurable.rb +52 -0
- data/lib/julewire/core/integration/destination_health.rb +43 -0
- data/lib/julewire/core/integration/event_subscriber.rb +62 -0
- data/lib/julewire/core/integration/facade.rb +131 -0
- data/lib/julewire/core/integration/fork_hooks.rb +79 -0
- data/lib/julewire/core/integration/health.rb +41 -0
- data/lib/julewire/core/integration/ivar_state.rb +38 -0
- data/lib/julewire/core/integration/lifecycle.rb +22 -0
- data/lib/julewire/core/integration/scoped.rb +34 -0
- data/lib/julewire/core/integration/settings.rb +92 -0
- data/lib/julewire/core/integration/subscriber_install.rb +39 -0
- data/lib/julewire/core/integration/subscription.rb +29 -0
- data/lib/julewire/core/integration/values.rb +192 -0
- data/lib/julewire/core/lifecycle_error.rb +7 -0
- data/lib/julewire/core/local_storage.rb +91 -0
- data/lib/julewire/core/processing/level_threshold.rb +53 -0
- data/lib/julewire/core/processing/match.rb +74 -0
- data/lib/julewire/core/processing/pipeline.rb +360 -0
- data/lib/julewire/core/processing/processor_chain.rb +69 -0
- data/lib/julewire/core/processing/processor_registry.rb +115 -0
- data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
- data/lib/julewire/core/processing/record_field_transform.rb +124 -0
- data/lib/julewire/core/processing/sampling.rb +109 -0
- data/lib/julewire/core/processing.rb +41 -0
- data/lib/julewire/core/propagation/carrier.rb +93 -0
- data/lib/julewire/core/propagation.rb +50 -0
- data/lib/julewire/core/records/console_formatter.rb +24 -0
- data/lib/julewire/core/records/deconstruct.rb +19 -0
- data/lib/julewire/core/records/display_message.rb +166 -0
- data/lib/julewire/core/records/draft.rb +576 -0
- data/lib/julewire/core/records/formatter.rb +14 -0
- data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
- data/lib/julewire/core/records/metadata.rb +23 -0
- data/lib/julewire/core/records/public_projection.rb +51 -0
- data/lib/julewire/core/records/raw_input.rb +41 -0
- data/lib/julewire/core/records/record.rb +175 -0
- data/lib/julewire/core/records/severity.rb +44 -0
- data/lib/julewire/core/runtime.rb +515 -0
- data/lib/julewire/core/runtime_locator.rb +20 -0
- data/lib/julewire/core/runtime_registry.rb +48 -0
- data/lib/julewire/core/runtime_state.rb +39 -0
- data/lib/julewire/core/scheduling/deadline.rb +24 -0
- data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
- data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
- data/lib/julewire/core/sentinel.rb +18 -0
- data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
- data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
- data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
- data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
- data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
- data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
- data/lib/julewire/core/serialization/exception_shape.rb +88 -0
- data/lib/julewire/core/serialization/json_encoder.rb +69 -0
- data/lib/julewire/core/serialization/serializer.rb +233 -0
- data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
- data/lib/julewire/core/serialization/text_encoder.rb +147 -0
- data/lib/julewire/core/serialization/value_copy.rb +209 -0
- data/lib/julewire/core/serialization/value_traversal.rb +150 -0
- data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
- data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
- data/lib/julewire/core/testing/chaos/destination.rb +55 -0
- data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
- data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
- data/lib/julewire/core/testing/chaos.rb +80 -0
- data/lib/julewire/core/testing/contracts/component.rb +162 -0
- data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
- data/lib/julewire/core/testing/contracts/integration.rb +166 -0
- data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
- data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
- data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
- data/lib/julewire/core/testing/contracts/wire.rb +60 -0
- data/lib/julewire/core/testing/contracts.rb +24 -0
- data/lib/julewire/core/testing/coverage.rb +58 -0
- data/lib/julewire/core/testing/test_reports.rb +78 -0
- data/lib/julewire/core/testing.rb +122 -0
- data/lib/julewire/core/validation.rb +69 -0
- data/lib/julewire/core/version.rb +7 -0
- data/lib/julewire/core.rb +80 -0
- data/lib/julewire/error.rb +5 -0
- data/lib/julewire-core.rb +3 -0
- metadata +237 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Scheduling
|
|
6
|
+
# @api integration_spi
|
|
7
|
+
class DeadlineScheduler
|
|
8
|
+
CLOCK = Process::CLOCK_MONOTONIC
|
|
9
|
+
Entry = Data.define(:deadline, :token, :callback)
|
|
10
|
+
|
|
11
|
+
def initialize(thread_name:, idle: :keep_alive)
|
|
12
|
+
@thread_name = thread_name
|
|
13
|
+
@idle = idle
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@condition = ConditionVariable.new
|
|
16
|
+
@entries = {}
|
|
17
|
+
# A heap keeps timeout scheduling cheap without non-shareable scheduler dependencies.
|
|
18
|
+
@heap = []
|
|
19
|
+
@next_token = 0
|
|
20
|
+
@generation = 0
|
|
21
|
+
@pid = Process.pid
|
|
22
|
+
@thread = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def schedule(timeout, &block)
|
|
26
|
+
raise ArgumentError, "block required" unless block
|
|
27
|
+
|
|
28
|
+
timeout = Float(timeout)
|
|
29
|
+
if timeout <= 0
|
|
30
|
+
yield
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
token = next_token
|
|
36
|
+
entry = Entry.new(monotonic_time + timeout, token, block)
|
|
37
|
+
@entries[token] = entry
|
|
38
|
+
heap_push(entry)
|
|
39
|
+
ensure_thread
|
|
40
|
+
@condition.signal
|
|
41
|
+
token
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def cancel(token)
|
|
46
|
+
return unless token
|
|
47
|
+
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
@entries.delete(token)
|
|
50
|
+
@condition.signal
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def after_fork!
|
|
55
|
+
if @pid == Process.pid
|
|
56
|
+
reset_same_process
|
|
57
|
+
else
|
|
58
|
+
reset_after_fork
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def reset_same_process
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
@generation += 1
|
|
69
|
+
@entries = {}
|
|
70
|
+
@heap = []
|
|
71
|
+
@next_token = 0
|
|
72
|
+
@thread = nil
|
|
73
|
+
@condition.broadcast
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def reset_after_fork
|
|
78
|
+
@mutex = Mutex.new
|
|
79
|
+
@condition = ConditionVariable.new
|
|
80
|
+
@entries = {}
|
|
81
|
+
@heap = []
|
|
82
|
+
@next_token = 0
|
|
83
|
+
@generation += 1
|
|
84
|
+
@pid = Process.pid
|
|
85
|
+
@thread = nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def next_token
|
|
89
|
+
@next_token += 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ensure_thread
|
|
93
|
+
return if @thread&.alive?
|
|
94
|
+
|
|
95
|
+
generation = @generation
|
|
96
|
+
@thread = Thread.new { run(generation) }
|
|
97
|
+
@thread.name = @thread_name
|
|
98
|
+
@thread.report_on_exception = false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def run(generation)
|
|
102
|
+
loop do
|
|
103
|
+
callback = next_expired_callback(generation)
|
|
104
|
+
return unless callback
|
|
105
|
+
|
|
106
|
+
safe_call(callback)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def next_expired_callback(generation)
|
|
111
|
+
@mutex.synchronize do
|
|
112
|
+
loop do
|
|
113
|
+
return unless generation == @generation
|
|
114
|
+
|
|
115
|
+
discard_cancelled_head
|
|
116
|
+
if @heap.empty?
|
|
117
|
+
return clear_thread if exit_when_idle?
|
|
118
|
+
|
|
119
|
+
@condition.wait(@mutex)
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
entry = @heap.fetch(0)
|
|
124
|
+
remaining = entry.deadline - monotonic_time
|
|
125
|
+
if remaining.positive?
|
|
126
|
+
@condition.wait(@mutex, remaining)
|
|
127
|
+
else
|
|
128
|
+
heap_pop
|
|
129
|
+
@entries.delete(entry.token)
|
|
130
|
+
return entry.callback
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def discard_cancelled_head
|
|
137
|
+
heap_pop while @heap.any? && !@entries.key?(@heap.fetch(0).token)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def clear_thread
|
|
141
|
+
@thread = nil
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def exit_when_idle?
|
|
146
|
+
@idle == :exit
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def safe_call(callback)
|
|
150
|
+
callback.call
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def monotonic_time
|
|
156
|
+
Process.clock_gettime(CLOCK)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def heap_push(entry)
|
|
160
|
+
@heap << entry
|
|
161
|
+
sift_up(@heap.length - 1)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def heap_pop
|
|
165
|
+
return @heap.pop if @heap.one?
|
|
166
|
+
|
|
167
|
+
top = @heap.fetch(0)
|
|
168
|
+
@heap[0] = @heap.pop
|
|
169
|
+
sift_down(0)
|
|
170
|
+
top
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def sift_up(index)
|
|
174
|
+
while index.positive?
|
|
175
|
+
parent = (index - 1) / 2
|
|
176
|
+
break if earlier_or_equal?(@heap.fetch(parent), @heap.fetch(index))
|
|
177
|
+
|
|
178
|
+
swap_heap(index, parent)
|
|
179
|
+
index = parent
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def sift_down(index)
|
|
184
|
+
loop do
|
|
185
|
+
left = (index * 2) + 1
|
|
186
|
+
right = left + 1
|
|
187
|
+
smallest = index
|
|
188
|
+
smallest = left if left < @heap.length && earlier_or_equal?(@heap.fetch(left), @heap.fetch(smallest))
|
|
189
|
+
smallest = right if right < @heap.length && earlier_or_equal?(@heap.fetch(right), @heap.fetch(smallest))
|
|
190
|
+
break if smallest == index
|
|
191
|
+
|
|
192
|
+
swap_heap(index, smallest)
|
|
193
|
+
index = smallest
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def earlier_or_equal?(left, right)
|
|
198
|
+
left.deadline < right.deadline || (left.deadline == right.deadline && left.token <= right.token)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def swap_heap(left, right)
|
|
202
|
+
@heap[left], @heap[right] = @heap[right], @heap[left]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Scheduling
|
|
6
|
+
module SharedScheduler
|
|
7
|
+
THREAD_NAME = "julewire-deadline-scheduler"
|
|
8
|
+
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def schedule(timeout, &)
|
|
13
|
+
current = scheduler
|
|
14
|
+
current.schedule(timeout, &)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cancel(token)
|
|
18
|
+
current = scheduler
|
|
19
|
+
current.cancel(token)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def after_fork!
|
|
23
|
+
current = @scheduler
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
current&.after_fork!
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Private testing seam for isolating process-wide scheduler state.
|
|
30
|
+
def reset_for_test!
|
|
31
|
+
@mutex.synchronize { @scheduler = nil }
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
private :reset_for_test!
|
|
38
|
+
|
|
39
|
+
def scheduler
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@scheduler ||= DeadlineScheduler.new(thread_name: THREAD_NAME)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
class Sentinel
|
|
6
|
+
attr_reader :name
|
|
7
|
+
|
|
8
|
+
def initialize(name)
|
|
9
|
+
@name = Core.normalize_name(name, name: :sentinel)
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def inspect = "#<#{self.class} #{@name}>"
|
|
14
|
+
|
|
15
|
+
def to_s = inspect
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
class BacktraceLimiter
|
|
7
|
+
class << self
|
|
8
|
+
def call(value, max_backtrace_lines:)
|
|
9
|
+
new(max_backtrace_lines: max_backtrace_lines).call(value)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(max_backtrace_lines:)
|
|
14
|
+
@max_backtrace_lines = Validation.validate_integer_limit!(
|
|
15
|
+
max_backtrace_lines,
|
|
16
|
+
name: :max_backtrace_lines
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(value)
|
|
21
|
+
@seen = {}.compare_by_identity
|
|
22
|
+
limit_backtraces(value)
|
|
23
|
+
value
|
|
24
|
+
ensure
|
|
25
|
+
@seen = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def limit_backtraces(value)
|
|
31
|
+
while value.is_a?(Hash) && !@seen.key?(value)
|
|
32
|
+
@seen[value] = true
|
|
33
|
+
limit_backtrace_field!(value)
|
|
34
|
+
value = value[:cause]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def limit_backtrace_field!(error)
|
|
39
|
+
return unless error.key?(:backtrace)
|
|
40
|
+
|
|
41
|
+
if @max_backtrace_lines.zero?
|
|
42
|
+
error.delete(:backtrace)
|
|
43
|
+
elsif error[:backtrace].is_a?(Array)
|
|
44
|
+
error[:backtrace] = error[:backtrace].first(@max_backtrace_lines)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
# @api integration_spi
|
|
7
|
+
class BoundedTransform < BoundedTraversal
|
|
8
|
+
CONTINUE = Core.sentinel(:continue)
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def call(value, **, &)
|
|
12
|
+
new(**, &).call(value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(
|
|
17
|
+
max_depth: DEFAULT_MAX_DEPTH,
|
|
18
|
+
max_string_bytes: DEFAULT_MAX_STRING_BYTES,
|
|
19
|
+
max_array_items: DEFAULT_MAX_ARRAY_ITEMS,
|
|
20
|
+
max_hash_keys: DEFAULT_MAX_HASH_KEYS,
|
|
21
|
+
max_depth_value: MAX_DEPTH_VALUE,
|
|
22
|
+
truncation_key: TRUNCATION_METADATA_KEY.to_sym,
|
|
23
|
+
track_paths: nil,
|
|
24
|
+
&block
|
|
25
|
+
)
|
|
26
|
+
super(
|
|
27
|
+
max_array_items: max_array_items,
|
|
28
|
+
max_depth: max_depth,
|
|
29
|
+
max_depth_value: max_depth_value,
|
|
30
|
+
max_hash_keys: max_hash_keys,
|
|
31
|
+
max_string_bytes: max_string_bytes,
|
|
32
|
+
truncation_key: truncation_key
|
|
33
|
+
)
|
|
34
|
+
@transform = block
|
|
35
|
+
@prepare_values = !@transform.nil?
|
|
36
|
+
@track_paths = @prepare_values && !track_paths.equal?(false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(value)
|
|
40
|
+
@root = value
|
|
41
|
+
walk(value)
|
|
42
|
+
ensure
|
|
43
|
+
@root = nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def prepare_value(value, depth, key, path)
|
|
49
|
+
transformed = @transform.call(value, key: key, path: path, original: @root, depth: depth)
|
|
50
|
+
transformed.equal?(CONTINUE) ? value : transformed
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
class BoundedTraversal
|
|
7
|
+
include ValueTraversal
|
|
8
|
+
|
|
9
|
+
MAX_DEPTH_VALUE = "[MaxDepth]"
|
|
10
|
+
TRUNCATED_SUFFIX = "...[Truncated]"
|
|
11
|
+
TRUNCATION_METADATA_KEY = "_julewire_truncation"
|
|
12
|
+
DEFAULT_MAX_DEPTH = 8
|
|
13
|
+
DEFAULT_MAX_STRING_BYTES = 16_384
|
|
14
|
+
DEFAULT_MAX_ARRAY_ITEMS = 1_000
|
|
15
|
+
DEFAULT_MAX_HASH_KEYS = 1_000
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def truncation_metadata(
|
|
19
|
+
fields,
|
|
20
|
+
max_array_items: DEFAULT_MAX_ARRAY_ITEMS,
|
|
21
|
+
max_depth: DEFAULT_MAX_DEPTH,
|
|
22
|
+
max_hash_keys: DEFAULT_MAX_HASH_KEYS,
|
|
23
|
+
max_string_bytes: DEFAULT_MAX_STRING_BYTES
|
|
24
|
+
)
|
|
25
|
+
{
|
|
26
|
+
"truncated" => true,
|
|
27
|
+
"truncated_fields" => Array(fields).uniq,
|
|
28
|
+
"limits" => {
|
|
29
|
+
"max_array_items" => max_array_items,
|
|
30
|
+
"max_depth" => max_depth,
|
|
31
|
+
"max_hash_keys" => max_hash_keys,
|
|
32
|
+
"max_string_bytes" => max_string_bytes
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(max_array_items:, max_depth:, max_depth_value:, max_hash_keys:, max_string_bytes:,
|
|
39
|
+
truncation_key:)
|
|
40
|
+
@max_array_items = Validation.validate_integer_limit!(max_array_items, name: :max_array_items)
|
|
41
|
+
@max_depth = Validation.validate_integer_limit!(max_depth, name: :max_depth, positive: true)
|
|
42
|
+
@max_depth_value = max_depth_value
|
|
43
|
+
@max_hash_keys = Validation.validate_integer_limit!(max_hash_keys, name: :max_hash_keys)
|
|
44
|
+
@max_string_bytes = Validation.validate_integer_limit!(max_string_bytes, name: :max_string_bytes)
|
|
45
|
+
@truncation_key = truncation_key
|
|
46
|
+
@last_truncated = false
|
|
47
|
+
@compact_empty = false
|
|
48
|
+
@prepare_values = false
|
|
49
|
+
@track_paths = false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def walk(value)
|
|
55
|
+
@last_truncated = false
|
|
56
|
+
traverse(value) { |root, depth| walk_value(root, depth, nil, nil) }
|
|
57
|
+
ensure
|
|
58
|
+
@last_truncated = false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def walk_value(value, depth, key, path)
|
|
62
|
+
@last_truncated = false
|
|
63
|
+
value = prepare_value(value, depth, key, path) if @prepare_values
|
|
64
|
+
return max_depth_value if depth >= @max_depth
|
|
65
|
+
return walk_container(value, depth, path) if value.is_a?(Array) || hash_like?(value)
|
|
66
|
+
|
|
67
|
+
scalar_value(value, depth, key, path)
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
@last_truncated = false
|
|
70
|
+
error_value(e)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def prepare_value(value, _depth, _key, _path) = value
|
|
74
|
+
|
|
75
|
+
def hash_like?(value) = value.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
def scalar_value(value, _depth, _key, _path)
|
|
78
|
+
value.is_a?(String) ? string_value(value) : clear_truncated(value)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Transform-stage errors must bubble so processors can fail closed.
|
|
82
|
+
def error_value(error)
|
|
83
|
+
raise error
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def walk_container(value, depth, path)
|
|
87
|
+
return circular_value if traversal_seen?(value)
|
|
88
|
+
|
|
89
|
+
with_marked_traversal_container(value) do
|
|
90
|
+
value.is_a?(Array) ? walk_array(value, depth, path) : walk_hash(value, depth, path)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def circular_value
|
|
95
|
+
@last_truncated = true
|
|
96
|
+
Core::CIRCULAR_REFERENCE
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def max_depth_value
|
|
100
|
+
mark_truncated(copy_string(@max_depth_value))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def walk_hash(value, depth, path)
|
|
104
|
+
return walk_compact_hash(value, depth, path) if @compact_empty
|
|
105
|
+
|
|
106
|
+
walk_full_hash(value, depth, path)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def walk_full_hash(value, depth, path)
|
|
110
|
+
fields = nil
|
|
111
|
+
result = {}
|
|
112
|
+
track_paths = @track_paths
|
|
113
|
+
value.each do |raw_key, item|
|
|
114
|
+
if result.length >= @max_hash_keys
|
|
115
|
+
fields = append_truncation_field(fields, "hash_keys")
|
|
116
|
+
break
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
child = walk_value(item, depth + 1, raw_key, track_paths ? path_for(path, raw_key) : nil)
|
|
120
|
+
child_truncated = consume_truncated
|
|
121
|
+
key = key_value(raw_key)
|
|
122
|
+
key_truncated = consume_truncated
|
|
123
|
+
result[key] = child
|
|
124
|
+
fields = record_hash_truncation(fields, raw_key, key, key_truncated, child_truncated)
|
|
125
|
+
end
|
|
126
|
+
finish_hash(result, fields)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def walk_compact_hash(value, depth, path)
|
|
130
|
+
fields = nil
|
|
131
|
+
result = {}
|
|
132
|
+
track_paths = @track_paths
|
|
133
|
+
value.each do |raw_key, item|
|
|
134
|
+
next if raw_omitted_value?(item)
|
|
135
|
+
|
|
136
|
+
child = walk_value(item, depth + 1, raw_key, track_paths ? path_for(path, raw_key) : nil)
|
|
137
|
+
child_truncated = consume_truncated
|
|
138
|
+
next if omitted_value?(child)
|
|
139
|
+
|
|
140
|
+
if result.length >= @max_hash_keys
|
|
141
|
+
fields = append_truncation_field(fields, "hash_keys")
|
|
142
|
+
break
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
key = key_value(raw_key)
|
|
146
|
+
key_truncated = consume_truncated
|
|
147
|
+
result[key] = child
|
|
148
|
+
fields = record_hash_truncation(fields, raw_key, key, key_truncated, child_truncated)
|
|
149
|
+
end
|
|
150
|
+
finish_hash(result, fields)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def walk_array(value, depth, path)
|
|
154
|
+
return walk_compact_array(value, depth, path) if @compact_empty
|
|
155
|
+
|
|
156
|
+
walk_full_array(value, depth, path)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def walk_full_array(value, depth, path)
|
|
160
|
+
fields = nil
|
|
161
|
+
result = []
|
|
162
|
+
value.each do |item|
|
|
163
|
+
if result.length >= @max_array_items
|
|
164
|
+
fields = append_truncation_field(fields, "array_items")
|
|
165
|
+
break
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
child = walk_value(item, depth + 1, nil, path)
|
|
169
|
+
child_truncated = consume_truncated
|
|
170
|
+
result << child
|
|
171
|
+
fields = append_truncation_field(fields, "array_items") if child_truncated
|
|
172
|
+
end
|
|
173
|
+
finish_array(result, fields)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def walk_compact_array(value, depth, path)
|
|
177
|
+
fields = nil
|
|
178
|
+
result = []
|
|
179
|
+
value.each do |item|
|
|
180
|
+
next if raw_omitted_value?(item)
|
|
181
|
+
|
|
182
|
+
child = walk_value(item, depth + 1, nil, path)
|
|
183
|
+
child_truncated = consume_truncated
|
|
184
|
+
next if omitted_value?(child)
|
|
185
|
+
|
|
186
|
+
if result.length >= @max_array_items
|
|
187
|
+
fields = append_truncation_field(fields, "array_items")
|
|
188
|
+
break
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
result << child
|
|
192
|
+
fields = append_truncation_field(fields, "array_items") if child_truncated
|
|
193
|
+
end
|
|
194
|
+
finish_array(result, fields)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def omitted_value?(_value) = false
|
|
198
|
+
|
|
199
|
+
def raw_omitted_value?(_value) = false
|
|
200
|
+
|
|
201
|
+
def key_value(key)
|
|
202
|
+
clear_truncated(key.is_a?(String) ? copy_string(key) : key)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def record_hash_truncation(fields, raw_key, _key, _key_truncated, child_truncated)
|
|
206
|
+
return fields unless child_truncated
|
|
207
|
+
|
|
208
|
+
append_truncation_field(fields, raw_key.to_s)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def finish_hash(result, fields)
|
|
212
|
+
return clear_truncated(result) unless fields
|
|
213
|
+
|
|
214
|
+
result[@truncation_key] = truncation_metadata(fields) if @truncation_key
|
|
215
|
+
mark_truncated(result)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def finish_array(result, fields)
|
|
219
|
+
return clear_truncated(result) unless fields
|
|
220
|
+
|
|
221
|
+
result << { @truncation_key => truncation_metadata(fields) } if @truncation_key
|
|
222
|
+
mark_truncated(result)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def truncation_metadata(fields)
|
|
226
|
+
self.class.truncation_metadata(
|
|
227
|
+
fields,
|
|
228
|
+
max_array_items: @max_array_items,
|
|
229
|
+
max_depth: @max_depth,
|
|
230
|
+
max_hash_keys: @max_hash_keys,
|
|
231
|
+
max_string_bytes: @max_string_bytes
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def string_value(value)
|
|
236
|
+
return clear_truncated(copy_string(value)) if value.bytesize <= @max_string_bytes
|
|
237
|
+
|
|
238
|
+
mark_truncated("#{value.byteslice(0, @max_string_bytes).scrub("?")}#{TRUNCATED_SUFFIX}")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def mark_truncated(value)
|
|
242
|
+
@last_truncated = true
|
|
243
|
+
value
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def clear_truncated(value)
|
|
247
|
+
@last_truncated = false
|
|
248
|
+
value
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def consume_truncated
|
|
252
|
+
truncated = @last_truncated
|
|
253
|
+
@last_truncated = false
|
|
254
|
+
truncated
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def copy_string(value)
|
|
258
|
+
value.is_a?(String) && !value.frozen? ? value.dup : value
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def append_truncation_field(fields, field)
|
|
262
|
+
(fields ||= []) << field
|
|
263
|
+
fields
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def path_for(parent_path, key)
|
|
267
|
+
parent_path ? "#{parent_path}.#{key}" : key.to_s
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
private_constant :BoundedTraversal
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|