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,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Julewire
|
|
7
|
+
module Core
|
|
8
|
+
module Serialization
|
|
9
|
+
# @api extension
|
|
10
|
+
class TextEncoder
|
|
11
|
+
SEVERITY_STYLES = {
|
|
12
|
+
"debug" => 36,
|
|
13
|
+
"info" => 32,
|
|
14
|
+
"warn" => 33,
|
|
15
|
+
"error" => 31,
|
|
16
|
+
"fatal" => 35,
|
|
17
|
+
"unknown" => 37
|
|
18
|
+
}.freeze
|
|
19
|
+
PUNK_SEVERITY_STYLES = {
|
|
20
|
+
"debug" => 36,
|
|
21
|
+
"info" => 92,
|
|
22
|
+
"warn" => 93,
|
|
23
|
+
"error" => 91,
|
|
24
|
+
"fatal" => 95,
|
|
25
|
+
"unknown" => 97
|
|
26
|
+
}.freeze
|
|
27
|
+
PUNK_SEVERITY_GLYPHS = {
|
|
28
|
+
"debug" => "..",
|
|
29
|
+
"info" => ">>",
|
|
30
|
+
"warn" => "!!",
|
|
31
|
+
"error" => "XX",
|
|
32
|
+
"fatal" => "##",
|
|
33
|
+
"unknown" => "??"
|
|
34
|
+
}.freeze
|
|
35
|
+
THEMES = %i[plain punk].freeze
|
|
36
|
+
DEFAULT_MAX_VALUE_BYTES = 160
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
def punk_glyph(severity)
|
|
40
|
+
PUNK_SEVERITY_GLYPHS.fetch(severity.to_s, PUNK_SEVERITY_GLYPHS.fetch("unknown"))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(max_value_bytes: DEFAULT_MAX_VALUE_BYTES, color: false, append_newline: true, theme: :plain)
|
|
45
|
+
@max_value_bytes = Validation.validate_integer_limit!(
|
|
46
|
+
max_value_bytes,
|
|
47
|
+
name: :max_value_bytes,
|
|
48
|
+
positive: true
|
|
49
|
+
)
|
|
50
|
+
@color = color
|
|
51
|
+
@line_suffix = append_newline ? "\n" : ""
|
|
52
|
+
@theme = validate_theme(theme)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call(payload)
|
|
56
|
+
text = payload.is_a?(String) ? payload : line_for(payload)
|
|
57
|
+
"#{text}#{@line_suffix}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def line_for(payload)
|
|
63
|
+
fields = [
|
|
64
|
+
timestamp(payload),
|
|
65
|
+
severity(payload),
|
|
66
|
+
label(payload, :event),
|
|
67
|
+
label(payload, :source),
|
|
68
|
+
message(payload),
|
|
69
|
+
compact_hash(:payload, value_at(payload, :payload)),
|
|
70
|
+
compact_hash(:labels, value_at(payload, :labels))
|
|
71
|
+
].compact
|
|
72
|
+
fields.join(" ")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_theme(theme)
|
|
76
|
+
Validation.validate_symbol_choice!(theme, name: "text encoder theme", choices: THEMES)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def timestamp(payload)
|
|
80
|
+
value = value_at(payload, :timestamp)
|
|
81
|
+
return if blank?(value)
|
|
82
|
+
# Console output is human-facing; JSON keeps nanosecond precision.
|
|
83
|
+
return value.iso8601(6) if value.respond_to?(:iso8601)
|
|
84
|
+
|
|
85
|
+
value.to_s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def severity(payload)
|
|
89
|
+
value = (value_at(payload, :severity) || :info).to_s
|
|
90
|
+
label = severity_label(value)
|
|
91
|
+
return label unless @color
|
|
92
|
+
|
|
93
|
+
code = severity_styles.fetch(value, 37)
|
|
94
|
+
"\e[#{code}m#{label}\e[0m"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def severity_label(value)
|
|
98
|
+
label = value.upcase.ljust(5)
|
|
99
|
+
return label unless @theme == :punk
|
|
100
|
+
|
|
101
|
+
glyph = self.class.punk_glyph(value)
|
|
102
|
+
"#{glyph} #{value.upcase} #{glyph}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def severity_styles
|
|
106
|
+
@theme == :punk ? PUNK_SEVERITY_STYLES : SEVERITY_STYLES
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def label(payload, key)
|
|
110
|
+
value = value_at(payload, key)
|
|
111
|
+
return if blank?(value)
|
|
112
|
+
|
|
113
|
+
"#{key}=#{value}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def message(payload)
|
|
117
|
+
value = value_at(payload, :message)
|
|
118
|
+
return if blank?(value)
|
|
119
|
+
|
|
120
|
+
truncate(value.to_s)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def compact_hash(name, value)
|
|
124
|
+
return unless value.is_a?(Hash) && !value.empty?
|
|
125
|
+
|
|
126
|
+
"#{name}=#{truncate(JSON.generate(value, allow_nan: false))}"
|
|
127
|
+
rescue StandardError
|
|
128
|
+
"#{name}=#{truncate(value.inspect)}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def truncate(value)
|
|
132
|
+
return value if value.bytesize <= @max_value_bytes
|
|
133
|
+
|
|
134
|
+
"#{value.byteslice(0, @max_value_bytes).scrub("?")}..."
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def value_at(payload, key)
|
|
138
|
+
Fields::Lookup.value(payload, key)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def blank?(value)
|
|
142
|
+
Fields::Lookup.blank?(value)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
class ValueCopy
|
|
7
|
+
include ValueTraversal
|
|
8
|
+
|
|
9
|
+
CIRCULAR_REFERENCE = Core::CIRCULAR_REFERENCE
|
|
10
|
+
EMPTY_ARRAY = [].freeze
|
|
11
|
+
EMPTY_HASH = {}.freeze
|
|
12
|
+
POOL_KEY = :julewire_core_value_copy_pool
|
|
13
|
+
private_constant :EMPTY_ARRAY, :EMPTY_HASH, :POOL_KEY
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def call(
|
|
17
|
+
value,
|
|
18
|
+
compact_empty: false,
|
|
19
|
+
freeze_values: false,
|
|
20
|
+
max_depth: Core::NORMALIZATION_MAX_DEPTH,
|
|
21
|
+
symbolize_keys: false
|
|
22
|
+
)
|
|
23
|
+
return copy_leaf(value, freeze_values: freeze_values) unless container?(value)
|
|
24
|
+
|
|
25
|
+
copy_with(
|
|
26
|
+
cached_copier(
|
|
27
|
+
compact_empty: compact_empty,
|
|
28
|
+
freeze_values: freeze_values,
|
|
29
|
+
max_depth: max_depth,
|
|
30
|
+
symbolize_keys: symbolize_keys
|
|
31
|
+
),
|
|
32
|
+
value
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def omitted_empty?(value)
|
|
37
|
+
value.nil? || (value.is_a?(Hash) && value.empty?) || (value.is_a?(Array) && value.empty?)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def container?(value) = value.is_a?(Hash) || value.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
def cached_copier(compact_empty:, freeze_values:, max_depth:, symbolize_keys:)
|
|
45
|
+
# One copier per thread/options avoids per-record walker allocation.
|
|
46
|
+
pool = Thread.current.thread_variable_get(POOL_KEY)
|
|
47
|
+
unless pool
|
|
48
|
+
pool = {}
|
|
49
|
+
Thread.current.thread_variable_set(POOL_KEY, pool)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
key = cache_key(
|
|
53
|
+
compact_empty: compact_empty,
|
|
54
|
+
freeze_values: freeze_values,
|
|
55
|
+
max_depth: max_depth,
|
|
56
|
+
symbolize_keys: symbolize_keys
|
|
57
|
+
)
|
|
58
|
+
pool[key] ||= new(
|
|
59
|
+
compact_empty: compact_empty,
|
|
60
|
+
freeze_values: freeze_values,
|
|
61
|
+
max_depth: max_depth,
|
|
62
|
+
symbolize_keys: symbolize_keys
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cache_key(compact_empty:, freeze_values:, max_depth:, symbolize_keys:)
|
|
67
|
+
depth_key = max_depth || -1
|
|
68
|
+
flags = 0
|
|
69
|
+
flags |= 1 if compact_empty
|
|
70
|
+
flags |= 2 if freeze_values
|
|
71
|
+
flags |= 4 if symbolize_keys
|
|
72
|
+
(depth_key << 3) | flags
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def copy_with(copier, value)
|
|
76
|
+
return copier.call_reusable(value) unless copier.in_use?
|
|
77
|
+
|
|
78
|
+
new(
|
|
79
|
+
compact_empty: copier.compact_empty,
|
|
80
|
+
freeze_values: copier.freeze_values,
|
|
81
|
+
max_depth: copier.max_depth,
|
|
82
|
+
symbolize_keys: copier.symbolize_keys
|
|
83
|
+
).call(value)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def copy_leaf(value, freeze_values:)
|
|
87
|
+
return copy_string(value, freeze_values: freeze_values) if value.is_a?(String)
|
|
88
|
+
return copy_time(value, freeze_values: freeze_values) if value.is_a?(Time)
|
|
89
|
+
|
|
90
|
+
value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def copy_string(value, freeze_values:)
|
|
94
|
+
copy = value.frozen? ? value : value.dup
|
|
95
|
+
freeze_values ? copy.freeze : copy
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def copy_time(value, freeze_values:)
|
|
99
|
+
return value unless freeze_values
|
|
100
|
+
return value if value.frozen?
|
|
101
|
+
|
|
102
|
+
value.dup.freeze
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
attr_reader :compact_empty, :freeze_values, :max_depth, :symbolize_keys
|
|
107
|
+
|
|
108
|
+
def initialize(compact_empty:, freeze_values:, max_depth:, symbolize_keys:)
|
|
109
|
+
@compact_empty = compact_empty
|
|
110
|
+
@freeze_values = freeze_values
|
|
111
|
+
@max_depth = max_depth
|
|
112
|
+
@symbolize_keys = symbolize_keys
|
|
113
|
+
@in_use = false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def call(value)
|
|
117
|
+
traverse(value) { |root, depth| copy_value(root, depth) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def call_reusable(value)
|
|
121
|
+
@in_use = true
|
|
122
|
+
call(value)
|
|
123
|
+
ensure
|
|
124
|
+
@in_use = false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def in_use? = @in_use
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def copy_value(value, depth)
|
|
132
|
+
return copy_container(value, depth) if value.is_a?(Hash) || value.is_a?(Array)
|
|
133
|
+
return copy_string(value) if value.is_a?(String)
|
|
134
|
+
return copy_time(value) if value.is_a?(Time)
|
|
135
|
+
|
|
136
|
+
value
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def copy_container(value, depth)
|
|
140
|
+
return copy_string(Serializer::MAX_DEPTH_VALUE) if depth_limited?(depth)
|
|
141
|
+
return frozen_empty_container(value) if @freeze_values && value.empty?
|
|
142
|
+
|
|
143
|
+
with_traversal_container(value, CIRCULAR_REFERENCE) do
|
|
144
|
+
value.is_a?(Hash) ? copy_hash(value, depth) : copy_array(value, depth)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def depth_limited?(depth)
|
|
149
|
+
@max_depth && depth >= @max_depth
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def frozen_empty_container(value)
|
|
153
|
+
value.is_a?(Hash) ? EMPTY_HASH : EMPTY_ARRAY
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def copy_hash(value, depth)
|
|
157
|
+
result = {}
|
|
158
|
+
value.each do |key, item|
|
|
159
|
+
next if @compact_empty && self.class.omitted_empty?(item)
|
|
160
|
+
|
|
161
|
+
copied = copy_value(item, depth + 1)
|
|
162
|
+
next if @compact_empty && self.class.omitted_empty?(copied)
|
|
163
|
+
|
|
164
|
+
result[copy_key(key)] = copied
|
|
165
|
+
end
|
|
166
|
+
freeze_container(result)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def copy_array(value, depth)
|
|
170
|
+
result = []
|
|
171
|
+
value.each do |item|
|
|
172
|
+
next if @compact_empty && self.class.omitted_empty?(item)
|
|
173
|
+
|
|
174
|
+
copied = copy_value(item, depth + 1)
|
|
175
|
+
next if @compact_empty && self.class.omitted_empty?(copied)
|
|
176
|
+
|
|
177
|
+
result << copied
|
|
178
|
+
end
|
|
179
|
+
freeze_container(result)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def copy_key(key)
|
|
183
|
+
return key.to_sym if @symbolize_keys && key.is_a?(String)
|
|
184
|
+
return copy_string(key) if key.is_a?(String)
|
|
185
|
+
|
|
186
|
+
key
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def copy_string(value)
|
|
190
|
+
return value unless value.is_a?(String)
|
|
191
|
+
|
|
192
|
+
copy = value.frozen? ? value : value.dup
|
|
193
|
+
@freeze_values ? copy.freeze : copy
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def copy_time(value)
|
|
197
|
+
return value unless @freeze_values
|
|
198
|
+
return value if value.frozen?
|
|
199
|
+
|
|
200
|
+
value.dup.freeze
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def freeze_container(value)
|
|
204
|
+
@freeze_values ? value.freeze : value
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
module ValueTraversal
|
|
7
|
+
def traverse(value)
|
|
8
|
+
previous_seen = @traversal_seen
|
|
9
|
+
previous_first_seen = @traversal_first_seen
|
|
10
|
+
previous_second_seen = @traversal_second_seen
|
|
11
|
+
previous_third_seen = @traversal_third_seen
|
|
12
|
+
previous_fourth_seen = @traversal_fourth_seen
|
|
13
|
+
@traversal_seen = nil
|
|
14
|
+
@traversal_first_seen = nil
|
|
15
|
+
@traversal_second_seen = nil
|
|
16
|
+
@traversal_third_seen = nil
|
|
17
|
+
@traversal_fourth_seen = nil
|
|
18
|
+
yield(value, 0)
|
|
19
|
+
ensure
|
|
20
|
+
@traversal_seen = previous_seen
|
|
21
|
+
@traversal_first_seen = previous_first_seen
|
|
22
|
+
@traversal_second_seen = previous_second_seen
|
|
23
|
+
@traversal_third_seen = previous_third_seen
|
|
24
|
+
@traversal_fourth_seen = previous_fourth_seen
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def with_traversal_container(value, circular_value)
|
|
30
|
+
added = false
|
|
31
|
+
seen = @traversal_seen
|
|
32
|
+
first_seen = @traversal_first_seen
|
|
33
|
+
second_seen = @traversal_second_seen
|
|
34
|
+
third_seen = @traversal_third_seen
|
|
35
|
+
fourth_seen = @traversal_fourth_seen
|
|
36
|
+
return circular_value if seen&.key?(value) || first_seen.equal?(value) || second_seen.equal?(value) ||
|
|
37
|
+
third_seen.equal?(value) || fourth_seen.equal?(value)
|
|
38
|
+
|
|
39
|
+
added = mark_traversal_container(value, seen, first_seen, second_seen, third_seen, fourth_seen)
|
|
40
|
+
yield
|
|
41
|
+
ensure
|
|
42
|
+
unmark_traversal_container(value, added)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def traversal_seen?(value)
|
|
46
|
+
@traversal_seen&.key?(value) || @traversal_first_seen.equal?(value) ||
|
|
47
|
+
@traversal_second_seen.equal?(value) || @traversal_third_seen.equal?(value) ||
|
|
48
|
+
@traversal_fourth_seen.equal?(value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def with_marked_traversal_container(value)
|
|
52
|
+
added = mark_traversal_container(
|
|
53
|
+
value,
|
|
54
|
+
@traversal_seen,
|
|
55
|
+
@traversal_first_seen,
|
|
56
|
+
@traversal_second_seen,
|
|
57
|
+
@traversal_third_seen,
|
|
58
|
+
@traversal_fourth_seen
|
|
59
|
+
)
|
|
60
|
+
yield
|
|
61
|
+
ensure
|
|
62
|
+
unmark_traversal_container(value, added)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def mark_traversal_container(value, seen, first_seen, second_seen, third_seen, fourth_seen)
|
|
66
|
+
# Most records visit only a handful of live containers. Keep those in
|
|
67
|
+
# slots and allocate the identity hash only for genuinely deep walks.
|
|
68
|
+
if seen
|
|
69
|
+
seen[value] = true
|
|
70
|
+
:hash
|
|
71
|
+
elsif first_seen.nil?
|
|
72
|
+
mark_first_seen(value)
|
|
73
|
+
elsif second_seen.nil?
|
|
74
|
+
mark_second_seen(value)
|
|
75
|
+
elsif third_seen.nil?
|
|
76
|
+
mark_third_seen(value)
|
|
77
|
+
elsif fourth_seen.nil?
|
|
78
|
+
mark_fourth_seen(value)
|
|
79
|
+
else
|
|
80
|
+
promote_traversal_seen(value, first_seen, second_seen, third_seen, fourth_seen)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def unmark_traversal_container(value, added)
|
|
85
|
+
case added
|
|
86
|
+
when :hash
|
|
87
|
+
@traversal_seen.delete(value)
|
|
88
|
+
when :first, :second, :third, :fourth
|
|
89
|
+
unmark_traversal_slot(value, added)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def mark_first_seen(value)
|
|
94
|
+
@traversal_first_seen = value
|
|
95
|
+
:first
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def mark_second_seen(value)
|
|
99
|
+
@traversal_second_seen = value
|
|
100
|
+
:second
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def mark_third_seen(value)
|
|
104
|
+
@traversal_third_seen = value
|
|
105
|
+
:third
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def mark_fourth_seen(value)
|
|
109
|
+
@traversal_fourth_seen = value
|
|
110
|
+
:fourth
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def promote_traversal_seen(value, first_seen, second_seen, third_seen, fourth_seen)
|
|
114
|
+
@traversal_seen = {}.compare_by_identity
|
|
115
|
+
@traversal_seen[first_seen] = true
|
|
116
|
+
@traversal_seen[second_seen] = true
|
|
117
|
+
@traversal_seen[third_seen] = true
|
|
118
|
+
@traversal_seen[fourth_seen] = true
|
|
119
|
+
@traversal_seen[value] = true
|
|
120
|
+
clear_traversal_slots!
|
|
121
|
+
:hash
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def unmark_traversal_slot(value, added)
|
|
125
|
+
return @traversal_seen.delete(value) if @traversal_seen
|
|
126
|
+
|
|
127
|
+
clear_traversal_slot(added)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def clear_traversal_slot(slot)
|
|
131
|
+
case slot
|
|
132
|
+
when :first then @traversal_first_seen = nil
|
|
133
|
+
when :second then @traversal_second_seen = nil
|
|
134
|
+
when :third then @traversal_third_seen = nil
|
|
135
|
+
when :fourth then @traversal_fourth_seen = nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def clear_traversal_slots!
|
|
140
|
+
@traversal_first_seen = nil
|
|
141
|
+
@traversal_second_seen = nil
|
|
142
|
+
@traversal_third_seen = nil
|
|
143
|
+
@traversal_fourth_seen = nil
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private_constant :ValueTraversal
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Testing
|
|
6
|
+
module Chaos
|
|
7
|
+
class Catalog
|
|
8
|
+
Entry = Data.define(:kind, :name, :exercise)
|
|
9
|
+
KINDS = %i[processor formatter encoder destination subscriber listener].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :entries
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def build
|
|
15
|
+
catalog = new
|
|
16
|
+
yield catalog if block_given?
|
|
17
|
+
catalog
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def assert_contract(test_context, catalog:, errors:)
|
|
21
|
+
entries = catalog.entries
|
|
22
|
+
raise ArgumentError, "chaos catalog must have entries" if entries.empty?
|
|
23
|
+
|
|
24
|
+
entries.each do |entry|
|
|
25
|
+
assert_entry(test_context, entry, errors)
|
|
26
|
+
end
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def assert_entry(test_context, entry, errors)
|
|
33
|
+
Chaos.assert_contained(
|
|
34
|
+
test_context,
|
|
35
|
+
errors: errors,
|
|
36
|
+
description: "#{entry.kind} #{entry.name}"
|
|
37
|
+
) do |error|
|
|
38
|
+
entry.exercise.call(error)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
@entries = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def processor(name, &) = register(:processor, name, &)
|
|
48
|
+
|
|
49
|
+
def formatter(name, &) = register(:formatter, name, &)
|
|
50
|
+
|
|
51
|
+
def encoder(name, &) = register(:encoder, name, &)
|
|
52
|
+
|
|
53
|
+
def destination(name, &) = register(:destination, name, &)
|
|
54
|
+
|
|
55
|
+
def subscriber(name, &) = register(:subscriber, name, &)
|
|
56
|
+
|
|
57
|
+
def listener(name, &) = register(:listener, name, &)
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def register(kind, name, &exercise)
|
|
62
|
+
raise ArgumentError, "unknown chaos component kind #{kind.inspect}" unless KINDS.include?(kind)
|
|
63
|
+
raise ArgumentError, "chaos component exercise block required" unless exercise
|
|
64
|
+
|
|
65
|
+
@entries << Entry.new(kind, Core.normalize_name(name), exercise)
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Testing
|
|
6
|
+
module Chaos
|
|
7
|
+
module CoreRuntime
|
|
8
|
+
SCENARIOS = %i[
|
|
9
|
+
destination_processor
|
|
10
|
+
drop_callback
|
|
11
|
+
encoder
|
|
12
|
+
failure_callback
|
|
13
|
+
formatter
|
|
14
|
+
lifecycle_after_fork
|
|
15
|
+
lifecycle_flush
|
|
16
|
+
output
|
|
17
|
+
pipeline_processor
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
private_constant :SCENARIOS
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def assert_contract(test_context, runtime:, reset:, errors:)
|
|
24
|
+
SCENARIOS.each do |scenario|
|
|
25
|
+
errors.each do |error|
|
|
26
|
+
assert_scenario(test_context, runtime, reset, scenario, error)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def assert_scenario(test_context, runtime, reset, scenario, error)
|
|
35
|
+
reset.call
|
|
36
|
+
send(:"exercise_#{scenario}", runtime, error)
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
test_context.flunk(
|
|
39
|
+
"expected core #{scenario} failure to be contained, " \
|
|
40
|
+
"#{error.class} leaked #{e.class}: #{e.message}"
|
|
41
|
+
)
|
|
42
|
+
ensure
|
|
43
|
+
reset.call
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exercise_destination_processor(runtime, error)
|
|
47
|
+
configure_destination(runtime, processors: [Chaos.raiser(error)])
|
|
48
|
+
runtime.emit("chaos")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def exercise_drop_callback(runtime, error)
|
|
52
|
+
trigger = RuntimeError.new("julewire chaos formatter trigger")
|
|
53
|
+
configure_destination(runtime, formatter: Chaos.raiser(trigger), on_drop: Chaos.raiser(error))
|
|
54
|
+
runtime.emit("chaos")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def exercise_encoder(runtime, error)
|
|
58
|
+
configure_destination(runtime, encoder: Chaos.raiser(error))
|
|
59
|
+
runtime.emit("chaos")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def exercise_failure_callback(runtime, error)
|
|
63
|
+
trigger = RuntimeError.new("julewire chaos output trigger")
|
|
64
|
+
configure_destination(
|
|
65
|
+
runtime,
|
|
66
|
+
output: RaisingOutput.new(trigger, failures: %i[write]),
|
|
67
|
+
runtime_on_failure: Chaos.raiser(error)
|
|
68
|
+
)
|
|
69
|
+
runtime.emit("chaos")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def exercise_formatter(runtime, error)
|
|
73
|
+
configure_destination(runtime, formatter: Chaos.raiser(error))
|
|
74
|
+
runtime.emit("chaos")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def exercise_lifecycle_after_fork(runtime, error)
|
|
78
|
+
configure_destination(runtime, output: RaisingOutput.new(error, failures: %i[after_fork]))
|
|
79
|
+
runtime.after_fork!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def exercise_lifecycle_flush(runtime, error)
|
|
83
|
+
configure_destination(runtime, output: RaisingOutput.new(error, failures: %i[flush]))
|
|
84
|
+
runtime.flush
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def exercise_output(runtime, error)
|
|
88
|
+
configure_destination(runtime, output: RaisingOutput.new(error, failures: %i[write]))
|
|
89
|
+
runtime.emit("chaos")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def exercise_pipeline_processor(runtime, error)
|
|
93
|
+
runtime.configure do |config|
|
|
94
|
+
config.destinations.use(:default, output: NullOutput.new)
|
|
95
|
+
config.processors.use(Chaos.raiser(error))
|
|
96
|
+
end
|
|
97
|
+
runtime.emit("chaos")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def configure_destination(runtime, output: NullOutput.new, formatter: nil, encoder: nil,
|
|
101
|
+
on_drop: nil, processors: nil, runtime_on_failure: nil)
|
|
102
|
+
runtime.configure do |config|
|
|
103
|
+
config.on_failure = runtime_on_failure if runtime_on_failure
|
|
104
|
+
config.destinations.clear
|
|
105
|
+
config.destinations.use(
|
|
106
|
+
:default,
|
|
107
|
+
encoder: encoder || Julewire::Core::Serialization::JsonEncoder.new,
|
|
108
|
+
formatter: formatter || Julewire::Core::Records::Formatter.new,
|
|
109
|
+
on_drop: on_drop,
|
|
110
|
+
output: output,
|
|
111
|
+
processors: processors || []
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|