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