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,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
class DeepCompactEmpty
|
|
7
|
+
include ValueTraversal
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def call(value)
|
|
11
|
+
ValueCopy.call(value, compact_empty: true)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compact_owned!(value)
|
|
15
|
+
new.compact_owned!(value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def omitted?(value)
|
|
19
|
+
ValueCopy.omitted_empty?(value)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def compact_owned!(value)
|
|
24
|
+
traverse(value) { |root, _depth| compact_value!(root) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def compact_value!(value)
|
|
30
|
+
return compact_hash!(value) if value.is_a?(Hash)
|
|
31
|
+
return compact_array!(value) if value.is_a?(Array)
|
|
32
|
+
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def compact_hash!(value)
|
|
37
|
+
with_traversal_container(value, value) do
|
|
38
|
+
value.each do |key, item|
|
|
39
|
+
compacted = compact_value!(item)
|
|
40
|
+
if self.class.omitted?(compacted)
|
|
41
|
+
value.delete(key)
|
|
42
|
+
elsif !compacted.equal?(item)
|
|
43
|
+
value[key] = compacted
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def compact_array!(value)
|
|
51
|
+
with_traversal_container(value, value) do
|
|
52
|
+
index = 0
|
|
53
|
+
value.each do |item|
|
|
54
|
+
compacted = compact_value!(item)
|
|
55
|
+
next if self.class.omitted?(compacted)
|
|
56
|
+
|
|
57
|
+
value[index] = compacted
|
|
58
|
+
index += 1
|
|
59
|
+
end
|
|
60
|
+
value.slice!(index, value.length - index) if index < value.length
|
|
61
|
+
value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
class DeepFreeze
|
|
7
|
+
include ValueTraversal
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def call(value, max_depth: Core::NORMALIZATION_MAX_DEPTH, trust_frozen: false)
|
|
11
|
+
new(max_depth, trust_frozen: trust_frozen).call(value)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(max_depth, trust_frozen:)
|
|
16
|
+
@max_depth = max_depth
|
|
17
|
+
@trust_frozen = trust_frozen
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(value)
|
|
21
|
+
traverse(value) { |root, depth| freeze_value(root, depth) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def freeze_value(value, depth)
|
|
27
|
+
return value.freeze if value.is_a?(String)
|
|
28
|
+
return value if @trust_frozen && value.frozen?
|
|
29
|
+
return freeze_container(value, depth) if value.is_a?(Hash) || value.is_a?(Array)
|
|
30
|
+
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def freeze_container(value, depth)
|
|
35
|
+
return Serializer::MAX_DEPTH_VALUE.freeze if depth_limited?(depth)
|
|
36
|
+
|
|
37
|
+
with_traversal_container(value, value) do
|
|
38
|
+
value.is_a?(Hash) ? freeze_hash(value, depth) : freeze_array(value, depth)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def depth_limited?(depth)
|
|
43
|
+
@max_depth && depth >= @max_depth
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def freeze_hash(value, depth)
|
|
47
|
+
value.each { |key, item| freeze_child(value, key, item, depth) }
|
|
48
|
+
value.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def freeze_array(value, depth)
|
|
52
|
+
value.each_index { freeze_child(value, it, value[it], depth) }
|
|
53
|
+
value.freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def freeze_child(value, key, item, depth)
|
|
57
|
+
frozen = freeze_value(item, depth + 1)
|
|
58
|
+
value[key] = frozen unless value.frozen? || frozen.equal?(item)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
module EncodingSanitizer
|
|
7
|
+
class << self
|
|
8
|
+
def call(value)
|
|
9
|
+
raise TypeError, "value must be a String" unless value.is_a?(String)
|
|
10
|
+
|
|
11
|
+
return value if valid_utf8?(value) || valid_ascii_only?(value)
|
|
12
|
+
return value.scrub("?") if utf8?(value)
|
|
13
|
+
|
|
14
|
+
encode_utf8(value)
|
|
15
|
+
rescue EncodingError
|
|
16
|
+
encode_utf8(value.b.force_encoding(Encoding::UTF_8))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def valid_utf8?(value)
|
|
22
|
+
utf8?(value) && value.valid_encoding?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def valid_ascii_only?(value)
|
|
26
|
+
value.ascii_only? && value.valid_encoding?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def utf8?(value)
|
|
30
|
+
value.encoding == Encoding::UTF_8
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def encode_utf8(value)
|
|
34
|
+
value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
class ExceptionShape
|
|
7
|
+
include ValueTraversal
|
|
8
|
+
|
|
9
|
+
DEFAULT_MAX_CAUSE_DEPTH = 5
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def call(error, max_backtrace_lines: Core::MAX_BACKTRACE_LINES, max_cause_depth: DEFAULT_MAX_CAUSE_DEPTH)
|
|
13
|
+
new(
|
|
14
|
+
max_backtrace_lines: max_backtrace_lines,
|
|
15
|
+
max_cause_depth: max_cause_depth
|
|
16
|
+
).call(error)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(max_backtrace_lines:, max_cause_depth:)
|
|
21
|
+
@backtrace_limiter = BacktraceLimiter.new(max_backtrace_lines: max_backtrace_lines)
|
|
22
|
+
@include_backtraces = max_backtrace_lines.positive?
|
|
23
|
+
@max_cause_depth = Validation.validate_integer_limit!(max_cause_depth, name: :max_cause_depth)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(error)
|
|
27
|
+
traverse(error) { |root, depth| shape_exception(root, depth) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def shape_exception(error, depth)
|
|
33
|
+
return error unless error.is_a?(Exception)
|
|
34
|
+
|
|
35
|
+
with_traversal_container(error, Core::CIRCULAR_REFERENCE) do
|
|
36
|
+
exception_hash(error, depth)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def exception_hash(error, depth)
|
|
41
|
+
{
|
|
42
|
+
class: class_name(error),
|
|
43
|
+
message: error_message(error)
|
|
44
|
+
}.tap do |result|
|
|
45
|
+
if @include_backtraces
|
|
46
|
+
lines = backtrace(error)
|
|
47
|
+
result[:backtrace] = lines if lines
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
cause = exception_cause(error)
|
|
51
|
+
next unless cause
|
|
52
|
+
|
|
53
|
+
if depth >= @max_cause_depth
|
|
54
|
+
result[:cause_truncated] = true
|
|
55
|
+
else
|
|
56
|
+
result[:cause] = shape_exception(cause, depth + 1)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def class_name(error)
|
|
62
|
+
error.class.name || error.class.to_s
|
|
63
|
+
rescue StandardError
|
|
64
|
+
"Exception"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def error_message(error)
|
|
68
|
+
message = error.message
|
|
69
|
+
message.is_a?(String) ? message.dup : message.to_s
|
|
70
|
+
rescue StandardError
|
|
71
|
+
"[Unavailable]"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def backtrace(error)
|
|
75
|
+
@backtrace_limiter.call(backtrace: Core::Fields::FieldSet.deep_dup(error.backtrace))[:backtrace]
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def exception_cause(error)
|
|
81
|
+
error.cause
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Core
|
|
7
|
+
module Serialization
|
|
8
|
+
# @api extension
|
|
9
|
+
class JsonEncoder
|
|
10
|
+
def initialize(
|
|
11
|
+
max_depth: Serializer::DEFAULT_MAX_DEPTH,
|
|
12
|
+
max_string_bytes: Serializer::DEFAULT_MAX_STRING_BYTES,
|
|
13
|
+
max_array_items: Serializer::DEFAULT_MAX_ARRAY_ITEMS,
|
|
14
|
+
max_hash_keys: Serializer::DEFAULT_MAX_HASH_KEYS,
|
|
15
|
+
compact_empty: true,
|
|
16
|
+
max_backtrace_lines: Core::MAX_BACKTRACE_LINES,
|
|
17
|
+
append_newline: true
|
|
18
|
+
)
|
|
19
|
+
@max_depth = max_depth
|
|
20
|
+
@max_string_bytes = max_string_bytes
|
|
21
|
+
@max_array_items = max_array_items
|
|
22
|
+
@max_hash_keys = max_hash_keys
|
|
23
|
+
@compact_empty = compact_empty
|
|
24
|
+
@max_backtrace_lines = max_backtrace_lines
|
|
25
|
+
@line_suffix = append_newline ? "\n" : ""
|
|
26
|
+
@serializer_key = [
|
|
27
|
+
@max_depth,
|
|
28
|
+
@max_string_bytes,
|
|
29
|
+
@max_array_items,
|
|
30
|
+
@max_hash_keys,
|
|
31
|
+
@compact_empty,
|
|
32
|
+
@max_backtrace_lines
|
|
33
|
+
].freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(payload)
|
|
37
|
+
JSON.generate(serialized_payload(payload), allow_nan: false).tap do |json|
|
|
38
|
+
json << @line_suffix
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def serialized_payload(payload)
|
|
45
|
+
serializer = cached_serializer
|
|
46
|
+
return build_serializer.serialize(payload) if serializer.in_use?
|
|
47
|
+
|
|
48
|
+
serializer.serialize(payload)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def cached_serializer
|
|
52
|
+
SerializerPool.serializer(:julewire_core_json_encoder_serializers, @serializer_key) { build_serializer }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_serializer
|
|
56
|
+
Serializer.new(
|
|
57
|
+
max_depth: @max_depth,
|
|
58
|
+
max_string_bytes: @max_string_bytes,
|
|
59
|
+
max_array_items: @max_array_items,
|
|
60
|
+
max_hash_keys: @max_hash_keys,
|
|
61
|
+
compact_empty: @compact_empty,
|
|
62
|
+
max_backtrace_lines: @max_backtrace_lines,
|
|
63
|
+
copy_strings: false
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Julewire
|
|
7
|
+
module Core
|
|
8
|
+
module Serialization
|
|
9
|
+
class Serializer < BoundedTraversal
|
|
10
|
+
MAX_DEPTH_VALUE = BoundedTraversal::MAX_DEPTH_VALUE
|
|
11
|
+
OBJECT_VALUE = "[Object]"
|
|
12
|
+
NAN_VALUE = "NaN"
|
|
13
|
+
INFINITY_VALUE = "Infinity"
|
|
14
|
+
NEGATIVE_INFINITY_VALUE = "-Infinity"
|
|
15
|
+
TRUNCATED_SUFFIX = BoundedTraversal::TRUNCATED_SUFFIX
|
|
16
|
+
TRUNCATION_METADATA_KEY = BoundedTraversal::TRUNCATION_METADATA_KEY
|
|
17
|
+
DEFAULT_MAX_DEPTH = BoundedTraversal::DEFAULT_MAX_DEPTH
|
|
18
|
+
DEFAULT_MAX_STRING_BYTES = BoundedTraversal::DEFAULT_MAX_STRING_BYTES
|
|
19
|
+
DEFAULT_MAX_ARRAY_ITEMS = BoundedTraversal::DEFAULT_MAX_ARRAY_ITEMS
|
|
20
|
+
DEFAULT_MAX_HASH_KEYS = BoundedTraversal::DEFAULT_MAX_HASH_KEYS
|
|
21
|
+
MAX_KEY_BYTES = DEFAULT_MAX_STRING_BYTES
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def call(
|
|
25
|
+
value,
|
|
26
|
+
max_depth: DEFAULT_MAX_DEPTH,
|
|
27
|
+
max_string_bytes: DEFAULT_MAX_STRING_BYTES,
|
|
28
|
+
max_array_items: DEFAULT_MAX_ARRAY_ITEMS,
|
|
29
|
+
max_hash_keys: DEFAULT_MAX_HASH_KEYS,
|
|
30
|
+
compact_empty: false,
|
|
31
|
+
max_backtrace_lines: Core::MAX_BACKTRACE_LINES
|
|
32
|
+
)
|
|
33
|
+
new(
|
|
34
|
+
max_depth: max_depth,
|
|
35
|
+
max_string_bytes: max_string_bytes,
|
|
36
|
+
max_array_items: max_array_items,
|
|
37
|
+
max_hash_keys: max_hash_keys,
|
|
38
|
+
compact_empty: compact_empty,
|
|
39
|
+
max_backtrace_lines: max_backtrace_lines
|
|
40
|
+
).serialize(value)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(
|
|
45
|
+
max_depth: DEFAULT_MAX_DEPTH,
|
|
46
|
+
max_string_bytes: DEFAULT_MAX_STRING_BYTES,
|
|
47
|
+
max_array_items: DEFAULT_MAX_ARRAY_ITEMS,
|
|
48
|
+
max_hash_keys: DEFAULT_MAX_HASH_KEYS,
|
|
49
|
+
compact_empty: false,
|
|
50
|
+
max_backtrace_lines: Core::MAX_BACKTRACE_LINES,
|
|
51
|
+
copy_strings: true
|
|
52
|
+
)
|
|
53
|
+
super(
|
|
54
|
+
max_array_items: max_array_items,
|
|
55
|
+
max_depth: max_depth,
|
|
56
|
+
max_depth_value: MAX_DEPTH_VALUE,
|
|
57
|
+
max_hash_keys: max_hash_keys,
|
|
58
|
+
max_string_bytes: max_string_bytes,
|
|
59
|
+
truncation_key: TRUNCATION_METADATA_KEY
|
|
60
|
+
)
|
|
61
|
+
@max_backtrace_lines = Validation.validate_integer_limit!(
|
|
62
|
+
max_backtrace_lines,
|
|
63
|
+
name: :max_backtrace_lines
|
|
64
|
+
)
|
|
65
|
+
@compact_empty = compact_empty
|
|
66
|
+
@copy_strings = copy_strings
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def serialize(value)
|
|
70
|
+
@in_use = true
|
|
71
|
+
walk(record_data(value))
|
|
72
|
+
ensure
|
|
73
|
+
@in_use = false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def in_use? = @in_use
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def scalar_value(value, depth, _key, _path)
|
|
81
|
+
return serialize_exception(value, depth) if value.is_a?(Exception)
|
|
82
|
+
|
|
83
|
+
case value
|
|
84
|
+
when nil, true, false
|
|
85
|
+
clear_truncated(value)
|
|
86
|
+
when Numeric
|
|
87
|
+
serialize_numeric(value)
|
|
88
|
+
when Symbol
|
|
89
|
+
clear_truncated(value.to_s)
|
|
90
|
+
when String
|
|
91
|
+
serialize_string(value)
|
|
92
|
+
when Time, DateTime, Date
|
|
93
|
+
serialize_temporal(value)
|
|
94
|
+
else
|
|
95
|
+
return serialize_iso8601_temporal(value) if zone_temporal?(value)
|
|
96
|
+
|
|
97
|
+
serialize_object(value)
|
|
98
|
+
end
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
clear_truncated(unserializable_marker(e))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def serialize_exception(error, depth)
|
|
104
|
+
shape = ExceptionShape.call(error, max_backtrace_lines: @max_backtrace_lines)
|
|
105
|
+
walk_value(shape, depth + 1, nil, nil)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def hash_like?(value)
|
|
109
|
+
value.is_a?(Hash) || value.is_a?(Records::PublicProjection)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def record_data(value)
|
|
113
|
+
return value.serializable_data if value.is_a?(Records::Record)
|
|
114
|
+
|
|
115
|
+
value
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def serialize_numeric(value)
|
|
119
|
+
return serialize_float(value) if value.is_a?(Float)
|
|
120
|
+
return clear_truncated(value) if value.is_a?(Integer)
|
|
121
|
+
return serialize_string(value.to_s("F")) if defined?(BigDecimal) && value.is_a?(BigDecimal)
|
|
122
|
+
|
|
123
|
+
serialize_string(EncodingSanitizer.call(value.to_s))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def serialize_float(value)
|
|
127
|
+
return clear_truncated(value) if value.finite?
|
|
128
|
+
return clear_truncated(NAN_VALUE) if value.nan?
|
|
129
|
+
|
|
130
|
+
clear_truncated(value.positive? ? INFINITY_VALUE : NEGATIVE_INFINITY_VALUE)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def serialize_temporal(value)
|
|
134
|
+
return clear_truncated(value.getutc.iso8601(9)) if value.is_a?(Time)
|
|
135
|
+
return clear_truncated(value.iso8601(9)) if value.is_a?(DateTime)
|
|
136
|
+
|
|
137
|
+
clear_truncated(value.iso8601)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def serialize_iso8601_temporal(value)
|
|
141
|
+
temporal = value.respond_to?(:utc) ? value.utc : value
|
|
142
|
+
clear_truncated(EncodingSanitizer.call(temporal.iso8601(9)))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def zone_temporal?(value)
|
|
146
|
+
value.respond_to?(:iso8601) && value.respond_to?(:time_zone)
|
|
147
|
+
rescue StandardError
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def omitted_value?(value) = DeepCompactEmpty.omitted?(value)
|
|
152
|
+
|
|
153
|
+
def raw_omitted_value?(value) = DeepCompactEmpty.omitted?(value)
|
|
154
|
+
|
|
155
|
+
def key_value(key) = serialize_key(key)
|
|
156
|
+
|
|
157
|
+
def error_value(error) = clear_truncated(unserializable_marker(error))
|
|
158
|
+
|
|
159
|
+
def serialize_key(key)
|
|
160
|
+
case key
|
|
161
|
+
when String
|
|
162
|
+
serialize_key_string(key)
|
|
163
|
+
when Symbol
|
|
164
|
+
serialize_symbol_key(key)
|
|
165
|
+
when nil, true, false, Numeric
|
|
166
|
+
serialize_key_string(key.to_s)
|
|
167
|
+
else
|
|
168
|
+
serialize_key_string(object_marker(key))
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def serialize_symbol_key(key)
|
|
173
|
+
name = key.name
|
|
174
|
+
return serialize_trusted_key_string(name) if safe_trusted_key_name?(name)
|
|
175
|
+
|
|
176
|
+
serialize_key_string(name)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def safe_trusted_key_name?(value)
|
|
180
|
+
value.ascii_only? || (value.encoding == Encoding::UTF_8 && value.valid_encoding?)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def serialize_key_string(value)
|
|
184
|
+
string = EncodingSanitizer.call(value)
|
|
185
|
+
return clear_truncated(copy_string(string)) if string.bytesize <= MAX_KEY_BYTES
|
|
186
|
+
|
|
187
|
+
mark_truncated("#{string.byteslice(0, MAX_KEY_BYTES).scrub("?")}#{TRUNCATED_SUFFIX}")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def serialize_trusted_key_string(value)
|
|
191
|
+
return clear_truncated(value) if value.bytesize <= MAX_KEY_BYTES
|
|
192
|
+
|
|
193
|
+
mark_truncated("#{value.byteslice(0, MAX_KEY_BYTES).scrub("?")}#{TRUNCATED_SUFFIX}")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def serialize_object(value)
|
|
197
|
+
clear_truncated(object_marker(value))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def object_marker(value)
|
|
201
|
+
class_name = value.class.name
|
|
202
|
+
return OBJECT_VALUE if class_name.nil? || class_name.empty?
|
|
203
|
+
|
|
204
|
+
"[Object: #{EncodingSanitizer.call(class_name)}]"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def unserializable_marker(error)
|
|
208
|
+
class_name = error.class.name
|
|
209
|
+
return "[Unserializable]" if class_name.nil? || class_name.empty?
|
|
210
|
+
|
|
211
|
+
"[Unserializable: #{EncodingSanitizer.call(class_name)}]"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def serialize_string(value)
|
|
215
|
+
string = EncodingSanitizer.call(value)
|
|
216
|
+
return clear_truncated(copy_string(string)) if string.bytesize <= @max_string_bytes
|
|
217
|
+
|
|
218
|
+
mark_truncated("#{string.byteslice(0, @max_string_bytes).scrub("?")}#{TRUNCATED_SUFFIX}")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def copy_string(value)
|
|
222
|
+
value.frozen? || !@copy_strings ? value : value.dup
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def record_hash_truncation(fields, _raw_key, key, key_truncated, child_truncated)
|
|
226
|
+
return fields unless key_truncated || child_truncated
|
|
227
|
+
|
|
228
|
+
append_truncation_field(fields, key)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Serialization
|
|
6
|
+
module SerializerPool
|
|
7
|
+
class << self
|
|
8
|
+
def serializer(pool_key, serializer_key)
|
|
9
|
+
# Serializers carry traversal state, so pooled instances stay thread-local.
|
|
10
|
+
pool = Thread.current.thread_variable_get(pool_key)
|
|
11
|
+
unless pool
|
|
12
|
+
pool = {}
|
|
13
|
+
Thread.current.thread_variable_set(pool_key, pool)
|
|
14
|
+
end
|
|
15
|
+
pool[serializer_key] ||= yield
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|