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.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/docs/advanced-configuration.md +66 -0
  6. data/docs/attribute-keys.md +74 -0
  7. data/docs/configuration.md +327 -0
  8. data/docs/context-and-propagation.md +353 -0
  9. data/docs/contracts.md +211 -0
  10. data/docs/development.md +49 -0
  11. data/docs/extensions-and-api.md +567 -0
  12. data/docs/health-schema.md +104 -0
  13. data/docs/instrumentation-cheatsheet.md +29 -0
  14. data/docs/internals.md +135 -0
  15. data/docs/outputs-and-lifecycle.md +206 -0
  16. data/docs/quickstart.md +133 -0
  17. data/docs/record-sources.md +17 -0
  18. data/docs/records-and-data-policy.md +230 -0
  19. data/docs/security-and-wire.md +45 -0
  20. data/docs/tail.md +91 -0
  21. data/exe/julewire +6 -0
  22. data/julewire-core.gemspec +41 -0
  23. data/lib/julewire/core/cli/doctor.rb +143 -0
  24. data/lib/julewire/core/cli/line_helpers.rb +77 -0
  25. data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
  26. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
  27. data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
  28. data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
  29. data/lib/julewire/core/cli/log_formats.rb +123 -0
  30. data/lib/julewire/core/cli/tail.rb +153 -0
  31. data/lib/julewire/core/cli/transcode.rb +105 -0
  32. data/lib/julewire/core/cli.rb +73 -0
  33. data/lib/julewire/core/configuration.rb +99 -0
  34. data/lib/julewire/core/context_store.rb +384 -0
  35. data/lib/julewire/core/destinations/chaos_output.rb +91 -0
  36. data/lib/julewire/core/destinations/collection.rb +177 -0
  37. data/lib/julewire/core/destinations/definition.rb +125 -0
  38. data/lib/julewire/core/destinations/destination.rb +268 -0
  39. data/lib/julewire/core/destinations/registry.rb +81 -0
  40. data/lib/julewire/core/destinations/sink.rb +35 -0
  41. data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
  42. data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
  43. data/lib/julewire/core/destinations/write_step.rb +119 -0
  44. data/lib/julewire/core/destinations.rb +33 -0
  45. data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
  46. data/lib/julewire/core/diagnostics/doctor.rb +114 -0
  47. data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
  48. data/lib/julewire/core/diagnostics/health.rb +144 -0
  49. data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
  50. data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
  51. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
  52. data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
  53. data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
  54. data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
  55. data/lib/julewire/core/diagnostics/tail.rb +168 -0
  56. data/lib/julewire/core/diagnostics.rb +8 -0
  57. data/lib/julewire/core/error.rb +7 -0
  58. data/lib/julewire/core/execution/boundary.rb +106 -0
  59. data/lib/julewire/core/execution/handle.rb +77 -0
  60. data/lib/julewire/core/execution/lineage.rb +192 -0
  61. data/lib/julewire/core/execution/measurement_handle.rb +28 -0
  62. data/lib/julewire/core/execution/no_current_error.rb +9 -0
  63. data/lib/julewire/core/execution/scope.rb +246 -0
  64. data/lib/julewire/core/execution/scope_fields.rb +76 -0
  65. data/lib/julewire/core/execution/scope_identity.rb +71 -0
  66. data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
  67. data/lib/julewire/core/execution/summary_state.rb +206 -0
  68. data/lib/julewire/core/execution/view.rb +56 -0
  69. data/lib/julewire/core/facade_methods.rb +181 -0
  70. data/lib/julewire/core/fields/attribute_keys.rb +54 -0
  71. data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
  72. data/lib/julewire/core/fields/bags.rb +123 -0
  73. data/lib/julewire/core/fields/carry_proxy.rb +22 -0
  74. data/lib/julewire/core/fields/context_proxy.rb +11 -0
  75. data/lib/julewire/core/fields/field_set.rb +78 -0
  76. data/lib/julewire/core/fields/field_stack.rb +269 -0
  77. data/lib/julewire/core/fields/internal/deletion.rb +68 -0
  78. data/lib/julewire/core/fields/internal.rb +87 -0
  79. data/lib/julewire/core/fields/lookup.rb +35 -0
  80. data/lib/julewire/core/fields/section_proxy.rb +88 -0
  81. data/lib/julewire/core/fields/stack_set.rb +69 -0
  82. data/lib/julewire/core/fields/static_labels.rb +43 -0
  83. data/lib/julewire/core/fields/summary_proxy.rb +62 -0
  84. data/lib/julewire/core/integration/configurable.rb +52 -0
  85. data/lib/julewire/core/integration/destination_health.rb +43 -0
  86. data/lib/julewire/core/integration/event_subscriber.rb +62 -0
  87. data/lib/julewire/core/integration/facade.rb +131 -0
  88. data/lib/julewire/core/integration/fork_hooks.rb +79 -0
  89. data/lib/julewire/core/integration/health.rb +41 -0
  90. data/lib/julewire/core/integration/ivar_state.rb +38 -0
  91. data/lib/julewire/core/integration/lifecycle.rb +22 -0
  92. data/lib/julewire/core/integration/scoped.rb +34 -0
  93. data/lib/julewire/core/integration/settings.rb +92 -0
  94. data/lib/julewire/core/integration/subscriber_install.rb +39 -0
  95. data/lib/julewire/core/integration/subscription.rb +29 -0
  96. data/lib/julewire/core/integration/values.rb +192 -0
  97. data/lib/julewire/core/lifecycle_error.rb +7 -0
  98. data/lib/julewire/core/local_storage.rb +91 -0
  99. data/lib/julewire/core/processing/level_threshold.rb +53 -0
  100. data/lib/julewire/core/processing/match.rb +74 -0
  101. data/lib/julewire/core/processing/pipeline.rb +360 -0
  102. data/lib/julewire/core/processing/processor_chain.rb +69 -0
  103. data/lib/julewire/core/processing/processor_registry.rb +115 -0
  104. data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
  105. data/lib/julewire/core/processing/record_field_transform.rb +124 -0
  106. data/lib/julewire/core/processing/sampling.rb +109 -0
  107. data/lib/julewire/core/processing.rb +41 -0
  108. data/lib/julewire/core/propagation/carrier.rb +93 -0
  109. data/lib/julewire/core/propagation.rb +50 -0
  110. data/lib/julewire/core/records/console_formatter.rb +24 -0
  111. data/lib/julewire/core/records/deconstruct.rb +19 -0
  112. data/lib/julewire/core/records/display_message.rb +166 -0
  113. data/lib/julewire/core/records/draft.rb +576 -0
  114. data/lib/julewire/core/records/formatter.rb +14 -0
  115. data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
  116. data/lib/julewire/core/records/metadata.rb +23 -0
  117. data/lib/julewire/core/records/public_projection.rb +51 -0
  118. data/lib/julewire/core/records/raw_input.rb +41 -0
  119. data/lib/julewire/core/records/record.rb +175 -0
  120. data/lib/julewire/core/records/severity.rb +44 -0
  121. data/lib/julewire/core/runtime.rb +515 -0
  122. data/lib/julewire/core/runtime_locator.rb +20 -0
  123. data/lib/julewire/core/runtime_registry.rb +48 -0
  124. data/lib/julewire/core/runtime_state.rb +39 -0
  125. data/lib/julewire/core/scheduling/deadline.rb +24 -0
  126. data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
  127. data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
  128. data/lib/julewire/core/sentinel.rb +18 -0
  129. data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
  130. data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
  131. data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
  132. data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
  133. data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
  134. data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
  135. data/lib/julewire/core/serialization/exception_shape.rb +88 -0
  136. data/lib/julewire/core/serialization/json_encoder.rb +69 -0
  137. data/lib/julewire/core/serialization/serializer.rb +233 -0
  138. data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
  139. data/lib/julewire/core/serialization/text_encoder.rb +147 -0
  140. data/lib/julewire/core/serialization/value_copy.rb +209 -0
  141. data/lib/julewire/core/serialization/value_traversal.rb +150 -0
  142. data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
  143. data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
  144. data/lib/julewire/core/testing/chaos/destination.rb +55 -0
  145. data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
  146. data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
  147. data/lib/julewire/core/testing/chaos.rb +80 -0
  148. data/lib/julewire/core/testing/contracts/component.rb +162 -0
  149. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
  150. data/lib/julewire/core/testing/contracts/integration.rb +166 -0
  151. data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
  152. data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
  153. data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
  154. data/lib/julewire/core/testing/contracts/wire.rb +60 -0
  155. data/lib/julewire/core/testing/contracts.rb +24 -0
  156. data/lib/julewire/core/testing/coverage.rb +58 -0
  157. data/lib/julewire/core/testing/test_reports.rb +78 -0
  158. data/lib/julewire/core/testing.rb +122 -0
  159. data/lib/julewire/core/validation.rb +69 -0
  160. data/lib/julewire/core/version.rb +7 -0
  161. data/lib/julewire/core.rb +80 -0
  162. data/lib/julewire/error.rb +5 -0
  163. data/lib/julewire-core.rb +3 -0
  164. 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