julewire-core 1.0.0 → 1.1.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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/docs/advanced-configuration.md +2 -1
  4. data/docs/configuration.md +14 -7
  5. data/docs/context-and-propagation.md +26 -4
  6. data/docs/contracts.md +25 -50
  7. data/docs/extensions-and-api.md +87 -88
  8. data/docs/health-schema.md +12 -2
  9. data/docs/internals.md +24 -23
  10. data/docs/outputs-and-lifecycle.md +15 -8
  11. data/docs/records-and-data-policy.md +38 -7
  12. data/lib/julewire/core/cli/doctor.rb +20 -16
  13. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  14. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  15. data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
  16. data/lib/julewire/core/cli/log_formats.rb +9 -9
  17. data/lib/julewire/core/cli/tail.rb +19 -14
  18. data/lib/julewire/core/cli/transcode.rb +4 -5
  19. data/lib/julewire/core/cli.rb +1 -1
  20. data/lib/julewire/core/configuration.rb +1 -1
  21. data/lib/julewire/core/context_store.rb +56 -87
  22. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  23. data/lib/julewire/core/destinations/collection.rb +8 -16
  24. data/lib/julewire/core/destinations/definition.rb +1 -9
  25. data/lib/julewire/core/destinations/destination.rb +16 -66
  26. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  27. data/lib/julewire/core/destinations/registry.rb +1 -1
  28. data/lib/julewire/core/destinations/sink.rb +0 -1
  29. data/lib/julewire/core/destinations/synchronized_output.rb +54 -17
  30. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  31. data/lib/julewire/core/destinations/write_step.rb +5 -5
  32. data/lib/julewire/core/destinations.rb +0 -7
  33. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  34. data/lib/julewire/core/diagnostics/doctor.rb +22 -15
  35. data/lib/julewire/core/diagnostics/failure_snapshot.rb +9 -3
  36. data/lib/julewire/core/diagnostics/health.rb +41 -57
  37. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  38. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  39. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  40. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  41. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  42. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  43. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  44. data/lib/julewire/core/execution/boundary.rb +2 -8
  45. data/lib/julewire/core/execution/handle.rb +14 -17
  46. data/lib/julewire/core/execution/lineage.rb +26 -62
  47. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  48. data/lib/julewire/core/execution/scope.rb +17 -22
  49. data/lib/julewire/core/execution/scope_fields.rb +4 -13
  50. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  51. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  52. data/lib/julewire/core/execution/summary_state.rb +16 -19
  53. data/lib/julewire/core/execution/view.rb +8 -17
  54. data/lib/julewire/core/facade_methods.rb +67 -65
  55. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  56. data/lib/julewire/core/fields/bags.rb +23 -27
  57. data/lib/julewire/core/fields/field_set.rb +52 -16
  58. data/lib/julewire/core/fields/field_stack.rb +69 -109
  59. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  60. data/lib/julewire/core/fields/internal.rb +35 -26
  61. data/lib/julewire/core/fields/lookup.rb +9 -13
  62. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  63. data/lib/julewire/core/fields/stack_set.rb +9 -14
  64. data/lib/julewire/core/fields/static_labels.rb +1 -1
  65. data/lib/julewire/core/integration/configurable.rb +4 -3
  66. data/lib/julewire/core/integration/destination_health.rb +28 -7
  67. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  68. data/lib/julewire/core/integration/facade.rb +12 -15
  69. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  70. data/lib/julewire/core/integration/health.rb +0 -3
  71. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  72. data/lib/julewire/core/integration/protocol.rb +20 -0
  73. data/lib/julewire/core/integration/settings.rb +4 -4
  74. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  75. data/lib/julewire/core/integration/subscription.rb +3 -2
  76. data/lib/julewire/core/integration/values.rb +0 -17
  77. data/lib/julewire/core/local_storage.rb +3 -19
  78. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  79. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  80. data/lib/julewire/core/processing/match.rb +6 -4
  81. data/lib/julewire/core/processing/pipeline.rb +46 -40
  82. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  83. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  84. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  85. data/lib/julewire/core/processing/sampling.rb +1 -3
  86. data/lib/julewire/core/processing.rb +2 -2
  87. data/lib/julewire/core/propagation/carrier.rb +73 -19
  88. data/lib/julewire/core/propagation.rb +18 -4
  89. data/lib/julewire/core/records/build_input.rb +75 -0
  90. data/lib/julewire/core/records/console_formatter.rb +6 -6
  91. data/lib/julewire/core/records/deconstruct.rb +1 -1
  92. data/lib/julewire/core/records/display_message.rb +2 -4
  93. data/lib/julewire/core/records/draft.rb +127 -258
  94. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  95. data/lib/julewire/core/records/metadata.rb +4 -3
  96. data/lib/julewire/core/records/public_projection.rb +2 -2
  97. data/lib/julewire/core/records/raw_input.rb +1 -1
  98. data/lib/julewire/core/records/record.rb +30 -18
  99. data/lib/julewire/core/records/severity.rb +7 -15
  100. data/lib/julewire/core/runtime.rb +87 -72
  101. data/lib/julewire/core/runtime_registry.rb +20 -15
  102. data/lib/julewire/core/runtime_state.rb +5 -3
  103. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  104. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  105. data/lib/julewire/core/serialization/bounded_transform.rb +3 -2
  106. data/lib/julewire/core/serialization/bounded_traversal.rb +72 -83
  107. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  108. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  109. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  110. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  111. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  112. data/lib/julewire/core/serialization/serializer.rb +22 -49
  113. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  114. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  115. data/lib/julewire/core/serialization/truncation_metadata.rb +157 -0
  116. data/lib/julewire/core/serialization/value_copy.rb +183 -109
  117. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  118. data/lib/julewire/core/testing.rb +6 -56
  119. data/lib/julewire/core/validation.rb +2 -3
  120. data/lib/julewire/core/version.rb +1 -1
  121. data/lib/julewire/core.rb +2 -7
  122. metadata +7 -19
  123. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  124. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  125. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  126. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  127. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  128. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  129. data/lib/julewire/core/testing/chaos.rb +0 -80
  130. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  131. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  132. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  133. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  134. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  135. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  136. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  137. data/lib/julewire/core/testing/contracts.rb +0 -24
  138. data/lib/julewire/core/testing/coverage.rb +0 -58
  139. data/lib/julewire/core/testing/test_reports.rb +0 -78
@@ -11,10 +11,8 @@ module Julewire
11
11
  no_output_dropped
12
12
  processor_dropped
13
13
  processor_error
14
+ processor_invalid
14
15
  ].freeze
15
- EMPTY_HASH = {}.freeze
16
- private_constant :EMPTY_HASH
17
-
18
16
  # @api integration_spi
19
17
  # Integration-facing raw/normalized record pipeline.
20
18
  def initialize(configuration:, invalid_severity_reporter: Diagnostics::InvalidSeverityReporter.counter)
@@ -32,11 +30,11 @@ module Julewire
32
30
  initialize_tracking
33
31
  end
34
32
 
35
- def emit(input = Core::UNSET, **fields, &)
33
+ def emit(input = UNSET, **fields, &)
36
34
  emit_with_level_check(input, true, fields, &)
37
35
  end
38
36
 
39
- def emit_without_level(input = Core::UNSET, **fields, &)
37
+ def emit_without_level(input = UNSET, **fields, &)
40
38
  emit_with_level_check(input, false, fields, &)
41
39
  end
42
40
 
@@ -50,7 +48,7 @@ module Julewire
50
48
  # Runtime summaries already carry their captured scope fields.
51
49
  def emit_isolated_input(input, enforce_level: true)
52
50
  emit_input_with_guard(input, enforce_level: enforce_level, lazy: false) do
53
- build_isolated_draft(input)
51
+ build_isolated_owned_draft(input)
54
52
  end
55
53
  end
56
54
 
@@ -109,7 +107,6 @@ module Julewire
109
107
  {
110
108
  encoder: Serialization::JsonEncoder.new(max_backtrace_lines: configuration.error_backtrace_lines),
111
109
  formatter: Records::Formatter.new,
112
- error_backtrace_lines: configuration.error_backtrace_lines,
113
110
  on_drop: configuration.on_drop,
114
111
  on_failure: configuration.on_failure
115
112
  }
@@ -124,9 +121,10 @@ module Julewire
124
121
 
125
122
  def build_processor_chain(configuration)
126
123
  ProcessorChain.new(
127
- processors: configuration.processors.to_a.freeze,
128
- error_backtrace_lines: @error_backtrace_lines,
129
- on_error: method(:record_processor_error)
124
+ processors: configuration.processors.to_a,
125
+ on_error: method(:record_processor_error),
126
+ on_invalid: method(:record_invalid_processor_result),
127
+ on_invalid_draft: method(:record_invalid_processor_draft)
130
128
  )
131
129
  end
132
130
 
@@ -146,7 +144,7 @@ module Julewire
146
144
  if raw_input_blocked?(input, enforce_level: enforce_level, lazy: lazy)
147
145
  increment_pipeline_counter(:level_dropped)
148
146
  else
149
- emit_prepared_draft(yield, enforce_level: enforce_level, merge_static_labels: false)
147
+ emit_prepared_draft(yield, enforce_level: enforce_level)
150
148
  end
151
149
  finish_emit_attempt(degradation_marker)
152
150
  rescue StandardError => e
@@ -159,7 +157,6 @@ module Julewire
159
157
  return false unless @destinations.empty?
160
158
 
161
159
  record_no_output_drop
162
- true
163
160
  end
164
161
 
165
162
  def finish_emit_attempt(degradation_marker)
@@ -171,9 +168,7 @@ module Julewire
171
168
  Records::Record.validate_normalized!(record)
172
169
  return emit_fast_record(record, enforce_level: enforce_level) if fast_record_path?
173
170
 
174
- emit_prepared_draft(Records::Draft.from_record(record, freeze_sections: false),
175
- enforce_level: enforce_level)
176
- nil
171
+ emit_prepared_draft(Records::Draft.from_record(record), enforce_level: enforce_level)
177
172
  end
178
173
 
179
174
  def fast_record_path?
@@ -188,7 +183,6 @@ module Julewire
188
183
 
189
184
  increment_pipeline_counter(:entered)
190
185
  emit_to_destinations(record)
191
- nil
192
186
  end
193
187
 
194
188
  def raw_input_blocked?(input, enforce_level:, lazy:)
@@ -210,14 +204,14 @@ module Julewire
210
204
  )
211
205
  end
212
206
 
213
- def build_isolated_draft(input, input_owned: false)
207
+ def build_isolated_owned_draft(input)
214
208
  build_draft_from(
215
209
  input,
216
- input_owned: input_owned,
217
- context: EMPTY_HASH,
218
- neutral: EMPTY_HASH,
219
- attributes: EMPTY_HASH,
220
- carry: EMPTY_HASH,
210
+ input_owned: true,
211
+ context: nil,
212
+ neutral: nil,
213
+ attributes: nil,
214
+ carry: nil,
221
215
  scope: nil
222
216
  )
223
217
  end
@@ -229,9 +223,7 @@ module Julewire
229
223
  neutral: neutral,
230
224
  attributes: attributes,
231
225
  carry: carry,
232
- static_labels: @labels,
233
226
  input_owned: input_owned,
234
- freeze_sections: @processors_empty,
235
227
  scope: scope,
236
228
  error_backtrace_lines: @error_backtrace_lines,
237
229
  invalid_severity_reporter: @invalid_severity_reporter
@@ -241,7 +233,6 @@ module Julewire
241
233
  def initialize_tracking
242
234
  @health = Diagnostics::Health.new(
243
235
  counter_keys: COUNTER_KEYS,
244
- callback_metadata: {},
245
236
  callback_failure_counter: :callback_error
246
237
  )
247
238
  end
@@ -267,13 +258,13 @@ module Julewire
267
258
  @threshold.allow?(record_or_draft.fetch(:severity))
268
259
  end
269
260
 
270
- def emit_prepared_draft(draft, enforce_level:, merge_static_labels: true)
261
+ def emit_prepared_draft(draft, enforce_level:)
271
262
  if enforce_level && !emit_record?(draft)
272
263
  increment_pipeline_counter(:level_dropped)
273
264
  return
274
265
  end
275
266
 
276
- draft = merge_static_labels(draft) if merge_static_labels
267
+ draft = merge_static_labels(draft)
277
268
  increment_pipeline_counter(:entered)
278
269
  emit_processed_draft(draft, enforce_level: enforce_level)
279
270
  rescue StandardError => e
@@ -289,9 +280,12 @@ module Julewire
289
280
  return
290
281
  end
291
282
 
292
- processed, enforce_processed_level = processed_draft_and_level(processed, enforce_level)
283
+ if processed.instance_of?(ProcessorChain::ErrorResult)
284
+ processed = processed.draft
285
+ enforce_level = false
286
+ end
293
287
 
294
- if enforce_processed_level && !emit_record?(processed)
288
+ if enforce_level && !emit_record?(processed)
295
289
  increment_pipeline_counter(:level_dropped)
296
290
  return
297
291
  end
@@ -299,27 +293,40 @@ module Julewire
299
293
  emit_to_destinations(processed.to_record)
300
294
  end
301
295
 
302
- def processed_draft_and_level(processed, default_enforce_level)
303
- if processed.is_a?(ProcessorChain::ErrorResult)
304
- [processed.draft, false]
305
- else
306
- [processed, default_enforce_level]
307
- end
308
- end
309
-
310
296
  def record_processor_error(error, record_metadata)
311
297
  increment_pipeline_counter(:processor_error)
312
298
  notify_failure(error, phase: :processor, record_metadata: record_metadata)
313
299
  end
314
300
 
301
+ def record_invalid_processor_result(processor_name, result, record_metadata)
302
+ increment_pipeline_counter(:processor_invalid)
303
+ error, metadata = InvalidResultFailure.build(
304
+ message: "processor returned unsupported result",
305
+ phase: :processor_result,
306
+ processor_name: processor_name,
307
+ record_metadata: record_metadata,
308
+ result: result
309
+ )
310
+ notify_failure(error, **metadata)
311
+ end
312
+
313
+ def record_invalid_processor_draft(processor_name, error, record_metadata)
314
+ increment_pipeline_counter(:processor_invalid)
315
+ notify_failure(
316
+ error,
317
+ phase: :processor_data,
318
+ processor: processor_name,
319
+ record_metadata: record_metadata
320
+ )
321
+ end
322
+
315
323
  def emit_internal_error_record(error)
316
324
  emit_prepared_draft(
317
- Diagnostics::InternalRecords.emit_error(error, error_backtrace_lines: @error_backtrace_lines),
325
+ Diagnostics::InternalRecords.emit_error(error),
318
326
  enforce_level: false
319
327
  )
320
328
  rescue StandardError => e
321
329
  notify_failure(e, phase: :internal_error_record)
322
- nil
323
330
  end
324
331
 
325
332
  def emit_to_destinations(record)
@@ -328,7 +335,6 @@ module Julewire
328
335
 
329
336
  def record_no_output_drop
330
337
  increment_pipeline_counter(:no_output_dropped)
331
- nil
332
338
  end
333
339
 
334
340
  def notify_failure(error, **metadata)
@@ -7,10 +7,11 @@ module Julewire
7
7
  DROP = Core.sentinel(:drop)
8
8
  ErrorResult = Data.define(:draft)
9
9
 
10
- def initialize(processors:, error_backtrace_lines:, on_error:)
10
+ def initialize(processors:, on_error:, on_invalid:, on_invalid_draft:)
11
11
  @processors = processors
12
- @error_backtrace_lines = error_backtrace_lines
13
12
  @on_error = on_error
13
+ @on_invalid = on_invalid
14
+ @on_invalid_draft = on_invalid_draft
14
15
  end
15
16
 
16
17
  def empty? = @processors.empty?
@@ -19,13 +20,15 @@ module Julewire
19
20
  current = draft
20
21
 
21
22
  @processors.each do |processor|
22
- current = apply_processor_result(current, processor.call(current))
23
- break if current == :drop
23
+ result = processor.call(current)
24
+ current = apply_processor_result(current, processor, result)
25
+ return DROP if current.equal?(DROP)
26
+ return DROP unless valid_processor_draft?(processor, current)
24
27
  rescue StandardError => e
25
28
  action = handle_processor_error(processor, e, current)
26
29
  case action
27
30
  when :continue
28
- next
31
+ return DROP unless valid_processor_draft?(processor, current)
29
32
  when :drop
30
33
  return DROP
31
34
  else
@@ -33,15 +36,17 @@ module Julewire
33
36
  end
34
37
  end
35
38
 
36
- current == :drop ? DROP : current
39
+ current
37
40
  end
38
41
 
39
42
  private
40
43
 
41
- def apply_processor_result(current, result)
42
- return :drop if result == :drop
43
- return result if result.is_a?(Records::Draft)
44
+ def apply_processor_result(current, processor, result)
45
+ return DROP if result == :drop
46
+ return current if result.nil?
47
+ return result if result.instance_of?(Records::Draft)
44
48
 
49
+ @on_invalid.call(processor.processor_name, result, Records::Metadata.call(current))
45
50
  current
46
51
  end
47
52
 
@@ -55,12 +60,18 @@ module Julewire
55
60
  ErrorResult.new(processor_error_record(processor, error, record_metadata))
56
61
  end
57
62
 
63
+ def valid_processor_draft?(processor, draft)
64
+ draft.validate!
65
+ rescue StandardError => e
66
+ @on_invalid_draft.call(processor.processor_name, e, Records::Metadata.call(draft))
67
+ false
68
+ end
69
+
58
70
  def processor_error_record(processor, error, record_metadata)
59
71
  Diagnostics::InternalRecords.processor_error(
60
72
  processor_name: processor.processor_name,
61
73
  error: error,
62
- record_metadata: record_metadata,
63
- error_backtrace_lines: @error_backtrace_lines
74
+ record_metadata: record_metadata
64
75
  )
65
76
  end
66
77
  end
@@ -49,7 +49,6 @@ module Julewire
49
49
 
50
50
  entry = build_entry(processor, arguments, options)
51
51
  prepend ? @entries.unshift(entry) : @entries << entry
52
- self
53
52
  end
54
53
 
55
54
  def build_entry(processor, arguments = [], options = {})
@@ -66,7 +65,7 @@ module Julewire
66
65
  on_error: on_error,
67
66
  factory: true
68
67
  )
69
- elsif processor.is_a?(Class)
68
+ elsif processor.instance_of?(Class)
70
69
  Entry.new(
71
70
  entry: processor,
72
71
  arguments: arguments.dup.freeze,
@@ -81,13 +80,13 @@ module Julewire
81
80
  end
82
81
 
83
82
  def normalize_entry(entry)
84
- entry.is_a?(Entry) ? entry : build_entry(entry)
83
+ entry.instance_of?(Entry) ? entry : build_entry(entry)
85
84
  end
86
85
 
87
86
  def materialize(entry)
88
87
  processor = if entry.factory
89
88
  Processing.build(entry.entry, *entry.arguments, **entry.options)
90
- elsif entry.entry.is_a?(Class)
89
+ elsif entry.entry.instance_of?(Class)
91
90
  entry.entry.new(*entry.arguments, **entry.options)
92
91
  else
93
92
  entry.entry
@@ -14,10 +14,6 @@ module Julewire
14
14
  private_constant :SCALAR_KEY_SET
15
15
 
16
16
  class << self
17
- def container_keys = CONTAINER_KEYS
18
-
19
- def scalar_keys = SCALAR_KEYS
20
-
21
17
  def container_key?(key) = CONTAINER_KEY_SET.key?(key)
22
18
 
23
19
  def scalar_key?(key) = SCALAR_KEY_SET.key?(key)
@@ -77,7 +77,7 @@ module Julewire
77
77
  return :drop if value.nil?
78
78
  return if @threshold == HASH_SPACE
79
79
 
80
- Sampling.stable_hash(value) < @threshold ? nil : :drop
80
+ :drop unless Sampling.stable_hash(value) < @threshold
81
81
  end
82
82
 
83
83
  private
@@ -98,8 +98,6 @@ module Julewire
98
98
  end
99
99
 
100
100
  def field_value(hash, key)
101
- return unless hash.is_a?(Hash)
102
-
103
101
  Fields::FieldSet.value_for(hash, key)
104
102
  end
105
103
  end
@@ -10,7 +10,6 @@ module Julewire
10
10
  raise ArgumentError, "processor factory block required" unless factory
11
11
 
12
12
  @factories[normalize_kind(kind)] = factory
13
- nil
14
13
  end
15
14
 
16
15
  def build(kind, ...)
@@ -31,7 +30,8 @@ module Julewire
31
30
  raise ArgumentError, "processor kind must respond to #to_sym" unless kind.respond_to?(:to_sym)
32
31
 
33
32
  name = kind.to_sym
34
- raise ArgumentError, "processor kind cannot be empty" if name.name.empty?
33
+ raise ArgumentError, "processor kind #to_sym must return a Symbol" unless name.instance_of?(Symbol)
34
+ raise ArgumentError, "processor kind cannot be empty" if name.empty?
35
35
 
36
36
  name
37
37
  end
@@ -8,14 +8,40 @@ module Julewire
8
8
  # @api public
9
9
  module Carrier
10
10
  DEFAULT_KEY = "julewire"
11
+ DEFAULT_MAX_BYTES = 65_536
11
12
  DEFAULT_ENVELOPE = Core.sentinel(:default_envelope)
13
+
14
+ # @api integration_spi
15
+ class Extracted
16
+ attr_reader :envelope, :status, :reason, :error
17
+
18
+ def initialize(envelope:, status:, reason: nil, error: nil)
19
+ @envelope = envelope
20
+ @status = status
21
+ @reason = reason
22
+ @error = error
23
+ end
24
+
25
+ def failure? = !error.nil?
26
+ end
12
27
  private_constant :DEFAULT_ENVELOPE
13
28
 
29
+ # @api integration_spi
30
+ class ExtractionError < StandardError
31
+ attr_reader :status, :reason
32
+
33
+ def initialize(status, reason)
34
+ @status = status
35
+ @reason = reason
36
+ super(reason)
37
+ end
38
+ end
39
+
14
40
  class << self
15
41
  def encode(envelope: DEFAULT_ENVELOPE, max_bytes: nil)
16
42
  Validation.validate_byte_limit!(max_bytes, name: :max_bytes)
17
43
 
18
- encoded = JSON.generate(serialized_envelope(envelope), allow_nan: false)
44
+ encoded = JSON.generate(serialized_envelope(envelope))
19
45
  return if max_bytes && encoded.bytesize > max_bytes
20
46
 
21
47
  encoded
@@ -24,25 +50,27 @@ module Julewire
24
50
  def inject(carrier = {}, envelope: DEFAULT_ENVELOPE, key: DEFAULT_KEY, max_bytes: nil)
25
51
  validate_carrier!(carrier)
26
52
  encoded = encode(envelope: envelope, max_bytes: max_bytes)
27
- clear_carrier_key!(carrier, key) unless encoded
53
+ clear_carrier_key!(carrier, key)
28
54
  return unless encoded
29
55
 
30
56
  carrier[key.to_s] = encoded
31
57
  carrier
32
58
  end
33
59
 
34
- def extract(carrier, key: DEFAULT_KEY)
35
- value = carrier_value(carrier, key)
36
- return {} unless value
60
+ def extract_envelope(carrier, key: DEFAULT_KEY, max_bytes: DEFAULT_MAX_BYTES)
61
+ extract_result(carrier, key: key, max_bytes: max_bytes).envelope
62
+ end
37
63
 
38
- parsed = JSON.parse(value.to_s)
39
- parsed.is_a?(Hash) ? Fields::FieldSet.deep_symbolize_keys(parsed) : {}
40
- rescue StandardError
41
- {}
64
+ # @api integration_spi
65
+ def extract_result(carrier, key: DEFAULT_KEY, max_bytes: DEFAULT_MAX_BYTES)
66
+ Validation.validate_byte_limit!(max_bytes, name: :max_bytes)
67
+
68
+ extract_payload(carrier, key: key, max_bytes: max_bytes)
42
69
  end
43
70
 
44
- def restore(carrier, key: DEFAULT_KEY, link_executions: false, &)
45
- Propagation.restore(extract(carrier, key: key), link_executions: link_executions, &)
71
+ def restore(carrier, key: DEFAULT_KEY, link_executions: false, max_bytes: DEFAULT_MAX_BYTES, &)
72
+ envelope = extract_result(carrier, key: key, max_bytes: max_bytes).envelope
73
+ Propagation.restore(envelope, link_executions: link_executions, owned: true, &)
46
74
  end
47
75
 
48
76
  def serialized_envelope(envelope)
@@ -53,6 +81,36 @@ module Julewire
53
81
 
54
82
  private
55
83
 
84
+ def extract_payload(carrier, key:, max_bytes:)
85
+ value = carrier_value(carrier, key)
86
+ return extracted({}, :missing) unless value
87
+
88
+ string = value.to_s
89
+ if max_bytes && string.bytesize > max_bytes
90
+ return extracted_failure(:oversized, "carrier payload exceeds max_bytes")
91
+ end
92
+
93
+ parsed = JSON.parse(string)
94
+ unless parsed.instance_of?(Hash)
95
+ return extracted_failure(:non_hash,
96
+ "carrier payload must be a JSON object")
97
+ end
98
+
99
+ extracted(Fields::FieldSet.deep_symbolize_owned_keys(parsed), :ok)
100
+ rescue StandardError => e
101
+ extracted_failure(:malformed, "carrier payload is not valid JSON", e)
102
+ end
103
+
104
+ def extracted(envelope, status)
105
+ Extracted.new(envelope: envelope, status: status)
106
+ end
107
+
108
+ def extracted_failure(status, reason, cause = nil)
109
+ error = ExtractionError.new(status, reason)
110
+ error.set_backtrace(cause.backtrace) if cause
111
+ Extracted.new(envelope: {}, status: status, reason: reason, error: error)
112
+ end
113
+
56
114
  def carrier_value(carrier, key)
57
115
  return unless carrier.respond_to?(:[])
58
116
 
@@ -68,14 +126,10 @@ module Julewire
68
126
  def clear_carrier_key!(carrier, key)
69
127
  string_key = key.to_s
70
128
  symbol_key = Fields::Internal.normalize_key(key)
71
- if carrier.respond_to?(:delete)
72
- begin
73
- carrier.delete(string_key)
74
- carrier.delete(symbol_key)
75
- rescue StandardError
76
- clear_carrier_key_by_assignment(carrier, string_key, symbol_key)
77
- end
78
- else
129
+ begin
130
+ carrier.delete(string_key)
131
+ carrier.delete(symbol_key)
132
+ rescue StandardError
79
133
  clear_carrier_key_by_assignment(carrier, string_key, symbol_key)
80
134
  end
81
135
  end
@@ -13,18 +13,22 @@ module Julewire
13
13
  end
14
14
 
15
15
  def capture_local
16
- capture_with { Fields::FieldSet.deep_dup(it) }
16
+ capture_with { Fields::FieldSet.deep_dup_owned(it) }
17
17
  end
18
18
 
19
- def restore(envelope, link_executions: false, &)
19
+ def restore(envelope, link_executions: false, owned: false, &)
20
20
  raise ArgumentError, "block required" unless block_given?
21
21
 
22
- sections = FIELD_SECTIONS.to_h { |section| [section, hash_value(envelope, section)] }
23
- execution = hash_value(envelope, :execution)
22
+ Integration::Protocol.validate_symbol_hash(envelope) if owned
23
+ sections = FIELD_SECTIONS.to_h do |section|
24
+ [section, restore_hash_value(envelope, section, owned: owned)]
25
+ end
26
+ execution = restore_hash_value(envelope, :execution, owned: owned)
24
27
  ContextStore.current.with_propagation(
25
28
  **sections,
26
29
  execution: execution,
27
30
  link_executions: link_executions,
31
+ owned: owned,
28
32
  &
29
33
  )
30
34
  end
@@ -44,6 +48,16 @@ module Julewire
44
48
  value = Fields::FieldSet.value_for(hash, key)
45
49
  value.is_a?(Hash) ? value : {}
46
50
  end
51
+
52
+ def restore_hash_value(envelope, key, owned:)
53
+ return hash_value(envelope, key) unless owned
54
+ return {} unless envelope.key?(key)
55
+
56
+ value = envelope.fetch(key)
57
+ return value if value.is_a?(Hash)
58
+
59
+ raise TypeError, "propagation #{key} must be a Hash"
60
+ end
47
61
  end
48
62
  end
49
63
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Records
6
+ # Converts raw emit input into known record fields plus payload fields.
7
+ # @api bridge_spi
8
+ module BuildInput
9
+ RECORD_INPUT_KEYS = Record::REQUIRED_KEYS.freeze
10
+ RECORD_INPUT_KEY_SET = RECORD_INPUT_KEYS.to_h { [it, true] }.freeze
11
+ private_constant :RECORD_INPUT_KEYS, :RECORD_INPUT_KEY_SET
12
+
13
+ class << self
14
+ def normalize_public(input)
15
+ return {} if input.nil?
16
+ return normalize_public_hash(input) if RawInput.hash_input?(input)
17
+
18
+ { message: input.to_s }
19
+ end
20
+
21
+ def validate_owned(input)
22
+ raise TypeError, "owned record input must be a Hash" unless input.is_a?(Hash)
23
+
24
+ Serialization::DeepFreeze.validate_symbol_keys(input)
25
+ validate_known_keys!(input)
26
+ input
27
+ end
28
+
29
+ private
30
+
31
+ def validate_known_keys!(input)
32
+ unknown = input.each_key.reject { RECORD_INPUT_KEY_SET.key?(it) }
33
+ return if unknown.empty?
34
+
35
+ raise TypeError, "owned record input has unknown top-level keys: #{unknown.join(", ")}"
36
+ end
37
+
38
+ def normalize_public_hash(input)
39
+ normalized = {}
40
+ payload_fields = {}
41
+ input.each do |key, raw_value|
42
+ normalized_key = Fields::Internal.normalize_key(key)
43
+ value = raw_value.equal?(input) ? CIRCULAR_REFERENCE : raw_value
44
+ if RECORD_INPUT_KEY_SET.key?(normalized_key)
45
+ normalized[normalized_key] = value
46
+ else
47
+ payload_fields[normalized_key] = value
48
+ end
49
+ end
50
+ merge_unknown_payload!(normalized, payload_fields)
51
+ normalized
52
+ end
53
+
54
+ def merge_unknown_payload!(normalized, payload_fields)
55
+ return if payload_fields.empty?
56
+
57
+ normalized[:payload] = if normalized.key?(:payload)
58
+ merge_payload_input(normalized.fetch(:payload), payload_fields)
59
+ else
60
+ payload_fields
61
+ end
62
+ end
63
+
64
+ def merge_payload_input(explicit_payload, unknown_payload)
65
+ if explicit_payload.is_a?(Hash)
66
+ Fields::FieldSet.merge(unknown_payload, explicit_payload)
67
+ else
68
+ Fields::FieldSet.merge(unknown_payload, Fields::FieldSet::VALUE_KEY => explicit_payload)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -9,13 +9,13 @@ module Julewire
9
9
  Record.validate_normalized!(record)
10
10
 
11
11
  {
12
- event: record[:event],
13
- labels: record[:labels],
12
+ event: record.fetch(:event),
13
+ labels: record.fetch(:labels),
14
14
  message: DisplayMessage.call(record),
15
- payload: record[:payload],
16
- severity: record[:severity],
17
- source: record[:source],
18
- timestamp: record[:timestamp]
15
+ payload: record.fetch(:payload),
16
+ severity: record.fetch(:severity),
17
+ source: record.fetch(:source),
18
+ timestamp: record.fetch(:timestamp)
19
19
  }
20
20
  end
21
21
  end
@@ -9,7 +9,7 @@ module Julewire
9
9
  return to_h unless keys
10
10
 
11
11
  keys.each_with_object({}) do |key, selected|
12
- selected[key] = Fields::FieldSet.deep_dup(@data[key]) if @data.key?(key)
12
+ selected[key] = Fields::FieldSet.deep_dup(fetch(key)) if key?(key)
13
13
  end
14
14
  end
15
15
  end
@@ -118,9 +118,7 @@ module Julewire
118
118
 
119
119
  def duration_text(value)
120
120
  text = format("%.3f", value.round(3))
121
- text.delete_suffix!("0") while text.end_with?("0")
122
- text.delete_suffix!(".")
123
- text
121
+ text.sub(/(?:\.0+|(\.\d+?)0+)\z/, "\\1")
124
122
  end
125
123
 
126
124
  def job_queue(neutral)
@@ -153,7 +151,7 @@ module Julewire
153
151
  end
154
152
 
155
153
  def value_at(value, key)
156
- Fields::Lookup.value(value, key)
154
+ Fields::Lookup.wire_value(value, key)
157
155
  end
158
156
 
159
157
  def blank?(value)