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,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,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
|