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,576 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Julewire
6
+ module Core
7
+ module Records
8
+ # @api extension
9
+ class Draft
10
+ include Enumerable
11
+ include Deconstruct
12
+
13
+ class << self
14
+ def build( # rubocop:disable Metrics/ParameterLists -- Record construction has fixed public sections.
15
+ input = {},
16
+ context:,
17
+ scope:,
18
+ attributes: {},
19
+ neutral: {},
20
+ carry: {},
21
+ static_labels: {},
22
+ freeze_sections: true,
23
+ error_backtrace_lines: Core::MAX_BACKTRACE_LINES,
24
+ invalid_severity_reporter: Diagnostics::InvalidSeverityReporter
25
+ )
26
+ build_with(
27
+ input,
28
+ context: context,
29
+ neutral: neutral,
30
+ attributes: attributes,
31
+ carry: carry,
32
+ static_labels: static_labels,
33
+ scope: scope,
34
+ invalid_severity_reporter: invalid_severity_reporter,
35
+ options: BuildOptions.defensive(
36
+ freeze_sections: freeze_sections,
37
+ error_backtrace_lines: error_backtrace_lines
38
+ )
39
+ )
40
+ end
41
+
42
+ def build_pipeline_owned( # rubocop:disable Metrics/ParameterLists -- Record construction has fixed public sections.
43
+ input = {},
44
+ context:,
45
+ scope:,
46
+ attributes: {},
47
+ neutral: {},
48
+ carry: {},
49
+ static_labels: {},
50
+ input_owned: false,
51
+ freeze_sections: true,
52
+ error_backtrace_lines: Core::MAX_BACKTRACE_LINES,
53
+ invalid_severity_reporter: Diagnostics::InvalidSeverityReporter
54
+ )
55
+ build_with(
56
+ input,
57
+ context: context,
58
+ neutral: neutral,
59
+ attributes: attributes,
60
+ carry: carry,
61
+ static_labels: static_labels,
62
+ scope: scope,
63
+ invalid_severity_reporter: invalid_severity_reporter,
64
+ options: BuildOptions.pipeline_owned(
65
+ input_owned: input_owned,
66
+ freeze_sections: freeze_sections,
67
+ error_backtrace_lines: error_backtrace_lines
68
+ )
69
+ )
70
+ end
71
+
72
+ private
73
+
74
+ def build_with(input, context:, neutral:, attributes:, carry:, static_labels:, scope:, # rubocop:disable Metrics/ParameterLists
75
+ invalid_severity_reporter:, options:)
76
+ builder = Builder.new(
77
+ input,
78
+ context: context,
79
+ neutral: neutral,
80
+ attributes: attributes,
81
+ carry: carry,
82
+ static_labels: static_labels,
83
+ scope: scope,
84
+ invalid_severity_reporter: invalid_severity_reporter,
85
+ options: options
86
+ )
87
+ new(builder.to_h, lineage: builder.lineage, freeze_sections: options.freeze_sections)
88
+ end
89
+
90
+ public
91
+
92
+ def from_normalized_hash(data, lineage: nil, freeze_sections: true)
93
+ normalized = Fields::FieldSet.deep_symbolize_keys(data)
94
+ lineage ||= Execution::Lineage.from_execution_hash(normalized[:execution])
95
+ normalized[:execution] = Execution::Lineage.clean_lazy_relationship_hash(normalized[:execution])
96
+ normalized = Fields::Internal.frozen_deep_symbolize_keys(normalized) if freeze_sections
97
+ new(
98
+ normalized,
99
+ lineage: lineage,
100
+ freeze_sections: freeze_sections
101
+ )
102
+ end
103
+
104
+ def from_record(record, freeze_sections: true)
105
+ Record.validate_normalized!(record)
106
+ from_normalized_hash(record.to_h, lineage: record.lineage, freeze_sections: freeze_sections)
107
+ end
108
+ end
109
+
110
+ LINEAGE_IDENTITY_KEYS = %i[type id depth root parent].freeze
111
+ private_constant :LINEAGE_IDENTITY_KEYS
112
+
113
+ BuildOptions = Data.define(:fields_owned, :input_owned, :freeze_sections, :error_backtrace_lines) do
114
+ class << self
115
+ def defensive(freeze_sections:, error_backtrace_lines:)
116
+ return default_defensive.fetch(freeze_sections) if default_backtrace_lines?(error_backtrace_lines)
117
+
118
+ new(false, false, freeze_sections, error_backtrace_lines)
119
+ end
120
+
121
+ def pipeline_owned(input_owned:, freeze_sections:, error_backtrace_lines:)
122
+ if default_backtrace_lines?(error_backtrace_lines)
123
+ return default_pipeline.fetch(input_owned).fetch(freeze_sections)
124
+ end
125
+
126
+ new(true, input_owned, freeze_sections, error_backtrace_lines)
127
+ end
128
+
129
+ private
130
+
131
+ def default_backtrace_lines?(value)
132
+ value == Core::MAX_BACKTRACE_LINES
133
+ end
134
+
135
+ def default_defensive
136
+ # Hot-path defaults are reused by every emitted record.
137
+ @default_defensive ||= {
138
+ false => new(false, false, false, Core::MAX_BACKTRACE_LINES),
139
+ true => new(false, false, true, Core::MAX_BACKTRACE_LINES)
140
+ }.freeze
141
+ end
142
+
143
+ def default_pipeline
144
+ @default_pipeline ||= {
145
+ false => {
146
+ false => new(true, false, false, Core::MAX_BACKTRACE_LINES),
147
+ true => new(true, false, true, Core::MAX_BACKTRACE_LINES)
148
+ }.freeze,
149
+ true => {
150
+ false => new(true, true, false, Core::MAX_BACKTRACE_LINES),
151
+ true => new(true, true, true, Core::MAX_BACKTRACE_LINES)
152
+ }.freeze
153
+ }.freeze
154
+ end
155
+ end
156
+ end
157
+ private_constant :BuildOptions
158
+
159
+ def initialize(data, lineage: nil, freeze_sections: true)
160
+ @data = data
161
+ @freeze_sections = freeze_sections
162
+ @lineage = lineage || Execution::Lineage.from_execution_hash(@data[:execution])
163
+ validate!
164
+ end
165
+
166
+ def [](key) = @data[key]
167
+
168
+ def []=(key, value)
169
+ @lineage = nil if key == :execution
170
+ ensure_mutable_data!
171
+ @data[key] = value
172
+ @to_record = nil
173
+ end
174
+
175
+ def fetch(...) = @data.fetch(...)
176
+
177
+ def dig(...) = @data.dig(...)
178
+
179
+ def key?(key) = @data.key?(key)
180
+
181
+ def each(&) = @data.each(&)
182
+
183
+ def each_key(&) = @data.each_key(&)
184
+
185
+ def to_h = Fields::FieldSet.deep_dup(@data)
186
+
187
+ def transform_field!(key)
188
+ key = Fields::Internal.normalize_key(key)
189
+ replace_transformed_field!(key, yield(@data[key]))
190
+ self
191
+ end
192
+
193
+ def transform_section!(key)
194
+ key = Fields::Internal.normalize_key(key)
195
+ section = @data[key]
196
+ replacement = yield(section)
197
+ raise TypeError, "record #{key} must be a Hash" unless replacement.is_a?(Hash)
198
+
199
+ replace_transformed_field!(key, replacement)
200
+ self
201
+ end
202
+
203
+ def transform_record!
204
+ previous_lineage = @lineage
205
+ previous_identity = execution_lineage_identity(@data[:execution])
206
+ replacement = yield(@data)
207
+ @lineage = replacement_lineage_for(previous_lineage, previous_identity, replacement)
208
+ @data = replacement
209
+ @to_record = nil
210
+ self
211
+ end
212
+
213
+ Record::REQUIRED_KEYS.each do |key|
214
+ define_method(key) { @data[key] }
215
+ end
216
+
217
+ def validate!
218
+ Record.validate_normalized_hash!(@data)
219
+ self
220
+ end
221
+
222
+ def lineage = (@lineage ||= Execution::Lineage.from_execution_hash(@data[:execution]))
223
+
224
+ def to_record
225
+ @to_record ||= Record.from_owned_hash(@data, lineage: @lineage, trust_frozen: @freeze_sections)
226
+ end
227
+
228
+ private
229
+
230
+ def replace_transformed_field!(key, value)
231
+ preserve_lineage = transformed_field_lineage(key, value)
232
+ ensure_mutable_data!
233
+ @data[key] = value
234
+ @lineage = preserve_lineage if key == :execution
235
+ @to_record = nil
236
+ end
237
+
238
+ def ensure_mutable_data!
239
+ @data = Fields::FieldSet.deep_dup(@data) if @data.frozen?
240
+ end
241
+
242
+ def transformed_field_lineage(key, value)
243
+ return @lineage unless key == :execution
244
+
245
+ replacement_lineage_for(
246
+ @lineage,
247
+ execution_lineage_identity(@data[:execution]),
248
+ @data.merge(execution: value)
249
+ )
250
+ end
251
+
252
+ def replacement_lineage_for(lineage, previous, data)
253
+ return unless lineage
254
+
255
+ current = data.is_a?(Hash) ? execution_lineage_identity(data[:execution]) : nil
256
+ lineage if previous == current
257
+ end
258
+
259
+ def execution_lineage_identity(execution)
260
+ return unless execution.is_a?(Hash)
261
+
262
+ normalized = Fields::FieldSet.deep_symbolize_keys(execution)
263
+ LINEAGE_IDENTITY_KEYS.each_with_object({}) do |key, identity|
264
+ identity[key] = normalized[key] if normalized.key?(key)
265
+ end
266
+ end
267
+
268
+ class BuildInput
269
+ # Normalizes raw emit input into draft top-level fields plus payload.
270
+ RECORD_INPUT_KEYS = Record::REQUIRED_KEYS.freeze
271
+ RECORD_INPUT_KEY_SET = RECORD_INPUT_KEYS.to_h { [it, true] }.freeze
272
+ private_constant :RECORD_INPUT_KEYS
273
+ private_constant :RECORD_INPUT_KEY_SET
274
+
275
+ class << self
276
+ def call(input, owned:)
277
+ return {} if input.nil?
278
+ return input if owned && input.is_a?(Hash)
279
+ return shallow_symbolize(input) if RawInput.hash_input?(input)
280
+
281
+ { message: input.to_s }
282
+ end
283
+
284
+ private
285
+
286
+ def shallow_symbolize(input)
287
+ normalized = {}
288
+ payload_fields = {}
289
+ input.each do |key, raw_value|
290
+ normalized_key = Fields::Internal.normalize_key(key)
291
+ value = raw_value.equal?(input) ? CIRCULAR_REFERENCE : raw_value
292
+ if RECORD_INPUT_KEY_SET.key?(normalized_key)
293
+ normalized[normalized_key] = value
294
+ else
295
+ payload_fields[normalized_key] = value
296
+ end
297
+ end
298
+ merge_unknown_payload!(normalized, payload_fields)
299
+ normalized
300
+ end
301
+
302
+ def merge_unknown_payload!(normalized, payload_fields)
303
+ return if payload_fields.empty?
304
+
305
+ normalized[:payload] = if normalized.key?(:payload)
306
+ merge_payload_input(normalized[:payload], payload_fields)
307
+ else
308
+ payload_fields
309
+ end
310
+ end
311
+
312
+ def merge_payload_input(explicit_payload, unknown_payload)
313
+ if explicit_payload.is_a?(Hash)
314
+ Fields::FieldSet.merge(unknown_payload, explicit_payload)
315
+ else
316
+ Fields::FieldSet.merge(unknown_payload, Fields::FieldSet::VALUE_KEY => explicit_payload)
317
+ end
318
+ end
319
+ end
320
+ end
321
+ private_constant :BuildInput
322
+
323
+ class Builder
324
+ EMPTY_HASH = {}.freeze
325
+ private_constant :EMPTY_HASH
326
+
327
+ def initialize(input = {}, context:, neutral:, attributes:, carry:, static_labels:, scope:, # rubocop:disable Metrics/ParameterLists
328
+ invalid_severity_reporter:, options:)
329
+ @input_owned = options.input_owned
330
+ @input = BuildInput.call(input, owned: @input_owned)
331
+ @context = context || {}
332
+ @neutral = neutral || {}
333
+ @attributes = attributes || {}
334
+ @carry = carry || {}
335
+ @static_labels = static_labels || {}
336
+ @fields_owned = options.fields_owned
337
+ @freeze_sections = options.freeze_sections
338
+ @error_backtrace_lines = options.error_backtrace_lines
339
+ @invalid_severity_reporter = invalid_severity_reporter
340
+ @scope = scope
341
+ end
342
+
343
+ def to_h
344
+ source = normalized_value(:source)
345
+ event = immutable_scalar_value(event_value.to_s)
346
+
347
+ base_record(source, event)
348
+ end
349
+
350
+ def lineage
351
+ @lineage ||= @scope&.lineage || Execution::Lineage.from_execution_hash(input_execution_hash)
352
+ end
353
+
354
+ private
355
+
356
+ def base_record(source, event)
357
+ {
358
+ timestamp: timestamp_value,
359
+ severity: severity_for(source, event),
360
+ kind: kind_for(value(:kind)),
361
+ event: event,
362
+ message: normalized_value(:message),
363
+ logger: normalized_value(:logger),
364
+ source: source,
365
+ execution: execution_hash,
366
+ context: context_hash,
367
+ carry: carry_hash,
368
+ neutral: neutral_hash,
369
+ attributes: attributes_hash,
370
+ labels: labels_hash,
371
+ payload: hash_value(:payload),
372
+ metrics: hash_value(:metrics),
373
+ error: normalize_error(value(:error))
374
+ }
375
+ end
376
+
377
+ def event_value
378
+ raw_value = value(:event)
379
+ raw_value.nil? ? "log" : raw_value
380
+ end
381
+
382
+ def timestamp_value
383
+ raw_value = value(:timestamp)
384
+ Serialization::ValueCopy.call(raw_value.nil? ? Time.now.utc : raw_value, freeze_values: true)
385
+ end
386
+
387
+ def severity_for(source, event)
388
+ return normalize_record_severity(value(:severity), source: source, event: event) if present?(:severity)
389
+
390
+ :info
391
+ end
392
+
393
+ def normalize_record_severity(raw_value, source:, event:)
394
+ Records::Severity.normalize(raw_value)
395
+ rescue ArgumentError
396
+ # Below-threshold raw inputs warn before draft construction.
397
+ @invalid_severity_reporter.call(raw_value, source: source, event: event)
398
+ :info
399
+ end
400
+
401
+ def kind_for(kind)
402
+ return :point if kind.nil?
403
+ return kind if Record::KINDS.value?(kind)
404
+
405
+ Record::KINDS.fetch(kind.to_s) do
406
+ raise ArgumentError, "unsupported record kind: #{kind.inspect}"
407
+ end
408
+ end
409
+
410
+ def execution_hash
411
+ return base_execution_hash unless present?(:execution)
412
+
413
+ merge_section(scope_execution_hash, :execution)
414
+ end
415
+
416
+ def base_execution_hash
417
+ return scope_frozen_execution_hash if owned_frozen_scope_execution?
418
+
419
+ normalized_hash(scope_execution_hash)
420
+ end
421
+
422
+ def input_execution_hash
423
+ present?(:execution) ? hash_value(:execution) : scope_execution_hash
424
+ end
425
+
426
+ def context_hash
427
+ section_hash(@context, :context)
428
+ end
429
+
430
+ def carry_hash
431
+ section_hash(@carry, :carry)
432
+ end
433
+
434
+ def attributes_hash
435
+ section_hash(@attributes, :attributes)
436
+ end
437
+
438
+ def neutral_hash
439
+ section_hash(@neutral, :neutral)
440
+ end
441
+
442
+ def labels_hash
443
+ section_hash(labels_base, :labels)
444
+ end
445
+
446
+ def merge_section(base, key)
447
+ return merge_owned_section(base, key) if @input_owned
448
+
449
+ value = hash_value(key)
450
+ value = Execution::Lineage.clean_execution_hash(value) if key == :execution
451
+ return normalized_hash(value) if base.empty?
452
+
453
+ base = Fields::FieldSet.deep_dup(base)
454
+ merged = if key == :attributes
455
+ Fields::Internal.deep_merge!(base, value)
456
+ else
457
+ Fields::FieldSet.merge!(base, value)
458
+ end
459
+ normalized_hash(merged)
460
+ end
461
+
462
+ def merge_owned_section(base, key)
463
+ value = hash_value(key)
464
+ value = clean_owned_execution_hash(value) if key == :execution
465
+ return normalized_hash(value) if base.empty?
466
+
467
+ base = Fields::FieldSet.deep_dup(base)
468
+ merged = if key == :attributes
469
+ Fields::Internal.deep_merge_owned!(base, value)
470
+ else
471
+ Fields::Internal.merge_owned!(base, value)
472
+ end
473
+ normalized_hash(merged)
474
+ end
475
+
476
+ def clean_owned_execution_hash(value)
477
+ return Execution::Lineage.clean_execution_hash(value) if @freeze_sections
478
+
479
+ Execution::Lineage.clean_owned_execution_hash(value)
480
+ end
481
+
482
+ def section_hash(base, key)
483
+ return base if owned_frozen_section?(base) && !present?(key)
484
+ return normalized_hash(base) unless present?(key)
485
+
486
+ merge_section(base, key)
487
+ end
488
+
489
+ def owned_frozen_section?(base)
490
+ @fields_owned && @freeze_sections && base.is_a?(Hash) && base.frozen?
491
+ end
492
+
493
+ def owned_frozen_scope_execution?
494
+ @scope && @fields_owned && @freeze_sections
495
+ end
496
+
497
+ def labels_base
498
+ return scope_labels_hash if @static_labels.empty?
499
+
500
+ Fields::FieldSet.merge!(Fields::FieldSet.deep_dup(@static_labels), scope_labels_hash)
501
+ end
502
+
503
+ def hash_value(key)
504
+ return empty_hash unless present?(key)
505
+
506
+ raw_value = value(key)
507
+ return normalized_hash(raw_value) if raw_value.is_a?(Hash)
508
+
509
+ normalized_hash(Fields::FieldSet::VALUE_KEY => raw_value)
510
+ end
511
+
512
+ def value(key)
513
+ @input[key]
514
+ end
515
+
516
+ def normalized_value(key)
517
+ immutable_scalar_value(value(key))
518
+ end
519
+
520
+ def immutable_scalar_value(value)
521
+ return value unless value.is_a?(String)
522
+ return value if value.frozen?
523
+ return value.dup unless @freeze_sections
524
+ return value.freeze if @input_owned
525
+
526
+ value.dup.freeze
527
+ end
528
+
529
+ def present?(key)
530
+ @input.key?(key)
531
+ end
532
+
533
+ def normalize_error(error)
534
+ case error
535
+ when nil
536
+ nil
537
+ when Exception
538
+ normalized_hash(Serialization::ExceptionShape.call(error, max_backtrace_lines: @error_backtrace_lines))
539
+ when Hash
540
+ normalized_hash(normalize_error_hash(error))
541
+ else
542
+ normalized_hash(message: error.to_s)
543
+ end
544
+ end
545
+
546
+ def normalize_error_hash(error)
547
+ Serialization::BacktraceLimiter.call(
548
+ Fields::FieldSet.deep_symbolize_keys(error),
549
+ max_backtrace_lines: @error_backtrace_lines
550
+ )
551
+ end
552
+
553
+ def normalized_hash(value)
554
+ return empty_hash if value.is_a?(Hash) && value.empty?
555
+
556
+ return value if @input_owned && !@freeze_sections && value.is_a?(Hash)
557
+ return Fields::Internal.frozen_deep_symbolize_keys(value) if @freeze_sections
558
+
559
+ Fields::FieldSet.deep_symbolize_keys(value)
560
+ end
561
+
562
+ def empty_hash
563
+ @freeze_sections ? EMPTY_HASH : {}
564
+ end
565
+
566
+ def scope_execution_hash = @scope ? @scope.frozen_execution_hash : EMPTY_HASH
567
+
568
+ def scope_frozen_execution_hash = @scope.frozen_execution_hash
569
+
570
+ def scope_labels_hash = @scope ? @scope.frozen_labels_hash : EMPTY_HASH
571
+ end
572
+ private_constant :Builder
573
+ end
574
+ end
575
+ end
576
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Records
6
+ # @api extension
7
+ class Formatter
8
+ def call(record)
9
+ PublicProjection.new(record)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Records
6
+ module LazyEmitInput
7
+ # Merges deferred emit block output with eager severity/input fields.
8
+ class SeverityInput
9
+ include Enumerable
10
+
11
+ def initialize(severity, input)
12
+ @severity = severity
13
+ @input = input
14
+ end
15
+
16
+ def key?(key)
17
+ severity_key?(key) || input_hash.key?(key) || input_hash.key?(key.to_s)
18
+ end
19
+
20
+ def [](key)
21
+ return @severity if severity_key?(key)
22
+
23
+ RawInput.value(input_hash, key)
24
+ end
25
+
26
+ def each
27
+ return enum_for(:each) unless block_given?
28
+
29
+ input_hash.each do |key, value|
30
+ yield key, value unless severity_key?(key)
31
+ end
32
+ yield :severity, @severity
33
+ end
34
+
35
+ def to_h
36
+ each_with_object({}) do |(key, value), hash|
37
+ hash[key] = value
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def input_hash
44
+ @input_hash ||= @input.is_a?(Hash) ? @input : { message: @input.to_s }
45
+ end
46
+
47
+ def severity_key?(key)
48
+ RawInput.severity_key?(key)
49
+ end
50
+ end
51
+ private_constant :SeverityInput
52
+
53
+ class << self
54
+ def call(input)
55
+ lazy_value = yield
56
+ return input if lazy_value.nil?
57
+ return lazy_value if empty_input?(input)
58
+
59
+ eager = input_hash(input)
60
+ lazy = input_hash(lazy_value)
61
+ lazy = without_severity(lazy) if explicit_severity?(eager)
62
+ Fields::FieldSet.merge(eager, lazy)
63
+ end
64
+
65
+ def with_severity(severity, input)
66
+ SeverityInput.new(severity, input)
67
+ end
68
+
69
+ def input?(value)
70
+ value.is_a?(SeverityInput)
71
+ end
72
+
73
+ private
74
+
75
+ def empty_input?(input)
76
+ input.nil? || (input.is_a?(Hash) && input.empty?)
77
+ end
78
+
79
+ def input_hash(value)
80
+ return value if value.is_a?(Hash)
81
+ return value.to_h if input?(value)
82
+
83
+ { message: value.to_s }
84
+ end
85
+
86
+ def explicit_severity?(input)
87
+ RawInput.explicit_severity?(input)
88
+ end
89
+
90
+ def without_severity(input)
91
+ return input unless input.is_a?(Hash)
92
+
93
+ RawInput.without_severity_keys(input)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Records
6
+ module Metadata
7
+ class << self
8
+ def call(record)
9
+ return {} unless record.respond_to?(:key?) && record.respond_to?(:[])
10
+
11
+ {
12
+ event: record[:event],
13
+ labels: record[:labels].is_a?(Hash) ? Fields::FieldSet.deep_dup(record[:labels]) : {},
14
+ logger: record[:logger],
15
+ severity: record[:severity],
16
+ source: record[:source]
17
+ }.compact
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end