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,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Destinations
|
|
6
|
+
class Definition
|
|
7
|
+
OPTION_KEYS = %i[
|
|
8
|
+
close_output
|
|
9
|
+
encoder
|
|
10
|
+
formatter
|
|
11
|
+
max_record_bytes
|
|
12
|
+
name
|
|
13
|
+
on_drop
|
|
14
|
+
on_failure
|
|
15
|
+
output
|
|
16
|
+
processors
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
INHERIT = Core.sentinel(:inherit)
|
|
20
|
+
private_constant :INHERIT
|
|
21
|
+
|
|
22
|
+
attr_reader :kind, :name
|
|
23
|
+
|
|
24
|
+
def initialize(kind, **options)
|
|
25
|
+
@kind = Destinations.normalize_name(kind)
|
|
26
|
+
validate_options!(options)
|
|
27
|
+
@name = Destinations.normalize_name(options.fetch(:name, @kind))
|
|
28
|
+
@options = options.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build(defaults:, output_identities: nil)
|
|
32
|
+
return build_factory_destination(defaults, output_identities: output_identities) if factory
|
|
33
|
+
|
|
34
|
+
output = resolve(:output, defaults)
|
|
35
|
+
reject_shared_output!(output, output_identities) if output_identities && !output.nil?
|
|
36
|
+
|
|
37
|
+
Destination.new(
|
|
38
|
+
name: name,
|
|
39
|
+
close_output: resolve(:close_output, defaults),
|
|
40
|
+
encoder: resolve(:encoder, defaults),
|
|
41
|
+
formatter: resolve(:formatter, defaults),
|
|
42
|
+
max_record_bytes: resolve(:max_record_bytes, defaults),
|
|
43
|
+
on_drop: resolve(:on_drop, defaults),
|
|
44
|
+
on_failure: resolve(:on_failure, defaults),
|
|
45
|
+
output: output,
|
|
46
|
+
error_backtrace_lines: defaults.fetch(:error_backtrace_lines),
|
|
47
|
+
processors: resolve(:processors, defaults)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def copy
|
|
52
|
+
self.class.new(kind, **@options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def validate_options!(options)
|
|
58
|
+
return if factory
|
|
59
|
+
|
|
60
|
+
Validation.validate_options!(options, OPTION_KEYS, name: :destination)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_factory_destination(defaults, output_identities:)
|
|
64
|
+
destination = factory.call(**factory_options(defaults))
|
|
65
|
+
Registry.validate!(destination)
|
|
66
|
+
reject_shared_output!(resource_identity(destination), output_identities) if output_identities
|
|
67
|
+
destination
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resource_identity(destination)
|
|
71
|
+
return destination.resource_identity if destination.respond_to?(:resource_identity)
|
|
72
|
+
|
|
73
|
+
destination
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def factory_options(defaults)
|
|
77
|
+
options = @options.merge(name: name)
|
|
78
|
+
options[:on_drop] = defaults.fetch(:on_drop) if !options.key?(:on_drop) && defaults.key?(:on_drop)
|
|
79
|
+
options[:on_failure] = defaults.fetch(:on_failure) if !options.key?(:on_failure) && defaults.key?(:on_failure)
|
|
80
|
+
options
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def factory
|
|
84
|
+
@factory ||= Destinations.factory_for(kind)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def resolve(key, defaults)
|
|
88
|
+
value = @options.fetch(key) { INHERIT }
|
|
89
|
+
return default_value(key, defaults) if value.equal?(INHERIT)
|
|
90
|
+
|
|
91
|
+
value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def default_value(key, defaults)
|
|
95
|
+
return defaults.fetch(key) if defaults.key?(key)
|
|
96
|
+
|
|
97
|
+
case key
|
|
98
|
+
when :close_output
|
|
99
|
+
false
|
|
100
|
+
when :encoder, :formatter, :on_drop, :on_failure
|
|
101
|
+
raise ArgumentError, "destination default #{key} is required"
|
|
102
|
+
when :max_record_bytes
|
|
103
|
+
DEFAULT_MAX_RECORD_BYTES
|
|
104
|
+
when :processors
|
|
105
|
+
[]
|
|
106
|
+
when :output
|
|
107
|
+
# No inherited output exists; build raises the required-output error.
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def reject_shared_output!(output, output_identities)
|
|
113
|
+
previous_name = output_identities[output]
|
|
114
|
+
if previous_name
|
|
115
|
+
raise ArgumentError,
|
|
116
|
+
"destination #{name.inspect} shares output with destination #{previous_name.inspect}; " \
|
|
117
|
+
"use a transport adapter for shared sinks"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
output_identities[output] = name
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Destinations
|
|
6
|
+
class Destination
|
|
7
|
+
COUNTER_KEYS = %i[
|
|
8
|
+
callback_error
|
|
9
|
+
encode_error
|
|
10
|
+
formatter_error
|
|
11
|
+
formatted
|
|
12
|
+
output_accepted
|
|
13
|
+
output_error
|
|
14
|
+
output_exception
|
|
15
|
+
output_rejected
|
|
16
|
+
processor_dropped
|
|
17
|
+
processor_error
|
|
18
|
+
received
|
|
19
|
+
record_too_large
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :name
|
|
23
|
+
|
|
24
|
+
def initialize( # rubocop:disable Metrics/ParameterLists -- Destination definitions pass normalized settings.
|
|
25
|
+
name:,
|
|
26
|
+
close_output:,
|
|
27
|
+
encoder:,
|
|
28
|
+
formatter:,
|
|
29
|
+
max_record_bytes:,
|
|
30
|
+
on_drop:,
|
|
31
|
+
on_failure:,
|
|
32
|
+
output:,
|
|
33
|
+
error_backtrace_lines: Core::MAX_BACKTRACE_LINES,
|
|
34
|
+
processors: []
|
|
35
|
+
)
|
|
36
|
+
@name = Destinations.normalize_name(name)
|
|
37
|
+
@formatter = validate_callable(formatter, name: :formatter)
|
|
38
|
+
@encoder = validate_callable(encoder, name: :encoder)
|
|
39
|
+
Validation.validate_byte_limit!(max_record_bytes, name: :max_record_bytes)
|
|
40
|
+
@max_record_bytes = max_record_bytes
|
|
41
|
+
@on_drop = validate_optional_callback(on_drop, name: :on_drop)
|
|
42
|
+
@on_failure = validate_optional_callback(on_failure, name: :on_failure)
|
|
43
|
+
raise ArgumentError, "destination #{@name.inspect} output is required" if output.nil?
|
|
44
|
+
|
|
45
|
+
@output = Sink.wrap(output, close_output: close_output)
|
|
46
|
+
@processor_chain = processor_chain(processors, error_backtrace_lines)
|
|
47
|
+
initialize_tracking
|
|
48
|
+
@write_step = build_write_step
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def emit(record)
|
|
52
|
+
degradation_marker = @health.degradation_marker
|
|
53
|
+
record = process_record(record)
|
|
54
|
+
return unless record
|
|
55
|
+
|
|
56
|
+
emit_processed_record(record, degradation_marker: degradation_marker)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def emit_processed_record(record, degradation_marker:)
|
|
60
|
+
return unless @write_step.call(record) == :accepted
|
|
61
|
+
|
|
62
|
+
clear_degradation_if_unchanged(degradation_marker)
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def flush(timeout: nil)
|
|
67
|
+
call_output_lifecycle(:flush, timeout: timeout)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def close(timeout: nil)
|
|
71
|
+
call_output_lifecycle(:close, timeout: timeout)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def after_fork!
|
|
75
|
+
initialize_tracking
|
|
76
|
+
@output.after_fork! if @output.respond_to?(:after_fork!)
|
|
77
|
+
self
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
notify_failure(
|
|
80
|
+
e,
|
|
81
|
+
action: :after_fork,
|
|
82
|
+
output_class: output_class_name,
|
|
83
|
+
phase: :output_lifecycle
|
|
84
|
+
)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resource_identity
|
|
89
|
+
return @output.resource_identity if @output.respond_to?(:resource_identity)
|
|
90
|
+
|
|
91
|
+
@output
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def health
|
|
95
|
+
{
|
|
96
|
+
counts: counts_health,
|
|
97
|
+
last_callback_failure: @health.last_callback_failure,
|
|
98
|
+
last_failure: @health.last_failure,
|
|
99
|
+
last_loss: @health.last_loss,
|
|
100
|
+
status: degraded? ? :degraded : :ok
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def counts_health
|
|
107
|
+
@health.counts
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def degraded?
|
|
111
|
+
@health.degraded?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def initialize_tracking
|
|
115
|
+
@health = Diagnostics::Health.new(
|
|
116
|
+
counter_keys: COUNTER_KEYS,
|
|
117
|
+
callback_metadata: { destination: name },
|
|
118
|
+
callback_failure_counter: :callback_error
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_write_step
|
|
123
|
+
WriteStep.new(
|
|
124
|
+
formatter: @formatter,
|
|
125
|
+
encoder: @encoder,
|
|
126
|
+
output: @output,
|
|
127
|
+
max_record_bytes: @max_record_bytes,
|
|
128
|
+
increment: method(:increment_counter),
|
|
129
|
+
failure: method(:record_write_step_failure),
|
|
130
|
+
loss: method(:record_write_step_loss),
|
|
131
|
+
output_class_name: method(:output_class_name)
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def notify_failure(error, **metadata)
|
|
136
|
+
@health.record_failure(error, callback: @on_failure, **metadata)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def record_write_step_failure(error, metadata)
|
|
140
|
+
notify_failure(error, **record_step_metadata(metadata))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def record_write_step_loss(reason, metadata)
|
|
144
|
+
record_drop(reason, **record_step_metadata(metadata))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def record_step_metadata(metadata)
|
|
148
|
+
record = metadata.delete(:record)
|
|
149
|
+
metadata[:record_metadata] = Records::Metadata.call(record) if record
|
|
150
|
+
metadata
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def record_drop(reason, **metadata)
|
|
154
|
+
record_loss(reason, metadata)
|
|
155
|
+
callback_metadata = { destination: name, phase: :destination, reason: reason }.merge(metadata)
|
|
156
|
+
callback_result = Diagnostics::CallbackNotifier.call(@on_drop, reason, callback_metadata)
|
|
157
|
+
record_callback_error(callback_result) if Diagnostics::CallbackNotifier.failure?(callback_result)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def record_callback_error(callback_failure)
|
|
161
|
+
@health.record_callback_failure(callback_failure)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def increment_counter(key)
|
|
165
|
+
@health.increment(key)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def record_loss(reason, metadata)
|
|
169
|
+
record_metadata = metadata.fetch(:record_metadata, {})
|
|
170
|
+
@health.record_loss(
|
|
171
|
+
reason: reason,
|
|
172
|
+
counter: nil,
|
|
173
|
+
at: Time.now.utc,
|
|
174
|
+
event: record_metadata[:event],
|
|
175
|
+
severity: record_metadata[:severity],
|
|
176
|
+
source: record_metadata[:source]
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def clear_degradation
|
|
181
|
+
@health.clear_degradation
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def clear_degradation_if_unchanged(marker)
|
|
185
|
+
@health.clear_degradation_if_unchanged(marker)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def validate_callable(callable, name:)
|
|
189
|
+
Validation.validate_callable!(callable, name: name)
|
|
190
|
+
callable
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def validate_optional_callback(callback, name:)
|
|
194
|
+
Validation.validate_callable!(callback, name: name, allow_nil: true)
|
|
195
|
+
callback
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def processor_chain(processors, error_backtrace_lines)
|
|
199
|
+
processors = processor_entries(processors)
|
|
200
|
+
return if processors.empty?
|
|
201
|
+
|
|
202
|
+
Processing::ProcessorChain.new(
|
|
203
|
+
processors: processors,
|
|
204
|
+
error_backtrace_lines: error_backtrace_lines,
|
|
205
|
+
on_error: method(:record_processor_error)
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def processor_entries(value)
|
|
210
|
+
case value
|
|
211
|
+
when Processing::ProcessorRegistry
|
|
212
|
+
value.to_a
|
|
213
|
+
else
|
|
214
|
+
Processing::ProcessorRegistry.new(Array(value)).to_a
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def process_record(record)
|
|
219
|
+
return record unless @processor_chain
|
|
220
|
+
|
|
221
|
+
processed = @processor_chain.call(Records::Draft.from_record(record, freeze_sections: false))
|
|
222
|
+
if processed.equal?(Processing::ProcessorChain::DROP)
|
|
223
|
+
increment_counter(:processor_dropped)
|
|
224
|
+
nil
|
|
225
|
+
elsif processed.is_a?(Processing::ProcessorChain::ErrorResult)
|
|
226
|
+
processed.draft.to_record
|
|
227
|
+
else
|
|
228
|
+
processed.to_record
|
|
229
|
+
end
|
|
230
|
+
rescue StandardError => e
|
|
231
|
+
notify_failure(e, phase: :destination_processor, record_metadata: Records::Metadata.call(record))
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def record_processor_error(error, record_metadata)
|
|
236
|
+
increment_counter(:processor_error)
|
|
237
|
+
notify_failure(error, phase: :destination_processor, record_metadata: record_metadata)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def call_output_lifecycle(method_name, timeout:)
|
|
241
|
+
Validation.validate_timeout!(timeout, name: :timeout)
|
|
242
|
+
call_output_lifecycle_safely(method_name, timeout)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def call_output_lifecycle_safely(method_name, _timeout)
|
|
246
|
+
# Collection owns the shared deadline; plain outputs expose no timeout API.
|
|
247
|
+
result = @output.public_send(method_name)
|
|
248
|
+
clear_degradation if method_name == :flush && result != false
|
|
249
|
+
result
|
|
250
|
+
rescue StandardError => e
|
|
251
|
+
notify_failure(
|
|
252
|
+
e,
|
|
253
|
+
action: method_name,
|
|
254
|
+
output_class: output_class_name,
|
|
255
|
+
phase: :output_lifecycle
|
|
256
|
+
)
|
|
257
|
+
false
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def output_class_name
|
|
261
|
+
return @output.output_class_name if @output.respond_to?(:output_class_name)
|
|
262
|
+
|
|
263
|
+
@output.class.name
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Destinations
|
|
6
|
+
class Registry
|
|
7
|
+
DESTINATION_METHODS = %i[name emit flush close health].freeze
|
|
8
|
+
private_constant :DESTINATION_METHODS
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def validate!(destination)
|
|
12
|
+
DESTINATION_METHODS.each do |method_name|
|
|
13
|
+
unless destination.respond_to?(method_name)
|
|
14
|
+
raise ArgumentError, "destination must respond to ##{method_name}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
destination
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(definitions = [])
|
|
23
|
+
@definitions = definitions.map { copy_definition(it) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use(name, **)
|
|
27
|
+
definition = Definition.new(name, **)
|
|
28
|
+
raise ArgumentError, "destination #{definition.name.inspect} is already configured" if key?(definition.name)
|
|
29
|
+
|
|
30
|
+
@definitions << definition
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add(destination)
|
|
35
|
+
self.class.validate!(destination)
|
|
36
|
+
raise ArgumentError, "destination #{destination.name.inspect} is already configured" if key?(destination.name)
|
|
37
|
+
|
|
38
|
+
@definitions << destination
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def clear
|
|
43
|
+
@definitions.clear
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def empty? = @definitions.empty?
|
|
48
|
+
|
|
49
|
+
def build(defaults:)
|
|
50
|
+
output_identities = {}.compare_by_identity
|
|
51
|
+
@definitions.map do |definition|
|
|
52
|
+
if definition.is_a?(Definition)
|
|
53
|
+
definition.build(defaults: defaults, output_identities: output_identities)
|
|
54
|
+
else
|
|
55
|
+
definition
|
|
56
|
+
end
|
|
57
|
+
end.freeze
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def copy
|
|
61
|
+
self.class.new(@definitions)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def freeze
|
|
65
|
+
@definitions.freeze
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def key?(name)
|
|
72
|
+
@definitions.any? { it.name == name }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def copy_definition(definition)
|
|
76
|
+
definition.respond_to?(:copy) ? definition.copy : definition
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Destinations
|
|
6
|
+
module Sink
|
|
7
|
+
class << self
|
|
8
|
+
def wrap(output, close_output: false)
|
|
9
|
+
reject_output_array!(output)
|
|
10
|
+
return output if wrapped?(output)
|
|
11
|
+
|
|
12
|
+
validate_writeable!(output)
|
|
13
|
+
SynchronizedOutput.new(output, close_output: close_output)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate_writeable!(output)
|
|
17
|
+
return if output.respond_to?(:write)
|
|
18
|
+
|
|
19
|
+
raise ArgumentError, "output must respond to #write"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reject_output_array!(output)
|
|
23
|
+
return unless output.is_a?(Array)
|
|
24
|
+
|
|
25
|
+
raise ArgumentError, "output arrays are transport adapter behavior; use destinations or an adapter output"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def wrapped?(output) = output.is_a?(SynchronizedOutput)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Destinations
|
|
6
|
+
class SynchronizedOutput
|
|
7
|
+
def initialize(output, close_output: false)
|
|
8
|
+
Sink.validate_writeable!(output)
|
|
9
|
+
@output = output
|
|
10
|
+
@close_output = close_output
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def after_fork!
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@output.after_fork! if @output.respond_to?(:after_fork!)
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def output_class_name = @output.class.name
|
|
21
|
+
|
|
22
|
+
def resource_identity = @output
|
|
23
|
+
|
|
24
|
+
def write(value)
|
|
25
|
+
@mutex.synchronize { @output.write(value) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def flush
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
return true unless @output.respond_to?(:flush)
|
|
31
|
+
|
|
32
|
+
@output.flush != false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def close
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
return true if output_closed?
|
|
39
|
+
|
|
40
|
+
result = if @close_output && @output.respond_to?(:close)
|
|
41
|
+
@output.close
|
|
42
|
+
elsif @output.respond_to?(:flush)
|
|
43
|
+
@output.flush
|
|
44
|
+
end
|
|
45
|
+
result != false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def output_closed?
|
|
52
|
+
@output.respond_to?(:closed?) ? @output.closed? : false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|