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.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/docs/advanced-configuration.md +66 -0
  6. data/docs/attribute-keys.md +74 -0
  7. data/docs/configuration.md +327 -0
  8. data/docs/context-and-propagation.md +353 -0
  9. data/docs/contracts.md +211 -0
  10. data/docs/development.md +49 -0
  11. data/docs/extensions-and-api.md +567 -0
  12. data/docs/health-schema.md +104 -0
  13. data/docs/instrumentation-cheatsheet.md +29 -0
  14. data/docs/internals.md +135 -0
  15. data/docs/outputs-and-lifecycle.md +206 -0
  16. data/docs/quickstart.md +133 -0
  17. data/docs/record-sources.md +17 -0
  18. data/docs/records-and-data-policy.md +230 -0
  19. data/docs/security-and-wire.md +45 -0
  20. data/docs/tail.md +91 -0
  21. data/exe/julewire +6 -0
  22. data/julewire-core.gemspec +41 -0
  23. data/lib/julewire/core/cli/doctor.rb +143 -0
  24. data/lib/julewire/core/cli/line_helpers.rb +77 -0
  25. data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
  26. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
  27. data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
  28. data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
  29. data/lib/julewire/core/cli/log_formats.rb +123 -0
  30. data/lib/julewire/core/cli/tail.rb +153 -0
  31. data/lib/julewire/core/cli/transcode.rb +105 -0
  32. data/lib/julewire/core/cli.rb +73 -0
  33. data/lib/julewire/core/configuration.rb +99 -0
  34. data/lib/julewire/core/context_store.rb +384 -0
  35. data/lib/julewire/core/destinations/chaos_output.rb +91 -0
  36. data/lib/julewire/core/destinations/collection.rb +177 -0
  37. data/lib/julewire/core/destinations/definition.rb +125 -0
  38. data/lib/julewire/core/destinations/destination.rb +268 -0
  39. data/lib/julewire/core/destinations/registry.rb +81 -0
  40. data/lib/julewire/core/destinations/sink.rb +35 -0
  41. data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
  42. data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
  43. data/lib/julewire/core/destinations/write_step.rb +119 -0
  44. data/lib/julewire/core/destinations.rb +33 -0
  45. data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
  46. data/lib/julewire/core/diagnostics/doctor.rb +114 -0
  47. data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
  48. data/lib/julewire/core/diagnostics/health.rb +144 -0
  49. data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
  50. data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
  51. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
  52. data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
  53. data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
  54. data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
  55. data/lib/julewire/core/diagnostics/tail.rb +168 -0
  56. data/lib/julewire/core/diagnostics.rb +8 -0
  57. data/lib/julewire/core/error.rb +7 -0
  58. data/lib/julewire/core/execution/boundary.rb +106 -0
  59. data/lib/julewire/core/execution/handle.rb +77 -0
  60. data/lib/julewire/core/execution/lineage.rb +192 -0
  61. data/lib/julewire/core/execution/measurement_handle.rb +28 -0
  62. data/lib/julewire/core/execution/no_current_error.rb +9 -0
  63. data/lib/julewire/core/execution/scope.rb +246 -0
  64. data/lib/julewire/core/execution/scope_fields.rb +76 -0
  65. data/lib/julewire/core/execution/scope_identity.rb +71 -0
  66. data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
  67. data/lib/julewire/core/execution/summary_state.rb +206 -0
  68. data/lib/julewire/core/execution/view.rb +56 -0
  69. data/lib/julewire/core/facade_methods.rb +181 -0
  70. data/lib/julewire/core/fields/attribute_keys.rb +54 -0
  71. data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
  72. data/lib/julewire/core/fields/bags.rb +123 -0
  73. data/lib/julewire/core/fields/carry_proxy.rb +22 -0
  74. data/lib/julewire/core/fields/context_proxy.rb +11 -0
  75. data/lib/julewire/core/fields/field_set.rb +78 -0
  76. data/lib/julewire/core/fields/field_stack.rb +269 -0
  77. data/lib/julewire/core/fields/internal/deletion.rb +68 -0
  78. data/lib/julewire/core/fields/internal.rb +87 -0
  79. data/lib/julewire/core/fields/lookup.rb +35 -0
  80. data/lib/julewire/core/fields/section_proxy.rb +88 -0
  81. data/lib/julewire/core/fields/stack_set.rb +69 -0
  82. data/lib/julewire/core/fields/static_labels.rb +43 -0
  83. data/lib/julewire/core/fields/summary_proxy.rb +62 -0
  84. data/lib/julewire/core/integration/configurable.rb +52 -0
  85. data/lib/julewire/core/integration/destination_health.rb +43 -0
  86. data/lib/julewire/core/integration/event_subscriber.rb +62 -0
  87. data/lib/julewire/core/integration/facade.rb +131 -0
  88. data/lib/julewire/core/integration/fork_hooks.rb +79 -0
  89. data/lib/julewire/core/integration/health.rb +41 -0
  90. data/lib/julewire/core/integration/ivar_state.rb +38 -0
  91. data/lib/julewire/core/integration/lifecycle.rb +22 -0
  92. data/lib/julewire/core/integration/scoped.rb +34 -0
  93. data/lib/julewire/core/integration/settings.rb +92 -0
  94. data/lib/julewire/core/integration/subscriber_install.rb +39 -0
  95. data/lib/julewire/core/integration/subscription.rb +29 -0
  96. data/lib/julewire/core/integration/values.rb +192 -0
  97. data/lib/julewire/core/lifecycle_error.rb +7 -0
  98. data/lib/julewire/core/local_storage.rb +91 -0
  99. data/lib/julewire/core/processing/level_threshold.rb +53 -0
  100. data/lib/julewire/core/processing/match.rb +74 -0
  101. data/lib/julewire/core/processing/pipeline.rb +360 -0
  102. data/lib/julewire/core/processing/processor_chain.rb +69 -0
  103. data/lib/julewire/core/processing/processor_registry.rb +115 -0
  104. data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
  105. data/lib/julewire/core/processing/record_field_transform.rb +124 -0
  106. data/lib/julewire/core/processing/sampling.rb +109 -0
  107. data/lib/julewire/core/processing.rb +41 -0
  108. data/lib/julewire/core/propagation/carrier.rb +93 -0
  109. data/lib/julewire/core/propagation.rb +50 -0
  110. data/lib/julewire/core/records/console_formatter.rb +24 -0
  111. data/lib/julewire/core/records/deconstruct.rb +19 -0
  112. data/lib/julewire/core/records/display_message.rb +166 -0
  113. data/lib/julewire/core/records/draft.rb +576 -0
  114. data/lib/julewire/core/records/formatter.rb +14 -0
  115. data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
  116. data/lib/julewire/core/records/metadata.rb +23 -0
  117. data/lib/julewire/core/records/public_projection.rb +51 -0
  118. data/lib/julewire/core/records/raw_input.rb +41 -0
  119. data/lib/julewire/core/records/record.rb +175 -0
  120. data/lib/julewire/core/records/severity.rb +44 -0
  121. data/lib/julewire/core/runtime.rb +515 -0
  122. data/lib/julewire/core/runtime_locator.rb +20 -0
  123. data/lib/julewire/core/runtime_registry.rb +48 -0
  124. data/lib/julewire/core/runtime_state.rb +39 -0
  125. data/lib/julewire/core/scheduling/deadline.rb +24 -0
  126. data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
  127. data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
  128. data/lib/julewire/core/sentinel.rb +18 -0
  129. data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
  130. data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
  131. data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
  132. data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
  133. data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
  134. data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
  135. data/lib/julewire/core/serialization/exception_shape.rb +88 -0
  136. data/lib/julewire/core/serialization/json_encoder.rb +69 -0
  137. data/lib/julewire/core/serialization/serializer.rb +233 -0
  138. data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
  139. data/lib/julewire/core/serialization/text_encoder.rb +147 -0
  140. data/lib/julewire/core/serialization/value_copy.rb +209 -0
  141. data/lib/julewire/core/serialization/value_traversal.rb +150 -0
  142. data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
  143. data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
  144. data/lib/julewire/core/testing/chaos/destination.rb +55 -0
  145. data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
  146. data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
  147. data/lib/julewire/core/testing/chaos.rb +80 -0
  148. data/lib/julewire/core/testing/contracts/component.rb +162 -0
  149. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
  150. data/lib/julewire/core/testing/contracts/integration.rb +166 -0
  151. data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
  152. data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
  153. data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
  154. data/lib/julewire/core/testing/contracts/wire.rb +60 -0
  155. data/lib/julewire/core/testing/contracts.rb +24 -0
  156. data/lib/julewire/core/testing/coverage.rb +58 -0
  157. data/lib/julewire/core/testing/test_reports.rb +78 -0
  158. data/lib/julewire/core/testing.rb +122 -0
  159. data/lib/julewire/core/validation.rb +69 -0
  160. data/lib/julewire/core/version.rb +7 -0
  161. data/lib/julewire/core.rb +80 -0
  162. data/lib/julewire/error.rb +5 -0
  163. data/lib/julewire-core.rb +3 -0
  164. 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