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,567 @@
1
+ # Extensions and API
2
+
3
+ Core extension contracts are intentionally Ruby-ish: small objects, `call`,
4
+ `write`, named destinations, and optional lifecycle methods.
5
+
6
+ `contracts.md` is the contract-tier source of truth. This page adds usage
7
+ detail for extension and integration authors.
8
+
9
+ Core uses duck typing. Configuration-time checks only require the expected
10
+ method names (`call`, `write`, `emit`, `name`). Arity, keyword, and return-value
11
+ mistakes are contained when the extension is invoked and are reported through
12
+ small health counters where possible.
13
+
14
+ ## Processors
15
+
16
+ Processors respond to:
17
+
18
+ ```ruby
19
+ processor.call(draft)
20
+ ```
21
+
22
+ They receive the current `Julewire::RecordDraft`. Public `Julewire.emit`
23
+ input is defensive; processors own the mutable draft until the final immutable
24
+ record boundary.
25
+
26
+ Allowed processor returns:
27
+
28
+ ```ruby
29
+ nil # draft was mutated in place or unchanged
30
+ draft # explicit draft return
31
+ :drop # stop delivery
32
+ anything else # ignored; current draft continues
33
+ ```
34
+
35
+ Mutate the draft for ordinary enrichment:
36
+
37
+ ```ruby
38
+ draft[:severity] = :warn
39
+ draft[:payload][:sampled] = true
40
+ ```
41
+
42
+ Draft sections are owned by the processor pipeline, so direct mutation is the
43
+ primary processor API:
44
+
45
+ ```ruby
46
+ draft.fetch(:context).fetch(:account)[:id] = "changed"
47
+ ```
48
+
49
+ Use transform helpers when replacing values or sections. They invalidate cached
50
+ records and keep execution lineage when execution identity is unchanged:
51
+
52
+ ```ruby
53
+ draft.transform_field!(:severity) { :warn }
54
+ draft.transform_section!(:payload) { |payload| payload.merge(sampled: true) }
55
+ ```
56
+
57
+ Use `transform_record!` for whole-record replacement transforms:
58
+
59
+ ```ruby
60
+ draft.transform_record! { |data| redact(data) }
61
+ ```
62
+
63
+ Do not call `to_record` and then mutate a fetched section in place; direct
64
+ section mutation cannot invalidate an already-cached immutable record.
65
+
66
+ Whole-record transforms receive core-owned draft data. Return a normalized
67
+ record hash; the pipeline validates it when the draft becomes immutable. Values
68
+ set on a draft may be frozen at that boundary.
69
+
70
+ Class entries pass positional and keyword constructor arguments to the
71
+ processor class. Register an already-built object when the application owns the
72
+ instance lifecycle.
73
+
74
+ ```ruby
75
+ class AddAttribute
76
+ def call(draft)
77
+ draft[:attributes][:app] = { processed: true }
78
+ end
79
+ end
80
+
81
+ Julewire.configure do |config|
82
+ config.processors.use AddTag
83
+ end
84
+ ```
85
+
86
+ For simple predicate policies, `Julewire::Match` is a processor:
87
+
88
+ ```ruby
89
+ config.processors.use Julewire::Match.new do
90
+ on(event: /^active_record\./, payload: { duration_ms: 100.. }) do |draft|
91
+ draft[:labels][:slow_sql] = true
92
+ end
93
+
94
+ on(severity: :debug) { :drop }
95
+ end
96
+ ```
97
+
98
+ For deterministic head sampling, use the registered `:sampling` processor:
99
+
100
+ ```ruby
101
+ config.processors.use(
102
+ :sampling,
103
+ rate: 0.1,
104
+ key: ->(draft) { draft.lineage.root_reference&.fetch(:id, nil) }
105
+ )
106
+ ```
107
+
108
+ The default key uses execution/root identifiers, then `context.request_id`, then
109
+ deterministic record fields. A custom `key:` callable returning `nil` drops the
110
+ record. `Julewire::Sampling.keep?(rate:, key:)` exposes the same deterministic
111
+ rate decision for custom processor and destination policies.
112
+
113
+ Processor exceptions are contained according to the registration policy.
114
+ The default is `on_error: :fail_closed`: core attempts to emit a minimal
115
+ `julewire.processor_error` record, the original record is not delivered, and
116
+ later processors are not run. Use `on_error: :fail_open` for non-critical
117
+ enrichment processors that should record the failure, keep the current draft,
118
+ and continue. Use `on_error: :drop` when a failing processor should suppress
119
+ the record. `on_error:` is a registry option, not a processor constructor
120
+ keyword.
121
+
122
+ Non-raising draft corruption is detected at the final immutable
123
+ `Julewire::Record` boundary and contained as an `emit_record` failure
124
+ without per-processor attribution.
125
+
126
+ Processors can inspect execution lineage before the default formatter strips it
127
+ from public output. Promote only the pieces you want to expose:
128
+
129
+ ```ruby
130
+ config.processors.use do |draft|
131
+ root_id = draft.lineage.root_reference&.fetch(:id, nil)
132
+ ancestor_count = draft.lineage.ancestors.length
133
+
134
+ draft[:labels][:root_execution_id] = root_id if root_id
135
+ draft[:payload][:ancestor_count] = ancestor_count
136
+ end
137
+ ```
138
+
139
+ Registry methods:
140
+
141
+ ```ruby
142
+ config.processors.use ProcessorClass
143
+ config.processors.use ProcessorClass, "constructor-arg", enabled: true
144
+ config.processors.use EnrichmentProcessor, on_error: :fail_open
145
+ config.processors.prepend FirstProcessor
146
+ config.processors.prepend(:redaction, on_error: :fail_closed)
147
+ config.processors.clear
148
+ ```
149
+
150
+ Class entries are instantiated at configure time. Stateful processors should be
151
+ designed with that lifecycle in mind.
152
+ Integration gems may register processor kinds; applications wire them through
153
+ the same registry instead of constructing the common processor object directly.
154
+ Processor kind names are part of each integration's extension contract.
155
+
156
+ ## Emit Input Lifecycle
157
+
158
+ Application emit input crosses a few small objects before it becomes a draft:
159
+
160
+ | Step | Owner | Job |
161
+ | ---- | ----- | --- |
162
+ | Facade merge | `Core.emit_input` | Combine positional input and keyword fields without normalizing app objects. |
163
+ | Lazy block | `Records::LazyEmitInput` | Keep block-built payloads lazy until the level gate passes and preserve eager severity helpers. |
164
+ | Threshold peek | `Records::RawInput` | Read severity, source, and event from raw input without building a record. |
165
+ | Draft build | `Draft::BuildInput` | Split raw input into normalized top-level fields plus payload. |
166
+
167
+ This split keeps below-threshold eager input and lazy blocks cheap while the
168
+ final immutable `Record` boundary still validates the full shape.
169
+
170
+ ## Formatters
171
+
172
+ Formatters respond to:
173
+
174
+ ```ruby
175
+ formatter.call(record)
176
+ ```
177
+
178
+ They return a payload object. The default formatter is
179
+ `Julewire::RecordFormatter`, which returns a public projection of the record. It
180
+ omits internal keys such as `:carry` and execution lineage internals.
181
+ `Julewire::Core::Records::PublicProjection.public_execution` exposes the same
182
+ execution projection without building the full output hash.
183
+
184
+ Custom formatters receive container-frozen `Julewire::Record` objects,
185
+ including the top-level `:carry` section. Hashes, arrays, and copied strings
186
+ inside the record are frozen; arbitrary app objects inside fields are still
187
+ object references. Use `record.to_h` for a mutable hash copy.
188
+ Formatters are responsible for destination-specific shape and must not mutate
189
+ the record; redaction policy belongs in processors or application code before
190
+ formatting.
191
+
192
+ ## Encoders
193
+
194
+ Encoders respond to:
195
+
196
+ ```ruby
197
+ encoder.call(payload)
198
+ ```
199
+
200
+ They receive formatter output and return the string written to the destination
201
+ output. The default encoder is `Julewire::JsonEncoder`, which writes one
202
+ serialized JSON object plus a newline. It applies Julewire serialization,
203
+ including JSON-safe primitives, string keys, bounds, and empty-field
204
+ compaction, before calling `JSON.generate`. `Julewire::TextEncoder` renders a
205
+ console payload or pre-rendered string as one text line.
206
+
207
+ Formatters own shape. Encoders own serialization and bytes. Keep provider
208
+ mapping, field names, and record projection in formatters; keep JSON, Oj, YAML,
209
+ raw text, or other byte encoding decisions in encoders.
210
+
211
+ ## Destinations
212
+
213
+ Direct core destinations pair one formatter, one encoder, and one output:
214
+
215
+ ```ruby
216
+ config.destinations.use(
217
+ :json_stdout,
218
+ formatter: Julewire::RecordFormatter.new,
219
+ encoder: Julewire::JsonEncoder.new,
220
+ output: $stdout
221
+ )
222
+ ```
223
+
224
+ Integration gems may register destination kinds. Use those through the same
225
+ runtime registry:
226
+
227
+ ```ruby
228
+ config.destinations.use(:provider_json, output: $stdout)
229
+ config.destinations.use(:transport, formatter: formatter, io: $stdout)
230
+ ```
231
+
232
+ Destination kind names are part of each integration's extension contract. A
233
+ leaf gem may also claim a familiar kind such as `:default` when it deliberately
234
+ changes that destination's runtime behavior.
235
+
236
+ When no destinations are configured, core runs in no-output mode and increments
237
+ `health[:pipeline][:counts][:no_output_dropped]`.
238
+
239
+ Destination names must be unique. Destination formatters get immutable
240
+ `Julewire::Record` objects after processors have run. The destination
241
+ boundary freezes normalized record containers so all formatters see the same
242
+ consistent container shape.
243
+ Use `Julewire::Core::Destinations.normalize_name` when custom destination
244
+ adapters accept a user-provided destination name.
245
+ Encoders turn formatter payload objects into strings for direct destinations.
246
+ Destination `on_failure` and `on_drop` callbacks inherit from global callbacks
247
+ unless overridden in `config.destinations.use`.
248
+ `processors:` may be passed to `config.destinations.use` for destination-local
249
+ policy. Those processors run after global processors and before that destination
250
+ formats the record. Their drops and failures are scoped to that destination.
251
+
252
+ Custom destinations can bypass the built-in formatter/JSON/output destination
253
+ and add an object directly:
254
+
255
+ ```ruby
256
+ config.destinations.add(MyDestination.new(name: :custom))
257
+ ```
258
+
259
+ Custom destinations must respond to `name`, `emit(record)`, `flush(timeout:)`,
260
+ `close(timeout:)`, and `health`. `flush` and `close` are successful when they
261
+ return without raising unless they return `false`.
262
+ `emit(record)` is successful when it returns without raising unless it returns
263
+ `false`. A custom destination exception is both a destination failure and a
264
+ dropped record; a plain `false` is a rejected record and calls `on_drop` with
265
+ `:destination_rejected`.
266
+
267
+ Custom destinations may also implement `after_fork!` for fork reset and
268
+ `resource_identity` when multiple destinations share the same closeable
269
+ resource. Transport adapters may expose adapter-specific lifecycle methods such
270
+ as `reopen`.
271
+
272
+ The registered `:tail_sampling` destination kind wraps another destination for
273
+ execution-level tail sampling. It buffers execution records until a summary
274
+ record arrives, keeps error and slow executions, samples the rest with
275
+ `Julewire::Sampling`, and forwards kept records to the wrapped destination:
276
+
277
+ ```ruby
278
+ config.destinations.use(
279
+ :tail_sampling,
280
+ destination: Julewire::Core::Destinations::Destination.new(output: $stdout),
281
+ sample_rate: 0.1,
282
+ slow_ms: 250
283
+ )
284
+ ```
285
+
286
+ ## Outputs
287
+
288
+ Outputs respond to:
289
+
290
+ ```ruby
291
+ output.write(string)
292
+ ```
293
+
294
+ They may also implement:
295
+
296
+ ```ruby
297
+ flush
298
+ close
299
+ health
300
+ ```
301
+
302
+ Output lifecycle hooks are sync and local. Runtime timeouts provide one carried
303
+ deadline while core walks destinations, but plain outputs do not receive the
304
+ timeout value and core cannot interrupt blocking raw outputs. After the first
305
+ attempted resource, exhausted deadlines skip later resources. Custom
306
+ destinations own async drain, retry, reopen targets, rotation, and
307
+ timeout-aware shutdown.
308
+
309
+ Plain output writes are successful when `write` returns without raising and does
310
+ not return `false`. A plain `false` is treated as a rejected write. A rejection
311
+ is a dropped record at the core destination boundary: the destination increments
312
+ `output_rejected` and calls `on_drop` with `:output_rejected`. Raise an
313
+ exception for ordinary failures.
314
+
315
+ Custom destinations that need richer backpressure, retry, or partial-accept
316
+ semantics should keep that policy inside the destination and expose it through
317
+ destination health.
318
+
319
+ ## Utility APIs
320
+
321
+ `contracts.md` owns the public utility inventory. The notes below cover the
322
+ parts with important ownership or boundary rules.
323
+
324
+ `EncodingSanitizer.call` repairs strings into valid UTF-8. It is intentionally
325
+ string-only; passing other objects is a type error.
326
+
327
+ `FieldSet` is the public helper for integration-owned field hashes. Its
328
+ documented surface is:
329
+
330
+ - `coerce`
331
+ - `merge` and `merge!`
332
+ - `deep_dup` and `deep_symbolize_keys`
333
+ - `frozen_copy`
334
+ - `value_for`
335
+ - `VALUE_KEY`
336
+
337
+ `coerce`, `merge`, and `merge!` normalize string keys to symbols and
338
+ defensive-copy values before inserting them, so later caller mutation does not
339
+ mutate core field containers. Use symbol keys after that boundary. `VALUE_KEY`
340
+ is the key used when non-hash field input is wrapped instead of dropped.
341
+
342
+ Other `FieldSet` singleton helpers are core-internal implementation support and
343
+ are not part of the extension contract.
344
+
345
+ `FieldSet.deep_dup` is intentionally narrow: it copies `Hash`, `Array`, and
346
+ mutable `String` values, relies on Ruby's hash-key string safety for string
347
+ keys, and handles cycles. Arbitrary mutable objects remain caller-owned.
348
+ Encoders and transport boundaries that need pure log-safe data should use
349
+ `Serializer.call` there.
350
+
351
+ `RecordFieldTransform` walks core's normalized record containers with
352
+ `BoundedTransform`. It owns record-shape policy only; processors supply the
353
+ actual filtering or replacement policy.
354
+
355
+ `Carrier` serializes propagation envelopes into flat string carriers for
356
+ external boundaries. It is provider-neutral and does not parse or synthesize
357
+ external headers. Use `max_bytes:` to leave a carrier unchanged and return
358
+ `nil` when the serialized envelope is too large for the target boundary.
359
+
360
+ `Julewire::RecordDraft.build` is the raw-input construction path used by core and
361
+ integration code. `Julewire::RecordDraft#to_record` freezes the final normalized data into
362
+ an immutable `Record` for formatters and destinations. `Record` is not a raw
363
+ input builder; it is the read-only destination boundary. Use
364
+ `Record.from_normalized_hash` only when an extension already owns a complete
365
+ symbol-key normalized record hash and needs the immutable destination shape.
366
+
367
+ ## Public Facade
368
+
369
+ `contracts.md` owns the public facade inventory. This section covers usage
370
+ details that extension and integration authors usually need.
371
+
372
+ Integrations that keep process-local state can register a reset hook with
373
+ `Julewire::Core::Integration::Lifecycle.register_after_fork(:integration_name,
374
+ component: :component_name) { ... }`. The hook runs after core has refreshed its
375
+ own process-local state and after the active pipeline has forwarded
376
+ `after_fork!` to destinations.
377
+
378
+ `Julewire.observe_self!(runtime_name = :default, target: :meta)` starts a
379
+ `Julewire::Core::Diagnostics::MetaObserver`. The observer samples one runtime's
380
+ health and emits health-change records into another named runtime. Pass
381
+ `start: false` and call `sample!` manually when deterministic polling is
382
+ preferred.
383
+
384
+ Framework and provider adapters may also use the core integration SPI. The
385
+ `Julewire::Core::Integration` namespace is split by concern:
386
+
387
+ Health:
388
+
389
+ - `Integration::Health.record_failure` for contained process-level adapter
390
+ failures.
391
+ - `Integration::Health.record_success` for recovery after a successful
392
+ integration operation.
393
+ - `Integration::Health.scoped(:name)` for bound process-integration health
394
+ helpers. Pass `runtime:` only when a failure belongs to a known runtime rather
395
+ than a process-level framework edge.
396
+
397
+ Lifecycle:
398
+
399
+ - `Integration::Lifecycle.require_optional(path)` for contained optional
400
+ requires.
401
+ - `Integration::Lifecycle.register_after_fork(:integration_name,
402
+ component: :component_name) { ... }` for process-local integration state.
403
+ - `Integration::IvarState` for idempotent framework subscriber state.
404
+ - `Integration::Subscription` for subscriber installs that can update
405
+ configuration and best-effort unsubscribe on reset.
406
+ - `Integration::SubscriberInstall` for class-level subscriber `install!`
407
+ implementations that expose `subscriber`, `installed?`, and `reset!`.
408
+ - `Integration::EventSubscriber` for integration event subscribers that share
409
+ configuration assignment and contained integration-health `emit` handling.
410
+ - `Integration::Settings` for small integration configuration objects with
411
+ deep-copied defaults, assignment-time validation, and optional predicate
412
+ accessors.
413
+
414
+ `Integration::Settings.setting` validators may be instance method names or
415
+ procs; return a normalized value or raise.
416
+
417
+ Runtime access:
418
+
419
+ - `Integration::Facade.with_execution` for framework integrations that
420
+ build fresh execution attributes and want the same execution boundary as
421
+ `Julewire.with_execution` without copying already-owned attribute hashes.
422
+ - `Integration::Facade.emit` for framework/provider integrations that
423
+ emit already-normalized, adapter-owned record hashes. This is not the
424
+ app-facing `Julewire.emit` input path; integrations should pass explicit
425
+ record keys such as `:event`, `:source`, `:payload`, and `:attributes`.
426
+
427
+ Owned field overlays:
428
+
429
+ - `Integration::Facade.with_context`, `with_carry`, `with_attributes`, and
430
+ `with_neutral` for block-scoped, already-normalized, adapter-owned field
431
+ hashes around callback, request, or message processing.
432
+ - `Integration::Facade.add_context`, `add_carry`, `add_attributes`, and
433
+ `add_neutral` for already-normalized, adapter-owned field hashes added to the
434
+ current execution or ambient context.
435
+ `add_carry` and `add_neutral` are deliberate symmetry points for integrations
436
+ that need ambient propagation or formatter-coordination fields outside a
437
+ scoped callback.
438
+ - `Integration::Facade.add_summary_attributes` and `add_summary_neutral` for
439
+ enriching the current execution summary.
440
+ - `Integration::Facade.summary_active?` and `increment_summary_attribute` for
441
+ integrations that observe framework events inside an existing execution and
442
+ need to enrich summary counters without using the application facade.
443
+
444
+ Payload reads and shaping:
445
+
446
+ - `Integration::Values::Read.value`, `hash_value`, `nested_value`,
447
+ `path_value`, `first_value`, and `blank?` for defensive reads from hashes,
448
+ framework objects, and indexed payloads.
449
+ - `Integration::Values::Shape.timestamp`, `payload_hash`, `hash_or_empty`,
450
+ `append_field`, `append_compact_field`, and `source_location_attributes` for
451
+ common event-payload shaping.
452
+ - `Julewire::Core.sentinel(:name)` for private integration sentinels that
453
+ should print readably in failures and diffs.
454
+ - `Julewire::Core::Validation` for shared option and byte-limit validation.
455
+
456
+ `hash_value` is for strict hash reads and only bridges symbol/string key forms.
457
+ Use `value`, `nested_value`, or `path_value` for foreign objects that expose
458
+ methods or indexed access.
459
+
460
+ Bounded transforms:
461
+
462
+ - `Julewire::Core::Serialization::BoundedTransform` when a processor or adapter needs a bounded
463
+ walk with core-compatible depth, array, hash, string, cycle, and truncation
464
+ behavior.
465
+ It can insert `_julewire_truncation` metadata before the final encoder sees
466
+ the payload.
467
+ - `Julewire::Serializer.truncation_metadata` and serializer truncation
468
+ constants when an adapter must emit core-compatible truncation markers before
469
+ handing data back to core.
470
+
471
+ This SPI is documented support for integration gems, but not a compatibility
472
+ freeze. It may change when the ecosystem gets cleaner. `contracts.md` is the
473
+ source of truth for the current tier inventory.
474
+
475
+ ## Extension Contract Tests
476
+
477
+ Extensions can require `julewire/core/testing` for small test primitives:
478
+
479
+ - `Julewire::Testing::CaptureDestination`
480
+ - `Julewire::Testing::NullOutput`
481
+ - `Julewire::Testing.configure_capture_destination`
482
+ - `Julewire::Testing::Chaos`
483
+ - `Julewire::Testing::Contracts`
484
+ - `Julewire::Testing::Coverage`
485
+
486
+ These helpers are shipped support for Julewire extension and integration gems,
487
+ not runtime application API.
488
+
489
+ `Julewire::Testing::Chaos.assert_contained(test_context) { |error| ... }`
490
+ runs a small `StandardError` corpus through containment checks. Use it for
491
+ extension paths that promise to absorb formatter, processor, destination, or
492
+ subscriber failures.
493
+ `Julewire::Testing::Chaos.assert_core_runtime_containment(test_context)` runs
494
+ the same corpus through core's curated runtime containment surfaces: processors,
495
+ formatters, encoders, outputs, callbacks, and lifecycle hooks.
496
+ `Julewire::Testing::Chaos.assert_destination_chaos_contract(...)` runs the
497
+ same corpus through a destination's formatter, encoder, output or transport,
498
+ and callback containment paths using destination builders supplied by the
499
+ extension test.
500
+ `Julewire::Testing::Chaos.assert_emitter_chaos_contract(...)` runs the same
501
+ corpus through a subscriber/listener-style entrypoint while the extension test
502
+ keeps ownership of framework-shaped failing inputs.
503
+ `Julewire::Testing::Chaos.catalog { ... }` builds a deterministic component
504
+ catalog, and `assert_discovered_chaos_contracts(...)` runs the corpus through
505
+ registered processor, formatter, encoder, destination, subscriber, and listener
506
+ entries. Use it when an extension can describe its containment surfaces without
507
+ reflecting over framework internals.
508
+ `Julewire::Testing::Chaos.raiser(error)` builds a callable that raises the
509
+ supplied error.
510
+
511
+ `Julewire::Testing::Contracts` contains shared extension assertions.
512
+ `contracts.md` owns the current helper inventory.
513
+
514
+ Contract helper tiers:
515
+
516
+ - Component contracts (`processor`, `formatter`, `destination`,
517
+ `record_draft`, record shape/source) are the documented extension test surface.
518
+ - Runtime, execution, propagation, integration, validation, truncation, bounded
519
+ transform, and scheduler contracts are integration SPI tests.
520
+ - Chaos helpers are shipped support for containment checks. They are intended
521
+ for extension/integration test suites, not app runtime code.
522
+
523
+ The runtime integration helper emits one point record inside an execution,
524
+ adds context, carry, and summary data, flushes, and asserts that destination
525
+ health is visible. Extensions provide their own output decoder and record paths,
526
+ because formatters may move Julewire fields into a different output shape.
527
+
528
+ `Julewire::Testing::Coverage` is shipped test support for Julewire
529
+ extension gems. It only requires SimpleCov when `Coverage.start!` runs with
530
+ `COVERAGE` set, so runtime users do not load coverage dependencies.
531
+
532
+ The execution-boundary helper gives integration and propagation extensions the
533
+ same probe data and lets the extension run it through its own unit of work. The
534
+ failure-containment helper verifies that extension failures do not escape
535
+ application calls and that health reports degradation.
536
+
537
+ ## Internal or Advanced
538
+
539
+ These are not ordinary application APIs:
540
+
541
+ - runtime internals
542
+ - pipeline private helpers
543
+ - remote-envelope runtime hook
544
+ - test helpers
545
+
546
+ Direct `Julewire::Core::Processing::Pipeline` construction is advanced test and extension plumbing.
547
+ Application code should use the `Julewire` facade. A pipeline is built from a
548
+ frozen configuration copy; ordinary destination extension goes through
549
+ `config.destinations.use`.
550
+
551
+ `Processing::Pipeline#emit` is the raw-input path and may emit internal Julewire error
552
+ records when normalization or processing fails. `Processing::Pipeline#emit_record` is a
553
+ trusted extension path: callers must pass a normalized `Julewire::Record`.
554
+ It reports contained failures through callbacks and health without recursively
555
+ building another internal record.
556
+
557
+ `Julewire::Core::RuntimeLocator.current=` is an advanced runtime hook for bridge
558
+ code. A bridge runtime must support the child-side facade methods it exposes and
559
+ the parent-side bridge calls it forwards: `emit_envelope`,
560
+ `emit_summary_record`, and `flush`. It is deliberately duck-typed; incompatible
561
+ runtimes fail when called, so application code should not replace it casually.
562
+
563
+ ## Remote Envelope Hook
564
+
565
+ Core exposes the bridge SPI runtime envelope hook used by bridge code. The hook
566
+ accepts detached input, context, attributes, carry, neutral data, and a scope
567
+ snapshot, then routes them through the active pipeline.
@@ -0,0 +1,104 @@
1
+ # Health Schema
2
+
3
+ `Julewire.health` is an in-process operational snapshot. It is useful for
4
+ metrics, smoke checks, and debug pages. It is not a delivery receipt.
5
+
6
+ ## Stable Fields
7
+
8
+ These fields are intended for integration dashboards and contract tests:
9
+
10
+ - `status`
11
+ - `closed`
12
+ - `generation`
13
+ - `counts`
14
+ - `last_failure`
15
+ - `last_callback_failure`
16
+ - `integrations.*.status`
17
+ - `integrations.*.counts`
18
+ - `integrations.*.last_failure`
19
+ - `process_integrations.*.status`
20
+ - `process_integrations.*.counts`
21
+ - `process_integrations.*.last_failure`
22
+ - `pipeline.configured`
23
+ - `pipeline.status`
24
+ - `pipeline.counts`
25
+ - `pipeline.last_callback_failure`
26
+ - `pipeline.last_failure`
27
+ - `destinations.*.status`
28
+ - `destinations.*.counts`
29
+ - `destinations.*.last_callback_failure`
30
+ - `destinations.*.last_failure`
31
+ - `destinations.*.last_loss`
32
+
33
+ Custom destination objects may expose their own nested health, but they own
34
+ those fields. Core direct destinations expose only core counters and loss
35
+ state.
36
+
37
+ `integrations.*` entries are runtime-local integration diagnostics. Use them
38
+ only when a failure can be honestly tied to a specific runtime.
39
+
40
+ `process_integrations.*` entries are process-owned integration diagnostics for
41
+ installs, framework subscribers, listeners, fork hooks, and other callbacks that
42
+ can fail before a record reaches a runtime. They expose only safe coordinates
43
+ such as integration name, component, action, phase, exception class, and
44
+ timestamp. A contained integration failure remains visible in `last_failure`,
45
+ but `status` can recover after the integration reports a later successful
46
+ operation.
47
+
48
+ Named runtimes have separate runtime-local `integrations`, pipeline, and
49
+ destination health, but they share `process_integrations`.
50
+
51
+ Top-level `counts` are process-runtime counters for the current runtime object.
52
+ `counts[:post_close_emits]` is scoped to the active runtime generation and resets
53
+ when a new pipeline is installed. `counts[:post_close_emits_total]` is the
54
+ runtime-lifetime total for the same rejected emits.
55
+ `counts[:invalid_record_severities]` is runtime-local; named runtimes do not
56
+ share that diagnostic count.
57
+ `pipeline.counts` and destination counters belong to the active pipeline and
58
+ reset on reconfigure. Top-level `generation` increments when configuration
59
+ installs a new pipeline.
60
+ `pipeline.counts[:processor_dropped]` counts intentional processor drops such
61
+ as sampling decisions; processor failures are counted separately under
62
+ `pipeline.counts[:processor_error]`.
63
+ Destination-local processors report the same `processor_dropped` and
64
+ `processor_error` counters under that destination's `counts`.
65
+
66
+ `status` fields describe current health for the active runtime generation.
67
+ Runtime, pipeline, and destination failure counters plus `last_failure` /
68
+ `last_loss` are historical. They remain visible after status recovers.
69
+ Destination loss or failure state recovers after a successful write or flush.
70
+ Integration state recovers after a successful integration operation. Pipeline
71
+ failure state recovers after a later emit completes without a pipeline failure.
72
+ Top-level `status` is `:ok`, `:degraded`, or `:closed`.
73
+ Pipeline `status` is `:unconfigured` before any destination is installed, then
74
+ `:ok` or `:degraded` for configured pipelines.
75
+
76
+ `destinations.*.last_loss` carries the last loss reason plus safe source,
77
+ event, severity, and timestamp metadata. It intentionally omits raw record data
78
+ and exception messages.
79
+
80
+ `destinations.*.last_failure` carries safe failure coordinates such as
81
+ exception class, phase, action, output class, and record coordinates when
82
+ available. It intentionally omits exception messages and raw output errors.
83
+
84
+ `Julewire.doctor` returns a scriptable summary built from `Julewire.health`.
85
+ It includes runtime, pipeline, destination, and integration status plus
86
+ warnings for unconfigured or degraded components.
87
+
88
+ ## Diagnostic Fields
89
+
90
+ These are useful while debugging, but they are more implementation-shaped:
91
+
92
+ - output class names
93
+ - last callback failure phase
94
+ - invalid severity details
95
+ - runtime, pipeline, and destination failure details
96
+ - callback failure counts
97
+ - detailed loss taxonomies
98
+
99
+ Diagnostic fields may move or grow as internals change. Avoid long-lived alerts
100
+ that depend on exact nested delivery details unless the integration owns the
101
+ mapping.
102
+
103
+ Health intentionally omits raw exception messages. Do not expose private error
104
+ objects or raw sink messages from app health endpoints.