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,360 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Processing
|
|
6
|
+
class Pipeline # rubocop:disable Metrics/ClassLength -- Pipeline owns normalization, processors, and destinations.
|
|
7
|
+
COUNTER_KEYS = %i[
|
|
8
|
+
callback_error
|
|
9
|
+
entered
|
|
10
|
+
level_dropped
|
|
11
|
+
no_output_dropped
|
|
12
|
+
processor_dropped
|
|
13
|
+
processor_error
|
|
14
|
+
].freeze
|
|
15
|
+
EMPTY_HASH = {}.freeze
|
|
16
|
+
private_constant :EMPTY_HASH
|
|
17
|
+
|
|
18
|
+
# @api integration_spi
|
|
19
|
+
# Integration-facing raw/normalized record pipeline.
|
|
20
|
+
def initialize(configuration:, invalid_severity_reporter: Diagnostics::InvalidSeverityReporter.counter)
|
|
21
|
+
@on_drop = configuration.on_drop
|
|
22
|
+
@on_failure = configuration.on_failure
|
|
23
|
+
@invalid_severity_reporter = invalid_severity_reporter
|
|
24
|
+
@destinations = build_destinations(configuration)
|
|
25
|
+
@labels = Fields::FieldSet.deep_symbolize_keys(configuration.labels.to_h)
|
|
26
|
+
@threshold = build_threshold(configuration)
|
|
27
|
+
@error_backtrace_lines = configuration.error_backtrace_lines
|
|
28
|
+
Validation.validate_callable!(configuration.on_failure, name: :on_failure, allow_nil: true)
|
|
29
|
+
@processor_chain = build_processor_chain(configuration)
|
|
30
|
+
@processors_empty = @processor_chain.empty?
|
|
31
|
+
@labels_empty = @labels.empty?
|
|
32
|
+
initialize_tracking
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def emit(input = Core::UNSET, **fields, &)
|
|
36
|
+
emit_with_level_check(input, true, fields, &)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def emit_without_level(input = Core::UNSET, **fields, &)
|
|
40
|
+
emit_with_level_check(input, false, fields, &)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Trusted integration path for adapter-owned input hashes.
|
|
44
|
+
def emit_integration(input, enforce_level: true)
|
|
45
|
+
emit_input_with_guard(input, enforce_level: enforce_level, lazy: false) do
|
|
46
|
+
build_draft(input, input_owned: true)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Runtime summaries already carry their captured scope fields.
|
|
51
|
+
def emit_isolated_input(input, enforce_level: true)
|
|
52
|
+
emit_input_with_guard(input, enforce_level: enforce_level, lazy: false) do
|
|
53
|
+
build_isolated_draft(input)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Trusted normalized-record path used by runtime summaries and bridge envelopes.
|
|
58
|
+
def emit_record(record, enforce_level: true)
|
|
59
|
+
return if no_output_dropped?
|
|
60
|
+
|
|
61
|
+
degradation_marker = @health.degradation_marker
|
|
62
|
+
emit_validated_record(record, enforce_level: enforce_level)
|
|
63
|
+
finish_emit_attempt(degradation_marker)
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
record_emit_failure(e, record)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def after_fork!
|
|
69
|
+
initialize_tracking
|
|
70
|
+
@destinations.after_fork!
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def flush(timeout: nil)
|
|
75
|
+
@destinations.flush(timeout: timeout)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def close(timeout: nil, skip_resource_identities: nil)
|
|
79
|
+
@destinations.close(timeout: timeout, skip_resource_identities: skip_resource_identities)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def lifecycle_resource_identities
|
|
83
|
+
@destinations.lifecycle_resource_identities
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def health
|
|
87
|
+
{
|
|
88
|
+
configured: !@destinations.empty?,
|
|
89
|
+
counts: pipeline_counts_snapshot,
|
|
90
|
+
destinations: @destinations.health,
|
|
91
|
+
last_callback_failure: @health.last_callback_failure,
|
|
92
|
+
last_failure: @health.last_failure,
|
|
93
|
+
status: pipeline_status
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def build_destinations(configuration)
|
|
100
|
+
Destinations::Collection.build(
|
|
101
|
+
configuration: configuration,
|
|
102
|
+
defaults: destination_defaults(configuration),
|
|
103
|
+
on_drop: method(:record_destination_drop),
|
|
104
|
+
on_failure: method(:notify_failure)
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def destination_defaults(configuration)
|
|
109
|
+
{
|
|
110
|
+
encoder: Serialization::JsonEncoder.new(max_backtrace_lines: configuration.error_backtrace_lines),
|
|
111
|
+
formatter: Records::Formatter.new,
|
|
112
|
+
error_backtrace_lines: configuration.error_backtrace_lines,
|
|
113
|
+
on_drop: configuration.on_drop,
|
|
114
|
+
on_failure: configuration.on_failure
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_threshold(configuration)
|
|
119
|
+
LevelThreshold.new(
|
|
120
|
+
level: configuration.level,
|
|
121
|
+
invalid_severity_reporter: @invalid_severity_reporter
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_processor_chain(configuration)
|
|
126
|
+
ProcessorChain.new(
|
|
127
|
+
processors: configuration.processors.to_a.freeze,
|
|
128
|
+
error_backtrace_lines: @error_backtrace_lines,
|
|
129
|
+
on_error: method(:record_processor_error)
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def emit_with_level_check(input, enforce_level, fields, &)
|
|
134
|
+
input = Core.emit_input(input, fields)
|
|
135
|
+
|
|
136
|
+
emit_input_with_guard(input, enforce_level: enforce_level, lazy: block_given?) do
|
|
137
|
+
input = Records::LazyEmitInput.call(input, &) if block_given?
|
|
138
|
+
build_draft(input)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def emit_input_with_guard(input, enforce_level:, lazy:)
|
|
143
|
+
return if no_output_dropped?
|
|
144
|
+
|
|
145
|
+
degradation_marker = @health.degradation_marker
|
|
146
|
+
if raw_input_blocked?(input, enforce_level: enforce_level, lazy: lazy)
|
|
147
|
+
increment_pipeline_counter(:level_dropped)
|
|
148
|
+
else
|
|
149
|
+
emit_prepared_draft(yield, enforce_level: enforce_level, merge_static_labels: false)
|
|
150
|
+
end
|
|
151
|
+
finish_emit_attempt(degradation_marker)
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
notify_failure(e, phase: :emit)
|
|
154
|
+
emit_internal_error_record(e)
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def no_output_dropped?
|
|
159
|
+
return false unless @destinations.empty?
|
|
160
|
+
|
|
161
|
+
record_no_output_drop
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def finish_emit_attempt(degradation_marker)
|
|
166
|
+
clear_degradation_if_unchanged(degradation_marker)
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def emit_validated_record(record, enforce_level:)
|
|
171
|
+
Records::Record.validate_normalized!(record)
|
|
172
|
+
return emit_fast_record(record, enforce_level: enforce_level) if fast_record_path?
|
|
173
|
+
|
|
174
|
+
emit_prepared_draft(Records::Draft.from_record(record, freeze_sections: false),
|
|
175
|
+
enforce_level: enforce_level)
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def fast_record_path?
|
|
180
|
+
@labels_empty && @processors_empty
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def emit_fast_record(record, enforce_level:)
|
|
184
|
+
if enforce_level && !emit_record?(record)
|
|
185
|
+
increment_pipeline_counter(:level_dropped)
|
|
186
|
+
return
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
increment_pipeline_counter(:entered)
|
|
190
|
+
emit_to_destinations(record)
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def raw_input_blocked?(input, enforce_level:, lazy:)
|
|
195
|
+
enforce_level &&
|
|
196
|
+
(!lazy || Records::RawInput.explicit_severity?(input)) &&
|
|
197
|
+
!@threshold.raw_input_allowed?(input)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def build_draft(input, input_owned: false)
|
|
201
|
+
store = ContextStore.current
|
|
202
|
+
build_draft_from(
|
|
203
|
+
input,
|
|
204
|
+
input_owned: input_owned,
|
|
205
|
+
context: store.context_hash,
|
|
206
|
+
neutral: store.neutral_hash,
|
|
207
|
+
attributes: store.attributes_hash,
|
|
208
|
+
carry: store.carry_hash,
|
|
209
|
+
scope: store.current_scope_or_snapshot
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def build_isolated_draft(input, input_owned: false)
|
|
214
|
+
build_draft_from(
|
|
215
|
+
input,
|
|
216
|
+
input_owned: input_owned,
|
|
217
|
+
context: EMPTY_HASH,
|
|
218
|
+
neutral: EMPTY_HASH,
|
|
219
|
+
attributes: EMPTY_HASH,
|
|
220
|
+
carry: EMPTY_HASH,
|
|
221
|
+
scope: nil
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def build_draft_from(input, input_owned:, context:, neutral:, attributes:, carry:, scope:)
|
|
226
|
+
Records::Draft.build_pipeline_owned(
|
|
227
|
+
input,
|
|
228
|
+
context: context,
|
|
229
|
+
neutral: neutral,
|
|
230
|
+
attributes: attributes,
|
|
231
|
+
carry: carry,
|
|
232
|
+
static_labels: @labels,
|
|
233
|
+
input_owned: input_owned,
|
|
234
|
+
freeze_sections: @processors_empty,
|
|
235
|
+
scope: scope,
|
|
236
|
+
error_backtrace_lines: @error_backtrace_lines,
|
|
237
|
+
invalid_severity_reporter: @invalid_severity_reporter
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def initialize_tracking
|
|
242
|
+
@health = Diagnostics::Health.new(
|
|
243
|
+
counter_keys: COUNTER_KEYS,
|
|
244
|
+
callback_metadata: {},
|
|
245
|
+
callback_failure_counter: :callback_error
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def pipeline_status
|
|
250
|
+
return :unconfigured if @destinations.empty?
|
|
251
|
+
|
|
252
|
+
@health.degraded? ? :degraded : :ok
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def clear_degradation_if_unchanged(marker)
|
|
256
|
+
@health.clear_degradation_if_unchanged(marker)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def merge_static_labels(draft)
|
|
260
|
+
return draft if @labels_empty
|
|
261
|
+
|
|
262
|
+
draft[:labels] = Fields::FieldSet.merge(@labels, draft.fetch(:labels))
|
|
263
|
+
draft
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def emit_record?(record_or_draft)
|
|
267
|
+
@threshold.allow?(record_or_draft.fetch(:severity))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def emit_prepared_draft(draft, enforce_level:, merge_static_labels: true)
|
|
271
|
+
if enforce_level && !emit_record?(draft)
|
|
272
|
+
increment_pipeline_counter(:level_dropped)
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
draft = merge_static_labels(draft) if merge_static_labels
|
|
277
|
+
increment_pipeline_counter(:entered)
|
|
278
|
+
emit_processed_draft(draft, enforce_level: enforce_level)
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
record_emit_failure(e, draft)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def emit_processed_draft(draft, enforce_level:)
|
|
284
|
+
return emit_to_destinations(draft.to_record) if @processors_empty
|
|
285
|
+
|
|
286
|
+
processed = @processor_chain.call(draft)
|
|
287
|
+
if processed.equal?(ProcessorChain::DROP)
|
|
288
|
+
increment_pipeline_counter(:processor_dropped)
|
|
289
|
+
return
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
processed, enforce_processed_level = processed_draft_and_level(processed, enforce_level)
|
|
293
|
+
|
|
294
|
+
if enforce_processed_level && !emit_record?(processed)
|
|
295
|
+
increment_pipeline_counter(:level_dropped)
|
|
296
|
+
return
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
emit_to_destinations(processed.to_record)
|
|
300
|
+
end
|
|
301
|
+
|
|
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
|
+
def record_processor_error(error, record_metadata)
|
|
311
|
+
increment_pipeline_counter(:processor_error)
|
|
312
|
+
notify_failure(error, phase: :processor, record_metadata: record_metadata)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def emit_internal_error_record(error)
|
|
316
|
+
emit_prepared_draft(
|
|
317
|
+
Diagnostics::InternalRecords.emit_error(error, error_backtrace_lines: @error_backtrace_lines),
|
|
318
|
+
enforce_level: false
|
|
319
|
+
)
|
|
320
|
+
rescue StandardError => e
|
|
321
|
+
notify_failure(e, phase: :internal_error_record)
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def emit_to_destinations(record)
|
|
326
|
+
@destinations.emit(record)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def record_no_output_drop
|
|
330
|
+
increment_pipeline_counter(:no_output_dropped)
|
|
331
|
+
nil
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def notify_failure(error, **metadata)
|
|
335
|
+
@health.record_failure(error, callback: @on_failure, **metadata)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def record_destination_drop(reason, **metadata)
|
|
339
|
+
callback_result = Diagnostics::CallbackNotifier.call(@on_drop, reason, metadata.merge(reason: reason))
|
|
340
|
+
return unless Diagnostics::CallbackNotifier.failure?(callback_result)
|
|
341
|
+
|
|
342
|
+
@health.record_callback_failure(callback_result)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def record_emit_failure(error, record)
|
|
346
|
+
notify_failure(error, phase: :emit_record, record_metadata: Records::Metadata.call(record))
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def pipeline_counts_snapshot
|
|
351
|
+
@health.counts
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def increment_pipeline_counter(key)
|
|
355
|
+
@health.increment(key)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Processing
|
|
6
|
+
class ProcessorChain
|
|
7
|
+
DROP = Core.sentinel(:drop)
|
|
8
|
+
ErrorResult = Data.define(:draft)
|
|
9
|
+
|
|
10
|
+
def initialize(processors:, error_backtrace_lines:, on_error:)
|
|
11
|
+
@processors = processors
|
|
12
|
+
@error_backtrace_lines = error_backtrace_lines
|
|
13
|
+
@on_error = on_error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def empty? = @processors.empty?
|
|
17
|
+
|
|
18
|
+
def call(draft)
|
|
19
|
+
current = draft
|
|
20
|
+
|
|
21
|
+
@processors.each do |processor|
|
|
22
|
+
current = apply_processor_result(current, processor.call(current))
|
|
23
|
+
break if current == :drop
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
action = handle_processor_error(processor, e, current)
|
|
26
|
+
case action
|
|
27
|
+
when :continue
|
|
28
|
+
next
|
|
29
|
+
when :drop
|
|
30
|
+
return DROP
|
|
31
|
+
else
|
|
32
|
+
return action
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
current == :drop ? DROP : current
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def apply_processor_result(current, result)
|
|
42
|
+
return :drop if result == :drop
|
|
43
|
+
return result if result.is_a?(Records::Draft)
|
|
44
|
+
|
|
45
|
+
current
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def handle_processor_error(processor, error, current)
|
|
49
|
+
record_metadata = Records::Metadata.call(current)
|
|
50
|
+
@on_error.call(error, record_metadata)
|
|
51
|
+
|
|
52
|
+
return :continue if processor.on_error == ProcessorWrapper::FAIL_OPEN
|
|
53
|
+
return :drop if processor.on_error == ProcessorWrapper::DROP
|
|
54
|
+
|
|
55
|
+
ErrorResult.new(processor_error_record(processor, error, record_metadata))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def processor_error_record(processor, error, record_metadata)
|
|
59
|
+
Diagnostics::InternalRecords.processor_error(
|
|
60
|
+
processor_name: processor.processor_name,
|
|
61
|
+
error: error,
|
|
62
|
+
record_metadata: record_metadata,
|
|
63
|
+
error_backtrace_lines: @error_backtrace_lines
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Processing
|
|
6
|
+
class ProcessorRegistry
|
|
7
|
+
Entry = Data.define(:entry, :arguments, :options, :on_error, :factory)
|
|
8
|
+
|
|
9
|
+
def initialize(entries = [])
|
|
10
|
+
@entries = entries.map { normalize_entry(it) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def use(processor = nil, *arguments, **options, &block)
|
|
14
|
+
add(resolve_processor(processor, block), arguments, options, prepend: false)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def prepend(processor = nil, *arguments, **options, &block)
|
|
18
|
+
add(resolve_processor(processor, block), arguments, options, prepend: true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clear
|
|
22
|
+
@entries.clear
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_a
|
|
27
|
+
@entries.map { materialize(it) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def copy
|
|
31
|
+
self.class.new(@entries)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def freeze
|
|
35
|
+
@entries.freeze
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def resolve_processor(processor, block)
|
|
42
|
+
raise ArgumentError, "pass processor or block, not both" if processor && block
|
|
43
|
+
|
|
44
|
+
processor || block
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add(processor, arguments, options, prepend:)
|
|
48
|
+
raise ArgumentError, "processor or block is required" unless processor
|
|
49
|
+
|
|
50
|
+
entry = build_entry(processor, arguments, options)
|
|
51
|
+
prepend ? @entries.unshift(entry) : @entries << entry
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_entry(processor, arguments = [], options = {})
|
|
56
|
+
options = options.dup
|
|
57
|
+
on_error = ProcessorWrapper.normalize_policy(
|
|
58
|
+
options.delete(:on_error) { ProcessorWrapper::FAIL_CLOSED }
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if factory_processor?(processor)
|
|
62
|
+
Entry.new(
|
|
63
|
+
entry: processor.to_sym,
|
|
64
|
+
arguments: arguments.dup.freeze,
|
|
65
|
+
options: options.freeze,
|
|
66
|
+
on_error: on_error,
|
|
67
|
+
factory: true
|
|
68
|
+
)
|
|
69
|
+
elsif processor.is_a?(Class)
|
|
70
|
+
Entry.new(
|
|
71
|
+
entry: processor,
|
|
72
|
+
arguments: arguments.dup.freeze,
|
|
73
|
+
options: options.freeze,
|
|
74
|
+
on_error: on_error,
|
|
75
|
+
factory: false
|
|
76
|
+
)
|
|
77
|
+
else
|
|
78
|
+
validate_processor_object!(processor, arguments, options)
|
|
79
|
+
Entry.new(entry: processor, arguments: [].freeze, options: {}.freeze, on_error: on_error, factory: false)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def normalize_entry(entry)
|
|
84
|
+
entry.is_a?(Entry) ? entry : build_entry(entry)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def materialize(entry)
|
|
88
|
+
processor = if entry.factory
|
|
89
|
+
Processing.build(entry.entry, *entry.arguments, **entry.options)
|
|
90
|
+
elsif entry.entry.is_a?(Class)
|
|
91
|
+
entry.entry.new(*entry.arguments, **entry.options)
|
|
92
|
+
else
|
|
93
|
+
entry.entry
|
|
94
|
+
end
|
|
95
|
+
ProcessorWrapper.new(processor, on_error: entry.on_error)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def factory_processor?(processor)
|
|
99
|
+
processor.respond_to?(:to_sym) && Processing.factory_for(processor)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_processor_object!(processor, arguments, options)
|
|
103
|
+
raise ArgumentError, "unknown processor kind #{processor.to_sym.inspect}" if processor.respond_to?(:to_sym)
|
|
104
|
+
|
|
105
|
+
raise ArgumentError, "processor constructor arguments require a class" unless arguments.empty?
|
|
106
|
+
raise ArgumentError, "processor options require a class" unless options.empty?
|
|
107
|
+
|
|
108
|
+
return if processor.respond_to?(:call)
|
|
109
|
+
|
|
110
|
+
raise ArgumentError, "processor must respond to call"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Processing
|
|
6
|
+
class ProcessorWrapper
|
|
7
|
+
FAIL_CLOSED = :fail_closed
|
|
8
|
+
FAIL_OPEN = :fail_open
|
|
9
|
+
DROP = :drop
|
|
10
|
+
POLICIES = [FAIL_CLOSED, FAIL_OPEN, DROP].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :on_error
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def normalize_policy(value)
|
|
16
|
+
Validation.validate_symbol_choice!(value, name: "processor on_error", choices: POLICIES)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(processor, on_error: FAIL_CLOSED)
|
|
21
|
+
validate_processor!(processor)
|
|
22
|
+
@processor = processor
|
|
23
|
+
@on_error = self.class.normalize_policy(on_error)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(...)
|
|
27
|
+
@processor.call(...)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def processor_name
|
|
31
|
+
@processor.class.name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate_processor!(processor)
|
|
37
|
+
return if processor.respond_to?(:call)
|
|
38
|
+
|
|
39
|
+
raise ArgumentError, "processor must respond to call"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Processing
|
|
6
|
+
class RecordFieldTransform
|
|
7
|
+
CONTAINER_KEYS = Fields::Bags.transform_container_sections
|
|
8
|
+
SCALAR_KEYS = (Records::Record::REQUIRED_KEYS - CONTAINER_KEYS).freeze
|
|
9
|
+
CONTAINER_KEY_SET = CONTAINER_KEYS.to_h { [it, true] }.freeze
|
|
10
|
+
SCALAR_KEY_SET = SCALAR_KEYS.to_h { [it, true] }.freeze
|
|
11
|
+
private_constant :CONTAINER_KEYS
|
|
12
|
+
private_constant :SCALAR_KEYS
|
|
13
|
+
private_constant :CONTAINER_KEY_SET
|
|
14
|
+
private_constant :SCALAR_KEY_SET
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def container_keys = CONTAINER_KEYS
|
|
18
|
+
|
|
19
|
+
def scalar_keys = SCALAR_KEYS
|
|
20
|
+
|
|
21
|
+
def container_key?(key) = CONTAINER_KEY_SET.key?(key)
|
|
22
|
+
|
|
23
|
+
def scalar_key?(key) = SCALAR_KEY_SET.key?(key)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(
|
|
27
|
+
max_array_items: nil,
|
|
28
|
+
max_depth: nil,
|
|
29
|
+
max_hash_keys: nil,
|
|
30
|
+
max_string_bytes: nil,
|
|
31
|
+
preserve_top_level_keys: nil,
|
|
32
|
+
track_paths: false
|
|
33
|
+
)
|
|
34
|
+
@bounded_options = bounded_options(
|
|
35
|
+
max_array_items: max_array_items,
|
|
36
|
+
max_depth: max_depth,
|
|
37
|
+
max_hash_keys: max_hash_keys,
|
|
38
|
+
max_string_bytes: max_string_bytes
|
|
39
|
+
)
|
|
40
|
+
@preserve_top_level_key_set = Array(preserve_top_level_keys).to_h { [it, true] }
|
|
41
|
+
@track_paths = track_paths
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(record, &)
|
|
45
|
+
record.each_with_object({}) do |(key, value), result|
|
|
46
|
+
result[key] = transform_record_field(key, value, record, &)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def transform_record_field(key, value, record, &)
|
|
53
|
+
return transform_container(key, value, record, &) if transform_container?(key, value)
|
|
54
|
+
return value unless transform_scalar?(key)
|
|
55
|
+
|
|
56
|
+
transform_scalar(key, value, record, &)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def transform_container?(key, value)
|
|
60
|
+
value.is_a?(Hash) && self.class.container_key?(key)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def transform_scalar?(key)
|
|
64
|
+
self.class.scalar_key?(key) && !@preserve_top_level_key_set.key?(key)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def transform_container(top_level_key, value, record, &)
|
|
68
|
+
Serialization::BoundedTransform.call(
|
|
69
|
+
value,
|
|
70
|
+
**@bounded_options,
|
|
71
|
+
track_paths: @track_paths
|
|
72
|
+
) do |item, key:, path:, depth:, **|
|
|
73
|
+
yield(
|
|
74
|
+
item,
|
|
75
|
+
key: key,
|
|
76
|
+
path: path,
|
|
77
|
+
prefixed_path: prefixed_path(path, top_level_key),
|
|
78
|
+
original: record,
|
|
79
|
+
depth: depth,
|
|
80
|
+
top_level_key: top_level_key
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def transform_scalar(top_level_key, value, record, &)
|
|
86
|
+
path = top_level_key.to_s if @track_paths
|
|
87
|
+
transformed = yield(
|
|
88
|
+
value,
|
|
89
|
+
key: top_level_key,
|
|
90
|
+
path: path,
|
|
91
|
+
prefixed_path: path,
|
|
92
|
+
original: record,
|
|
93
|
+
depth: 1,
|
|
94
|
+
top_level_key: top_level_key
|
|
95
|
+
)
|
|
96
|
+
transformed = value if transformed.equal?(Serialization::BoundedTransform::CONTINUE)
|
|
97
|
+
bound_value(transformed)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def bound_value(value)
|
|
101
|
+
Serialization::BoundedTransform.call(
|
|
102
|
+
value,
|
|
103
|
+
**@bounded_options
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def prefixed_path(path, top_level_key)
|
|
108
|
+
return unless path
|
|
109
|
+
|
|
110
|
+
"#{top_level_key}.#{path}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def bounded_options(max_array_items:, max_depth:, max_hash_keys:, max_string_bytes:)
|
|
114
|
+
{
|
|
115
|
+
max_array_items: max_array_items,
|
|
116
|
+
max_depth: max_depth,
|
|
117
|
+
max_hash_keys: max_hash_keys,
|
|
118
|
+
max_string_bytes: max_string_bytes
|
|
119
|
+
}.compact
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|