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,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Execution
6
+ # @api public
7
+ # Read-only view returned by Julewire.current_execution.
8
+ class View
9
+ attr_reader :finished_at, :id, :lineage, :started_at, :type
10
+
11
+ def initialize(scope)
12
+ @scope = scope
13
+ @id = scope.id
14
+ @type = scope.type
15
+ @started_at = scope.started_at
16
+ @finished_at = scope.finished_at
17
+ @parent_scope = scope.parent
18
+ @lineage = scope.lineage
19
+ @parent = nil
20
+ @execution_hash = nil
21
+ @context_hash = nil
22
+ @carry_hash = nil
23
+ @neutral_hash = nil
24
+ @attributes_hash = nil
25
+ @labels_hash = nil
26
+ @summary_hash = nil
27
+ @metrics_hash = nil
28
+ end
29
+
30
+ def parent
31
+ return unless @parent_scope
32
+
33
+ @parent ||= self.class.new(@parent_scope)
34
+ end
35
+
36
+ def execution_hash = Fields::FieldSet.deep_dup(@execution_hash ||= @scope.frozen_execution_hash)
37
+
38
+ def context_hash = Fields::FieldSet.deep_dup(@context_hash ||= @scope.context_hash)
39
+
40
+ def carry_hash = Fields::FieldSet.deep_dup(@carry_hash ||= @scope.carry_hash)
41
+
42
+ def neutral_hash = Fields::FieldSet.deep_dup(@neutral_hash ||= @scope.neutral_hash)
43
+
44
+ def attributes_hash = Fields::FieldSet.deep_dup(@attributes_hash ||= @scope.attributes_hash)
45
+
46
+ def labels_hash = Fields::FieldSet.deep_dup(@labels_hash ||= @scope.frozen_labels_hash)
47
+
48
+ def summary_hash = Fields::FieldSet.deep_dup(@summary_hash ||= @scope.summary_hash)
49
+
50
+ def metrics_hash = Fields::FieldSet.deep_dup(@metrics_hash ||= @scope.metrics_hash)
51
+
52
+ def finished? = !finished_at.nil?
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module FacadeMethods
6
+ def runtime(name = :default)
7
+ Core::RuntimeRegistry.fetch(name, current: Core::RuntimeLocator.current)
8
+ end
9
+
10
+ def config = runtime.config
11
+
12
+ def configure(&) = runtime.configure(&)
13
+
14
+ def context = runtime.context
15
+
16
+ def attributes = runtime.attributes
17
+
18
+ def carry = runtime.carry
19
+
20
+ def current_execution = runtime.current_execution
21
+
22
+ def current_execution? = runtime.current_execution?
23
+
24
+ def emit(record = Core::UNSET, **fields, &)
25
+ runtime.emit(record, **fields, &)
26
+ end
27
+
28
+ def debug(record = Core::UNSET, **fields, &)
29
+ emit_with_severity(:debug, record, fields, &)
30
+ end
31
+
32
+ def info(record = Core::UNSET, **fields, &)
33
+ emit_with_severity(:info, record, fields, &)
34
+ end
35
+
36
+ def warn(record = Core::UNSET, **fields, &)
37
+ emit_with_severity(:warn, record, fields, &)
38
+ end
39
+
40
+ def error(record = Core::UNSET, **fields, &)
41
+ emit_with_severity(:error, record, fields, &)
42
+ end
43
+
44
+ def fatal(record = Core::UNSET, **fields, &)
45
+ emit_with_severity(:fatal, record, fields, &)
46
+ end
47
+
48
+ def unknown(record = Core::UNSET, **fields, &)
49
+ emit_with_severity(:unknown, record, fields, &)
50
+ end
51
+
52
+ def flush(timeout: Core::UNSET)
53
+ runtime.flush(timeout: timeout)
54
+ end
55
+
56
+ def health = runtime.health
57
+
58
+ def measure(key, &)
59
+ summary.measure(key, &)
60
+ end
61
+
62
+ def measure_start(key) = summary.measure_start(key)
63
+
64
+ def doctor(name = :default)
65
+ Core::Diagnostics::Doctor.call(runtime(name))
66
+ end
67
+
68
+ def tail(name = :default, **)
69
+ Core::Diagnostics::Tail.attach!(runtime(name), **)
70
+ end
71
+
72
+ def observe_self!(name = :default, **)
73
+ Core::Diagnostics::MetaObserver.attach!(name, **)
74
+ end
75
+
76
+ def dev!(name = :default, output: $stdout, color: Core::UNSET, chaos: false, banner: chaos, tail: true)
77
+ color = output.respond_to?(:tty?) ? output.tty? : true if color.equal?(Core::UNSET)
78
+ punk!(name, output: output, color: color, chaos: chaos, banner: banner)
79
+ return unless tail
80
+
81
+ tail_options = tail == true ? {} : tail
82
+ raise ArgumentError, "tail must be true, false, or an options Hash" unless tail_options.is_a?(Hash)
83
+
84
+ Core::Diagnostics::Tail.attach!(runtime(name), **tail_options)
85
+ end
86
+
87
+ def punk!(name = :default, output: $stdout, color: true, chaos: false, banner: chaos)
88
+ output.write(punk_banner) if banner
89
+ output = punk_chaos_output(output, chaos) if chaos
90
+
91
+ runtime(name).configure do |config|
92
+ config.destinations.clear
93
+ config.destinations.use(
94
+ :default,
95
+ formatter: ConsoleFormatter.new,
96
+ encoder: TextEncoder.new(color: color, theme: :punk),
97
+ output: output
98
+ )
99
+ end
100
+ end
101
+
102
+ def fiber(**, &)
103
+ raise ArgumentError, "block required" unless block_given?
104
+
105
+ envelope = Core::Propagation.capture_local
106
+ Fiber.new(**) do |*args|
107
+ with_cleared_configure_guard do
108
+ Core::Propagation.restore(envelope) { yield(*args) }
109
+ end
110
+ end
111
+ end
112
+
113
+ def labels = runtime.labels
114
+
115
+ def after_fork! = runtime.after_fork!
116
+
117
+ def reset! = runtime.reset!
118
+
119
+ def close(timeout: Core::UNSET)
120
+ runtime.close(timeout: timeout)
121
+ end
122
+
123
+ def summary = runtime.summary
124
+
125
+ def start_execution(type:, **)
126
+ runtime.start_execution(type: type, **)
127
+ end
128
+
129
+ def thread(*, &)
130
+ raise ArgumentError, "block required" unless block_given?
131
+
132
+ envelope = Core::Propagation.capture_local
133
+ Thread.new(*) do |*thread_args|
134
+ with_cleared_configure_guard do
135
+ Core::Propagation.restore(envelope) { yield(*thread_args) }
136
+ end
137
+ end
138
+ end
139
+
140
+ def with_execution(type:, **, &)
141
+ runtime.with_execution(type: type, **, &)
142
+ end
143
+
144
+ private
145
+
146
+ def punk_banner
147
+ "!!JULEWIRE PUNK!! chaos containment armed\n"
148
+ end
149
+
150
+ def punk_chaos_output(output, chaos)
151
+ options = chaos.is_a?(Hash) ? chaos : {}
152
+ Core::Destinations::ChaosOutput.new(output, **options)
153
+ end
154
+
155
+ def emit_with_severity(severity, record, fields, &)
156
+ if record.equal?(Core::UNSET)
157
+ fields.delete("severity")
158
+ fields[:severity] = severity
159
+ runtime.emit(fields, &)
160
+ elsif !block_given? && !record.is_a?(Hash)
161
+ # Scalar eager logs stay allocation-light; lazy inputs need the wrapper
162
+ # so block-built records can still receive the eager severity.
163
+ input = fields.empty? ? { message: record.to_s } : Core.emit_input(record, fields)
164
+ input.delete("severity")
165
+ input[:severity] = severity
166
+ runtime.emit(input)
167
+ else
168
+ runtime.emit(Core::Records::LazyEmitInput.with_severity(severity, Core.emit_input(record, fields)), &)
169
+ end
170
+ end
171
+
172
+ def with_cleared_configure_guard
173
+ previous_guard = Fiber[Core::Runtime::CONFIGURE_GUARD_KEY]
174
+ Fiber[Core::Runtime::CONFIGURE_GUARD_KEY] = nil
175
+ yield
176
+ ensure
177
+ Fiber[Core::Runtime::CONFIGURE_GUARD_KEY] = previous_guard
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ module Julewire
5
+ module Core
6
+ module Fields
7
+ # @api integration_spi
8
+ module AttributeKeys
9
+ HTTP_REQUEST_METHOD = :"http.request.method"
10
+ HTTP_RESPONSE_BODY_SIZE = :"http.response.body.size"
11
+ HTTP_RESPONSE_STATUS_CODE = :"http.response.status_code"
12
+ URL_FULL = :"url.full"
13
+ URL_PATH = :"url.path"
14
+ USER_AGENT_ORIGINAL = :"user_agent.original"
15
+ CLIENT_ADDRESS = :"client.address"
16
+
17
+ CODE_FILE_PATH = :"code.file.path"
18
+ CODE_FUNCTION_NAME = :"code.function.name"
19
+ CODE_LINE_NUMBER = :"code.line.number"
20
+
21
+ MESSAGING_BATCH_MESSAGE_COUNT = :"messaging.batch.message_count"
22
+ MESSAGING_CONSUMER_GROUP_NAME = :"messaging.consumer.group.name"
23
+ MESSAGING_DESTINATION_NAME = :"messaging.destination.name"
24
+ MESSAGING_DESTINATION_PARTITION_ID = :"messaging.destination.partition.id"
25
+ MESSAGING_KAFKA_MESSAGE_KEY = :"messaging.kafka.message.key"
26
+ MESSAGING_KAFKA_OFFSET = :"messaging.kafka.offset"
27
+ MESSAGING_OPERATION_NAME = :"messaging.operation.name"
28
+ MESSAGING_OPERATION_TYPE = :"messaging.operation.type"
29
+ MESSAGING_SYSTEM = :"messaging.system"
30
+
31
+ JOB_ENQUEUED_AT = :"job.enqueued_at"
32
+ JOB_EXECUTION_COUNT = :"job.execution_count"
33
+ JOB_ID = :"job.id"
34
+ JOB_NAME = :"job.name"
35
+ JOB_PRIORITY = :"job.priority"
36
+ JOB_PROVIDER_ID = :"job.provider_id"
37
+ JOB_QUEUE_NAME = :"job.queue.name"
38
+ JOB_SCHEDULED_AT = :"job.scheduled_at"
39
+ JOB_STATUS = :"job.status"
40
+ JOB_SYSTEM = :"job.system"
41
+
42
+ class << self
43
+ def fields(fields)
44
+ return {} unless fields.is_a?(Hash)
45
+
46
+ fields.compact
47
+ end
48
+
49
+ def from(neutral) = neutral.is_a?(Hash) ? neutral : {}
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Fields
6
+ class AttributesProxy < SectionProxy
7
+ def initialize(store) = super(store, :attributes)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Fields
6
+ # @api extension
7
+ module Bags
8
+ Definition = Data.define(
9
+ :name,
10
+ :record_hash,
11
+ :transform_container,
12
+ :app_write,
13
+ :integration_write,
14
+ :propagate,
15
+ :emit_by_default,
16
+ :delete_paths,
17
+ :stack
18
+ )
19
+ RECORD_SCALAR_KEYS = %i[
20
+ timestamp
21
+ severity
22
+ kind
23
+ event
24
+ message
25
+ logger
26
+ source
27
+ ].freeze
28
+
29
+ class << self
30
+ private
31
+
32
+ def define(name, **capabilities)
33
+ Definition.new(
34
+ name: name,
35
+ record_hash: true,
36
+ transform_container: true,
37
+ app_write: false,
38
+ integration_write: false,
39
+ propagate: false,
40
+ emit_by_default: true,
41
+ delete_paths: false,
42
+ stack: false,
43
+ **capabilities
44
+ )
45
+ end
46
+ end
47
+
48
+ DEFINITIONS = {
49
+ execution: define(:execution, integration_write: true, propagate: true),
50
+ context: define(
51
+ :context,
52
+ app_write: true,
53
+ integration_write: true,
54
+ propagate: true,
55
+ stack: true
56
+ ),
57
+ carry: define(
58
+ :carry,
59
+ app_write: true,
60
+ integration_write: true,
61
+ propagate: true,
62
+ emit_by_default: false,
63
+ delete_paths: true,
64
+ stack: true
65
+ ),
66
+ neutral: define(:neutral, integration_write: true, emit_by_default: false, stack: true),
67
+ attributes: define(
68
+ :attributes,
69
+ app_write: true,
70
+ integration_write: true,
71
+ stack: true
72
+ ),
73
+ labels: define(:labels),
74
+ payload: define(:payload),
75
+ metrics: define(:metrics),
76
+ error: define(:error, record_hash: false),
77
+ summary: define(
78
+ :summary,
79
+ record_hash: false,
80
+ transform_container: false,
81
+ integration_write: true,
82
+ emit_by_default: false
83
+ )
84
+ }.freeze
85
+ private_constant :Definition, :DEFINITIONS, :RECORD_SCALAR_KEYS
86
+
87
+ class << self
88
+ def definition(name) = DEFINITIONS.fetch(name)
89
+
90
+ def record_scalar_keys = RECORD_SCALAR_KEYS
91
+
92
+ def record_hash_sections = select_names(:record_hash)
93
+
94
+ def required_record_keys = (record_scalar_keys + record_hash_sections + %i[error]).freeze
95
+
96
+ def transform_container_sections = select_names(:transform_container)
97
+
98
+ def hidden_output_sections
99
+ DEFINITIONS.filter_map do |name, definition|
100
+ name if definition.record_hash && !definition.emit_by_default
101
+ end.freeze
102
+ end
103
+
104
+ def app_write_sections = select_names(:app_write)
105
+
106
+ def integration_write_sections = select_names(:integration_write)
107
+
108
+ def propagation_sections = select_names(:propagate)
109
+
110
+ def stack_sections = select_names(:stack)
111
+
112
+ def delete_paths?(name) = definition(name).delete_paths
113
+
114
+ private
115
+
116
+ def select_names(attribute)
117
+ DEFINITIONS.filter_map { |name, definition| name if definition.public_send(attribute) }.freeze
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Fields
6
+ class CarryProxy < SectionProxy
7
+ def initialize(store) = super(store, :carry)
8
+
9
+ def delete(*path)
10
+ @store.delete_carry(path)
11
+ self
12
+ end
13
+
14
+ def without(*path, &)
15
+ raise ArgumentError, "block required" unless block_given?
16
+
17
+ @store.without_carry(path, &)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Fields
6
+ class ContextProxy < SectionProxy
7
+ def initialize(store) = super(store, :context)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Fields
6
+ # @api integration_spi
7
+ module FieldSet
8
+ VALUE_KEY = :value
9
+
10
+ class << self
11
+ # Public ingress accepts String or Symbol keys. Core stores Symbol keys
12
+ # after normalization so extension contracts stay simple.
13
+ def coerce(fields = nil, keyword_fields = {}, invalid: :ignore)
14
+ coerced = {}
15
+ coerce_fields!(coerced, fields, invalid: invalid) unless fields.nil?
16
+ merge!(coerced, keyword_fields) unless keyword_fields.empty?
17
+ coerced
18
+ end
19
+
20
+ def merge(left, right)
21
+ merge!(deep_symbolize_keys(left), right)
22
+ end
23
+
24
+ def merge!(target, fields)
25
+ return target unless fields.is_a?(Hash)
26
+
27
+ fields.each do |key, value|
28
+ target[Fields::Internal.normalize_key(key)] = copy_field_value(value)
29
+ end
30
+
31
+ target
32
+ end
33
+
34
+ def deep_dup(value)
35
+ return {} if value.is_a?(Hash) && value.empty?
36
+ return [] if value.is_a?(Array) && value.empty?
37
+
38
+ Serialization::ValueCopy.call(value)
39
+ end
40
+
41
+ def deep_symbolize_keys(value)
42
+ return {} if value.is_a?(Hash) && value.empty?
43
+ return [] if value.is_a?(Array) && value.empty?
44
+
45
+ Serialization::ValueCopy.call(value, symbolize_keys: true)
46
+ end
47
+
48
+ def frozen_copy(value)
49
+ Fields::Internal.frozen_copy(value)
50
+ end
51
+
52
+ def value_for(hash, key, default: nil)
53
+ return default unless hash.is_a?(Hash)
54
+
55
+ normalized = key.is_a?(String) ? Fields::Internal.normalize_key(key) : key
56
+ return hash[normalized] if hash.key?(normalized)
57
+
58
+ default
59
+ end
60
+
61
+ private
62
+
63
+ def coerce_fields!(target, fields, invalid:)
64
+ if fields.is_a?(Hash)
65
+ merge!(target, fields)
66
+ elsif invalid == :wrap
67
+ target[VALUE_KEY] = deep_dup(fields)
68
+ elsif invalid == :raise
69
+ raise ArgumentError, "fields must be a Hash"
70
+ end
71
+ end
72
+
73
+ def copy_field_value(value) = deep_symbolize_keys(value)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end