julewire-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/docs/advanced-configuration.md +66 -0
- data/docs/attribute-keys.md +74 -0
- data/docs/configuration.md +327 -0
- data/docs/context-and-propagation.md +353 -0
- data/docs/contracts.md +211 -0
- data/docs/development.md +49 -0
- data/docs/extensions-and-api.md +567 -0
- data/docs/health-schema.md +104 -0
- data/docs/instrumentation-cheatsheet.md +29 -0
- data/docs/internals.md +135 -0
- data/docs/outputs-and-lifecycle.md +206 -0
- data/docs/quickstart.md +133 -0
- data/docs/record-sources.md +17 -0
- data/docs/records-and-data-policy.md +230 -0
- data/docs/security-and-wire.md +45 -0
- data/docs/tail.md +91 -0
- data/exe/julewire +6 -0
- data/julewire-core.gemspec +41 -0
- data/lib/julewire/core/cli/doctor.rb +143 -0
- data/lib/julewire/core/cli/line_helpers.rb +77 -0
- data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
- data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
- data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
- data/lib/julewire/core/cli/log_formats.rb +123 -0
- data/lib/julewire/core/cli/tail.rb +153 -0
- data/lib/julewire/core/cli/transcode.rb +105 -0
- data/lib/julewire/core/cli.rb +73 -0
- data/lib/julewire/core/configuration.rb +99 -0
- data/lib/julewire/core/context_store.rb +384 -0
- data/lib/julewire/core/destinations/chaos_output.rb +91 -0
- data/lib/julewire/core/destinations/collection.rb +177 -0
- data/lib/julewire/core/destinations/definition.rb +125 -0
- data/lib/julewire/core/destinations/destination.rb +268 -0
- data/lib/julewire/core/destinations/registry.rb +81 -0
- data/lib/julewire/core/destinations/sink.rb +35 -0
- data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
- data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
- data/lib/julewire/core/destinations/write_step.rb +119 -0
- data/lib/julewire/core/destinations.rb +33 -0
- data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
- data/lib/julewire/core/diagnostics/doctor.rb +114 -0
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
- data/lib/julewire/core/diagnostics/health.rb +144 -0
- data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
- data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
- data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
- data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
- data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
- data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
- data/lib/julewire/core/diagnostics/tail.rb +168 -0
- data/lib/julewire/core/diagnostics.rb +8 -0
- data/lib/julewire/core/error.rb +7 -0
- data/lib/julewire/core/execution/boundary.rb +106 -0
- data/lib/julewire/core/execution/handle.rb +77 -0
- data/lib/julewire/core/execution/lineage.rb +192 -0
- data/lib/julewire/core/execution/measurement_handle.rb +28 -0
- data/lib/julewire/core/execution/no_current_error.rb +9 -0
- data/lib/julewire/core/execution/scope.rb +246 -0
- data/lib/julewire/core/execution/scope_fields.rb +76 -0
- data/lib/julewire/core/execution/scope_identity.rb +71 -0
- data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
- data/lib/julewire/core/execution/summary_state.rb +206 -0
- data/lib/julewire/core/execution/view.rb +56 -0
- data/lib/julewire/core/facade_methods.rb +181 -0
- data/lib/julewire/core/fields/attribute_keys.rb +54 -0
- data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
- data/lib/julewire/core/fields/bags.rb +123 -0
- data/lib/julewire/core/fields/carry_proxy.rb +22 -0
- data/lib/julewire/core/fields/context_proxy.rb +11 -0
- data/lib/julewire/core/fields/field_set.rb +78 -0
- data/lib/julewire/core/fields/field_stack.rb +269 -0
- data/lib/julewire/core/fields/internal/deletion.rb +68 -0
- data/lib/julewire/core/fields/internal.rb +87 -0
- data/lib/julewire/core/fields/lookup.rb +35 -0
- data/lib/julewire/core/fields/section_proxy.rb +88 -0
- data/lib/julewire/core/fields/stack_set.rb +69 -0
- data/lib/julewire/core/fields/static_labels.rb +43 -0
- data/lib/julewire/core/fields/summary_proxy.rb +62 -0
- data/lib/julewire/core/integration/configurable.rb +52 -0
- data/lib/julewire/core/integration/destination_health.rb +43 -0
- data/lib/julewire/core/integration/event_subscriber.rb +62 -0
- data/lib/julewire/core/integration/facade.rb +131 -0
- data/lib/julewire/core/integration/fork_hooks.rb +79 -0
- data/lib/julewire/core/integration/health.rb +41 -0
- data/lib/julewire/core/integration/ivar_state.rb +38 -0
- data/lib/julewire/core/integration/lifecycle.rb +22 -0
- data/lib/julewire/core/integration/scoped.rb +34 -0
- data/lib/julewire/core/integration/settings.rb +92 -0
- data/lib/julewire/core/integration/subscriber_install.rb +39 -0
- data/lib/julewire/core/integration/subscription.rb +29 -0
- data/lib/julewire/core/integration/values.rb +192 -0
- data/lib/julewire/core/lifecycle_error.rb +7 -0
- data/lib/julewire/core/local_storage.rb +91 -0
- data/lib/julewire/core/processing/level_threshold.rb +53 -0
- data/lib/julewire/core/processing/match.rb +74 -0
- data/lib/julewire/core/processing/pipeline.rb +360 -0
- data/lib/julewire/core/processing/processor_chain.rb +69 -0
- data/lib/julewire/core/processing/processor_registry.rb +115 -0
- data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
- data/lib/julewire/core/processing/record_field_transform.rb +124 -0
- data/lib/julewire/core/processing/sampling.rb +109 -0
- data/lib/julewire/core/processing.rb +41 -0
- data/lib/julewire/core/propagation/carrier.rb +93 -0
- data/lib/julewire/core/propagation.rb +50 -0
- data/lib/julewire/core/records/console_formatter.rb +24 -0
- data/lib/julewire/core/records/deconstruct.rb +19 -0
- data/lib/julewire/core/records/display_message.rb +166 -0
- data/lib/julewire/core/records/draft.rb +576 -0
- data/lib/julewire/core/records/formatter.rb +14 -0
- data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
- data/lib/julewire/core/records/metadata.rb +23 -0
- data/lib/julewire/core/records/public_projection.rb +51 -0
- data/lib/julewire/core/records/raw_input.rb +41 -0
- data/lib/julewire/core/records/record.rb +175 -0
- data/lib/julewire/core/records/severity.rb +44 -0
- data/lib/julewire/core/runtime.rb +515 -0
- data/lib/julewire/core/runtime_locator.rb +20 -0
- data/lib/julewire/core/runtime_registry.rb +48 -0
- data/lib/julewire/core/runtime_state.rb +39 -0
- data/lib/julewire/core/scheduling/deadline.rb +24 -0
- data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
- data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
- data/lib/julewire/core/sentinel.rb +18 -0
- data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
- data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
- data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
- data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
- data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
- data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
- data/lib/julewire/core/serialization/exception_shape.rb +88 -0
- data/lib/julewire/core/serialization/json_encoder.rb +69 -0
- data/lib/julewire/core/serialization/serializer.rb +233 -0
- data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
- data/lib/julewire/core/serialization/text_encoder.rb +147 -0
- data/lib/julewire/core/serialization/value_copy.rb +209 -0
- data/lib/julewire/core/serialization/value_traversal.rb +150 -0
- data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
- data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
- data/lib/julewire/core/testing/chaos/destination.rb +55 -0
- data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
- data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
- data/lib/julewire/core/testing/chaos.rb +80 -0
- data/lib/julewire/core/testing/contracts/component.rb +162 -0
- data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
- data/lib/julewire/core/testing/contracts/integration.rb +166 -0
- data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
- data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
- data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
- data/lib/julewire/core/testing/contracts/wire.rb +60 -0
- data/lib/julewire/core/testing/contracts.rb +24 -0
- data/lib/julewire/core/testing/coverage.rb +58 -0
- data/lib/julewire/core/testing/test_reports.rb +78 -0
- data/lib/julewire/core/testing.rb +122 -0
- data/lib/julewire/core/validation.rb +69 -0
- data/lib/julewire/core/version.rb +7 -0
- data/lib/julewire/core.rb +80 -0
- data/lib/julewire/error.rb +5 -0
- data/lib/julewire-core.rb +3 -0
- metadata +237 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Context and Propagation
|
|
2
|
+
|
|
3
|
+
Context is ambient application data for the current execution/fiber. Carry is
|
|
4
|
+
small propagated integration/correlation data. Attributes are emitted
|
|
5
|
+
record-local data for integrations and formatters, but are not propagated.
|
|
6
|
+
Summary is final-only data for the current execution.
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
Julewire.context.add(request_id: "req-1")
|
|
10
|
+
Julewire.carry.add(http: { request_headers: { traceparent: "..." } })
|
|
11
|
+
Julewire.attributes.add(my_app: { shard: "a" })
|
|
12
|
+
|
|
13
|
+
Julewire.with_execution(type: :job, id: "job-1", labels: { worker: "billing" }) do
|
|
14
|
+
Julewire.context.add(worker: "background")
|
|
15
|
+
Julewire.attributes.add(runtime: { queue: "default" })
|
|
16
|
+
Julewire.summary.increment(:records_seen)
|
|
17
|
+
Julewire.measure(:upstream) { call_upstream }
|
|
18
|
+
Julewire.summary.append(:warnings, "slow upstream")
|
|
19
|
+
|
|
20
|
+
Julewire.emit(message: "processed")
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Context appears on point logs and summaries. Carry is available to processors,
|
|
25
|
+
formatters, and propagation envelopes, but the default formatter does not write
|
|
26
|
+
it to JSON output. Integration-specific attribute namespaces appear on emitted
|
|
27
|
+
records and summaries. Neutral fields coordinate formatters through the record's
|
|
28
|
+
`neutral` section and are stripped by the default formatter. Use
|
|
29
|
+
`Julewire.summary.add_attributes` to add attributes to a final summary. Summary
|
|
30
|
+
payload data appears only on the final summary. Attributes and summary data are
|
|
31
|
+
never propagated.
|
|
32
|
+
|
|
33
|
+
Nested executions inherit current attributes and neutral fields by default so
|
|
34
|
+
in-request child operations keep formatter coordination fields. Integrations
|
|
35
|
+
that restore a remote unit of work, such as a background job, can start the
|
|
36
|
+
execution with `inherit_attributes: false` and provide only that unit's fields.
|
|
37
|
+
Execution-level `labels:` apply to point records and the final summary for that
|
|
38
|
+
execution. Per-record labels can still override them.
|
|
39
|
+
|
|
40
|
+
## Merge Semantics
|
|
41
|
+
|
|
42
|
+
Nested `attributes` are deep-merged. This lets integrations add their own
|
|
43
|
+
namespaces without replacing each other.
|
|
44
|
+
|
|
45
|
+
`context`, `carry`, and `labels` merge at the top level on a record.
|
|
46
|
+
When context or carry overlays are stacked with `with`, an overlapping top-level
|
|
47
|
+
key replaces that whole value for lookups, snapshots, and emitted records.
|
|
48
|
+
|
|
49
|
+
Summary helpers have their own merge rules: `summary.add_attributes` deep-merges
|
|
50
|
+
attributes, while `summary.add` stores summary payload fields by key.
|
|
51
|
+
|
|
52
|
+
If you accept the block argument from `with_execution`, it is a read-only
|
|
53
|
+
execution view. Scalar execution fields are captured when the view is created;
|
|
54
|
+
field sections such as context, carry, and summary are materialized lazily on
|
|
55
|
+
first read. Mutating the live execution goes through `Julewire.context`,
|
|
56
|
+
`Julewire.carry`, `Julewire.attributes`, and `Julewire.summary`.
|
|
57
|
+
|
|
58
|
+
Deferred execution handles are not thread-safe mutation surfaces. If an
|
|
59
|
+
integration shares a handle across threads, it must serialize calls that add or
|
|
60
|
+
overlay context, carry, attributes, labels, or summary fields.
|
|
61
|
+
|
|
62
|
+
Ambient context is fiber-local. Applications that reuse fibers outside a
|
|
63
|
+
Julewire execution scope should overwrite or clear context before unrelated work
|
|
64
|
+
uses the same fiber.
|
|
65
|
+
|
|
66
|
+
## Context Helpers
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Julewire.context.add(user_id: "user-1")
|
|
70
|
+
|
|
71
|
+
Julewire.context.with(order_id: "order-1") do
|
|
72
|
+
Julewire.emit(message: "inside order")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Julewire.context[:user_id]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Public helpers accept string or symbol keys and normalize strings to symbols
|
|
79
|
+
when data enters core. After that boundary, use symbol keys.
|
|
80
|
+
|
|
81
|
+
Runtime helpers are best effort. Non-hash positional values are captured under
|
|
82
|
+
`:value` instead of raising from application code.
|
|
83
|
+
|
|
84
|
+
## Carry Helpers
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Julewire.carry.add(http: { request_headers: { traceparent: "..." } })
|
|
88
|
+
|
|
89
|
+
Julewire.carry.with(worker: { queue: "critical" }) do
|
|
90
|
+
Julewire.emit(message: "inside worker")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Julewire.carry.delete(:http, :request_headers, :authorization)
|
|
94
|
+
|
|
95
|
+
Julewire.carry.without(:http, :request_headers, :traceparent) do
|
|
96
|
+
Julewire.emit(message: "without trace carry")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Julewire.carry[:http]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Carry is for small facts that integrations and formatters need on every record
|
|
103
|
+
and across propagation boundaries. It is not application log content, a privacy
|
|
104
|
+
boundary, or summary storage. Avoid large blobs unless a processor policy will
|
|
105
|
+
handle them before formatting.
|
|
106
|
+
|
|
107
|
+
`carry.delete` is persistent until the same path is added again with
|
|
108
|
+
`carry.add`. That delete masks scoped `carry.with` overlays too. Use
|
|
109
|
+
`carry.without` for a temporary block-only mask.
|
|
110
|
+
|
|
111
|
+
## Summary Helpers
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
Julewire.summary.add(plan: "pro")
|
|
115
|
+
Julewire.summary.increment(:upstream_calls)
|
|
116
|
+
Julewire.measure(:upstream) { call_upstream }
|
|
117
|
+
Julewire.summary.increment_attribute(:active_job, :continuation_steps_completed)
|
|
118
|
+
Julewire.summary.append(:warnings, "slow upstream")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`Julewire.summary.active?` reports whether the current fiber has a live
|
|
122
|
+
execution scope. `add`, `increment`, and `append` are strict: they raise
|
|
123
|
+
`Julewire::Core::Execution::NoCurrentError` outside `with_execution` instead of
|
|
124
|
+
silently dropping summary data.
|
|
125
|
+
|
|
126
|
+
`append` is non-raising for normal summary values:
|
|
127
|
+
|
|
128
|
+
- missing key becomes `[value]`
|
|
129
|
+
- array gets appended
|
|
130
|
+
- scalar becomes `[existing, value]`
|
|
131
|
+
|
|
132
|
+
`increment` adds numeric payload values normally. `increment_attribute`
|
|
133
|
+
increments nested summary attributes by path. If a key already holds
|
|
134
|
+
non-numeric summary data, core preserves the existing value and appends the
|
|
135
|
+
increment using the same array conversion rule.
|
|
136
|
+
|
|
137
|
+
`Julewire.measure(:upstream) { ... }` returns the block value, increments
|
|
138
|
+
`payload.upstream_count`, and accumulates elapsed milliseconds in
|
|
139
|
+
`metrics.upstream_duration_ms`. It records the timing even when the measured
|
|
140
|
+
block raises.
|
|
141
|
+
|
|
142
|
+
Use `Julewire.measure_start(:upstream)` when the measured work cannot fit in a
|
|
143
|
+
block. It returns an idempotent handle; call `finish` once the work is done.
|
|
144
|
+
|
|
145
|
+
Logging should not crash the app, but core also should not silently throw away
|
|
146
|
+
surprising data.
|
|
147
|
+
|
|
148
|
+
Summary records default to `source: "julewire"` and
|
|
149
|
+
`event: "#{type}.completed"`. Caller code can override that shape:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
Julewire.with_execution(
|
|
153
|
+
type: :operation,
|
|
154
|
+
summary_source: "app",
|
|
155
|
+
summary_event: "operation.completed"
|
|
156
|
+
) do
|
|
157
|
+
Julewire.summary.add(status: 200)
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Error summaries emit with `severity: :error` unless the error path supplies an
|
|
162
|
+
explicit severity. Successful summaries use `summary_severity:` when set,
|
|
163
|
+
otherwise they use normal severity defaults.
|
|
164
|
+
|
|
165
|
+
## Current Execution
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
execution = Julewire.current_execution
|
|
169
|
+
execution.context_hash
|
|
170
|
+
execution.carry_hash
|
|
171
|
+
execution.summary_hash
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`current_execution?` is the cheap predicate. `current_execution` returns a
|
|
175
|
+
read-only execution view with lazily materialized field sections. Mutating the
|
|
176
|
+
live execution goes through `Julewire.context`, `Julewire.carry`,
|
|
177
|
+
`Julewire.attributes`, and `Julewire.summary`.
|
|
178
|
+
|
|
179
|
+
## Deferred Execution Handles
|
|
180
|
+
|
|
181
|
+
Integrations that start work in one place and finish it later can use an
|
|
182
|
+
explicit handle:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
handle = Julewire.start_execution(type: :request, id: "req-1")
|
|
186
|
+
|
|
187
|
+
status, headers, body = handle.run do
|
|
188
|
+
app.call(env)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
body = ContextRestoringBody.new(body, handle) do
|
|
192
|
+
handle.finish(reason: :closed, fields: { status: status })
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`finish` is idempotent. The first finish emits the summary; later close hooks
|
|
197
|
+
or timeout callbacks are no-ops. `run` restores the handle context and finishes
|
|
198
|
+
on exceptions, but successful deferred executions must call `finish` explicitly.
|
|
199
|
+
Finished summaries include a visible `julewire.completion` attribute with the
|
|
200
|
+
first finish reason.
|
|
201
|
+
Timeout summaries should say they timed out:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
handle.finish(reason: :timeout, fields: { completion_timeout_ms: 30_000 })
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Use `handle.with_context` around late body iteration, callbacks, or stream
|
|
208
|
+
cleanup when those operations should inherit the original execution context.
|
|
209
|
+
This is an integration primitive; ordinary application code should prefer
|
|
210
|
+
`Julewire.with_execution`.
|
|
211
|
+
|
|
212
|
+
## Propagation Envelopes
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
envelope = Julewire::Core::Propagation.capture
|
|
216
|
+
|
|
217
|
+
Julewire::Core::Propagation.restore(envelope) do
|
|
218
|
+
Julewire.emit(message: "restored")
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Propagation restores context, carry, and execution metadata into direct point
|
|
223
|
+
records and into the next execution scope. It does not propagate summary data.
|
|
224
|
+
Execution metadata is restored as ordinary execution fields. Use
|
|
225
|
+
`Julewire.with_execution(..., fields: { trace_id: "..." })` for custom
|
|
226
|
+
execution fields at the public API boundary.
|
|
227
|
+
|
|
228
|
+
Propagation serializes values through the core serializer. That means values
|
|
229
|
+
cross a log-safe boundary, not an object-identity boundary.
|
|
230
|
+
|
|
231
|
+
Core does not redact propagation envelopes. Do not put secrets in context or
|
|
232
|
+
carry unless your app or processor policy handles them.
|
|
233
|
+
|
|
234
|
+
By default, a new local execution started after restore gets a new local
|
|
235
|
+
lineage. At trusted boundaries where the remote execution should be the parent,
|
|
236
|
+
use `link_executions: true`:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
Julewire::Core::Propagation.restore(envelope, link_executions: true) do
|
|
240
|
+
Julewire.with_execution(type: :job, id: "job-1") { perform_job }
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Restoring an envelope is an explicit trust decision. Core validates shape, and
|
|
245
|
+
outbound injection can enforce a byte limit, but core does not authenticate who
|
|
246
|
+
wrote the carrier. At untrusted boundaries, extract only the carrier fields you
|
|
247
|
+
intentionally accept, apply any application spoofing/filter policy first, then
|
|
248
|
+
call `Carrier.restore` on that filtered carrier map.
|
|
249
|
+
|
|
250
|
+
## Flat Carriers
|
|
251
|
+
|
|
252
|
+
Adapters that cross a serialized boundary can put the propagation envelope into
|
|
253
|
+
a flat string carrier:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
headers = Julewire::Core::Propagation::Carrier.inject({})
|
|
257
|
+
|
|
258
|
+
Julewire::Core::Propagation::Carrier.restore(headers) do
|
|
259
|
+
Julewire.emit(message: "restored from headers")
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
For inbound request headers, keep restoration explicit:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
trusted = filter_trusted_headers(request.headers)
|
|
267
|
+
Julewire::Core::Propagation::Carrier.restore(trusted) do
|
|
268
|
+
handle_request
|
|
269
|
+
end
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Use `max_bytes:` when the carrier target has practical size constraints:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
headers = Julewire::Core::Propagation::Carrier.inject({}, max_bytes: 8 * 1024)
|
|
276
|
+
# => nil when the serialized carrier is too large; the carrier key is removed
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The default carrier key is `"julewire"`. Carriers are intentionally
|
|
280
|
+
provider-neutral: core stores only the Julewire propagation envelope. External
|
|
281
|
+
header conventions stay outside core.
|
|
282
|
+
|
|
283
|
+
Carriers are transport metadata. They may become visible if an application logs
|
|
284
|
+
full carrier maps. Keep integrations responsible for deciding which external
|
|
285
|
+
metadata to mirror into carry, and keep normal processors in the destination
|
|
286
|
+
path for emitted records.
|
|
287
|
+
|
|
288
|
+
## Thread and Fiber Wrappers
|
|
289
|
+
|
|
290
|
+
Raw threads and raw fibers do not magically inherit Julewire context. Use the
|
|
291
|
+
wrappers when you want propagation:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
Julewire.context.add(request_id: "req-1")
|
|
295
|
+
|
|
296
|
+
thread = Julewire.thread do
|
|
297
|
+
Julewire.emit(message: "from thread")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
thread.join
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
fiber = Julewire.fiber do
|
|
305
|
+
Julewire.emit(message: "from fiber")
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
fiber.resume
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Each wrapper captures the current propagation envelope once and restores it
|
|
312
|
+
around the block.
|
|
313
|
+
|
|
314
|
+
Thread and fiber wrappers use a local snapshot, not the serialized propagation
|
|
315
|
+
envelope. Ruby values keep their local shape across those same-process
|
|
316
|
+
boundaries. `Propagation.capture` remains the serialized/log-safe envelope for
|
|
317
|
+
cross-runtime and external transport boundaries.
|
|
318
|
+
|
|
319
|
+
Restored execution metadata is available to direct point logs when no live
|
|
320
|
+
execution scope is already active. A live scope wins over the restored snapshot.
|
|
321
|
+
The snapshot is also inherited by the next `with_execution` opened inside the
|
|
322
|
+
wrapper. It is not a live execution scope by itself, so `Julewire.summary` is
|
|
323
|
+
unavailable unless the wrapped code opens a new `with_execution`.
|
|
324
|
+
|
|
325
|
+
## Remote Runtime Hooks
|
|
326
|
+
|
|
327
|
+
Core keeps a small remote-envelope hook so bridge code can send normalized
|
|
328
|
+
input, context, and scope snapshots back to another runtime. The receiving
|
|
329
|
+
runtime's configuration, processors, level, destinations, labels, and outputs
|
|
330
|
+
remain authoritative.
|
|
331
|
+
|
|
332
|
+
Inside a remote runtime, `Julewire.reset!` is local to that runtime storage. It
|
|
333
|
+
does not reset another runtime's configuration, destinations, or health
|
|
334
|
+
counters.
|
|
335
|
+
|
|
336
|
+
## Reset Semantics
|
|
337
|
+
|
|
338
|
+
`Julewire.reset!` clears:
|
|
339
|
+
|
|
340
|
+
- active configuration
|
|
341
|
+
- current fiber context store
|
|
342
|
+
- the old active pipeline/output lifecycle
|
|
343
|
+
- live runtime post-close drop and callback-failure state
|
|
344
|
+
|
|
345
|
+
`health[:counts]` is different: it is monotonic for the current runtime object
|
|
346
|
+
and survives `reset!`. Use it for process-lifetime runtime lifecycle scrapes.
|
|
347
|
+
|
|
348
|
+
Other long-lived threads/fibers keep their own context stores until they reset
|
|
349
|
+
or exit. Long-lived workers should reset or replace per-execution context on
|
|
350
|
+
each unit of work.
|
|
351
|
+
|
|
352
|
+
Main runtime context uses storage on the current Ruby fiber. Non-main ractors
|
|
353
|
+
use thread-local storage because bridge runtimes are ractor-local.
|
data/docs/contracts.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Contracts
|
|
2
|
+
|
|
3
|
+
Core has five contract levels. Keep changes honest by deciding which level a
|
|
4
|
+
behavior belongs to before preserving or breaking it.
|
|
5
|
+
|
|
6
|
+
## Public application API
|
|
7
|
+
|
|
8
|
+
These APIs are for application code and scripts:
|
|
9
|
+
|
|
10
|
+
- Runtime configuration and lifecycle: `Julewire.configure`,
|
|
11
|
+
`Julewire.config`, `Julewire.reset!`, `Julewire.flush`, `Julewire.close`,
|
|
12
|
+
`Julewire.after_fork!`, and `Julewire.health`.
|
|
13
|
+
- `Julewire.runtime` for explicit secondary pipelines with independent
|
|
14
|
+
configuration, destinations, processors, health, and lifecycle.
|
|
15
|
+
- `Julewire.labels` for process labels shared by the active runtime.
|
|
16
|
+
- `Julewire.emit` and severity helpers: `Julewire.debug`, `Julewire.info`,
|
|
17
|
+
`Julewire.warn`, `Julewire.error`, `Julewire.fatal`, and
|
|
18
|
+
`Julewire.unknown`.
|
|
19
|
+
- Execution, context, attributes, carry, and summary facades:
|
|
20
|
+
`Julewire.with_execution`, `Julewire.context`, `Julewire.attributes`,
|
|
21
|
+
`Julewire.carry`, `Julewire.summary`, `Julewire.measure`,
|
|
22
|
+
`Julewire.measure_start`, `Julewire.start_execution`, and
|
|
23
|
+
`Julewire.current_execution` / `Julewire.current_execution?`.
|
|
24
|
+
- Local diagnostics and development helpers: `Julewire.doctor`,
|
|
25
|
+
`Julewire.tail`, `Julewire.dev!`, `Julewire.punk!`, and
|
|
26
|
+
`Julewire.observe_self!`.
|
|
27
|
+
- Context propagation wrappers: `Julewire.thread` and `Julewire.fiber`.
|
|
28
|
+
- Destination configuration through `config.destinations.use`.
|
|
29
|
+
- `Julewire::Core::Propagation::Carrier` and `Julewire::Core::Propagation` for explicit
|
|
30
|
+
cross-boundary propagation.
|
|
31
|
+
|
|
32
|
+
Application-facing logging calls contain Julewire `StandardError` failures.
|
|
33
|
+
They do not hide application exceptions.
|
|
34
|
+
|
|
35
|
+
## Extension contract
|
|
36
|
+
|
|
37
|
+
These APIs are intentionally available to integration code, but they are not
|
|
38
|
+
general application surface:
|
|
39
|
+
|
|
40
|
+
- `Julewire::RecordDraft` as the processor mutation surface and
|
|
41
|
+
raw-input construction surface used by core and integrations before records
|
|
42
|
+
become immutable. Prefer `transform_field!`, `transform_section!`, and
|
|
43
|
+
`transform_record!` for replacement transforms.
|
|
44
|
+
- Immutable `Julewire::Record` instances for destinations and formatters.
|
|
45
|
+
- `Julewire::Record::REQUIRED_KEYS` and `HASH_SECTIONS` for processors that
|
|
46
|
+
need to preserve core record shape.
|
|
47
|
+
- `record.lineage` for explicit execution lineage access. Normalized record
|
|
48
|
+
data keeps only cheap execution identity; full ancestors are read through the
|
|
49
|
+
lineage accessor.
|
|
50
|
+
- `Julewire::Core::Records::PublicProjection`, the public record projection
|
|
51
|
+
used by the default formatter and serializer.
|
|
52
|
+
- `Julewire::Core::Records::PublicProjection::INTERNAL_EXECUTION_KEYS` for provider
|
|
53
|
+
formatters that expose a public execution payload.
|
|
54
|
+
- Encoder objects that respond to `call(payload)` and return strings for direct
|
|
55
|
+
destinations.
|
|
56
|
+
- The five-method destination duck type: `name`, `emit`, `flush`, `close`, and
|
|
57
|
+
`health`.
|
|
58
|
+
- `Julewire::Core::Destinations.normalize_name` for destination-name
|
|
59
|
+
normalization shared by destination adapters.
|
|
60
|
+
- `Julewire::TailSampling` as a destination wrapper for execution-level tail
|
|
61
|
+
sampling.
|
|
62
|
+
- `Julewire::Testing::CaptureDestination`, `Julewire::Testing::NullOutput`,
|
|
63
|
+
`Julewire::Testing.capture`, `Julewire::Testing.configure_capture_destination`,
|
|
64
|
+
`Julewire::Testing::Contracts`, `Julewire::Testing::Chaos`, and
|
|
65
|
+
`Julewire::Testing::Coverage` as extension test support.
|
|
66
|
+
- `RuntimeLocator.current.emit_without_level` for host-process integrations
|
|
67
|
+
that already apply their framework's level gate.
|
|
68
|
+
|
|
69
|
+
Extensions should consume these contracts rather than reaching into pipeline,
|
|
70
|
+
runtime, storage, or destination internals.
|
|
71
|
+
|
|
72
|
+
Testing support is shipped for integration authors, but helper names may still
|
|
73
|
+
change when the ecosystem cleanup demands it.
|
|
74
|
+
|
|
75
|
+
`Julewire::Testing::Contracts` currently ships these shared assertions for
|
|
76
|
+
extension and integration gems:
|
|
77
|
+
|
|
78
|
+
- `assert_julewire_bounded_transform_spi_contract`
|
|
79
|
+
- `assert_julewire_deadline_scheduler_spi_contract`
|
|
80
|
+
- `assert_julewire_destination_contract`
|
|
81
|
+
- `assert_julewire_execution_boundary_contract`
|
|
82
|
+
- `assert_julewire_failure_containment_contract`
|
|
83
|
+
- `assert_julewire_formatter_contract`
|
|
84
|
+
- `assert_julewire_integration_failure_contract`
|
|
85
|
+
- `assert_julewire_integration_health_contract`
|
|
86
|
+
- `assert_julewire_integration_ivar_state_contract`
|
|
87
|
+
- `assert_julewire_integration_payload_contract`
|
|
88
|
+
- `assert_julewire_integration_spi_contract`
|
|
89
|
+
- `assert_julewire_integration_timestamp_contract`
|
|
90
|
+
- `assert_julewire_integration_value_contract`
|
|
91
|
+
- `assert_julewire_processor_contract`
|
|
92
|
+
- `assert_julewire_propagation_contract`
|
|
93
|
+
- `assert_julewire_record_draft_transform_contract`
|
|
94
|
+
- `assert_julewire_record_shape_contract`
|
|
95
|
+
- `assert_julewire_record_source_contract`
|
|
96
|
+
- `assert_julewire_runtime_integration_contract`
|
|
97
|
+
- `assert_julewire_truncation_marker_spi_contract`
|
|
98
|
+
- `assert_julewire_validation_spi_contract`
|
|
99
|
+
|
|
100
|
+
`Julewire::Testing::Chaos` currently ships containment helpers for extension
|
|
101
|
+
test suites:
|
|
102
|
+
|
|
103
|
+
- `assert_contained`
|
|
104
|
+
- `assert_core_runtime_containment`
|
|
105
|
+
- `assert_destination_chaos_contract`
|
|
106
|
+
- `assert_discovered_chaos_contracts`
|
|
107
|
+
- `assert_emitter_chaos_contract`
|
|
108
|
+
- `catalog`
|
|
109
|
+
- `raiser`
|
|
110
|
+
|
|
111
|
+
## Integration SPI
|
|
112
|
+
|
|
113
|
+
These APIs are for framework, provider, and transport integrations that need a
|
|
114
|
+
little more structure than application code. They are public to integrations,
|
|
115
|
+
but they are not intended as general application API:
|
|
116
|
+
|
|
117
|
+
- `Julewire::Core::Integration::Health` for contained process-level
|
|
118
|
+
integration health and scoped health wrappers.
|
|
119
|
+
- `Julewire::Core::Integration::Facade` for integration-owned emits,
|
|
120
|
+
execution boundaries, field overlays, and summary enrichment.
|
|
121
|
+
- `Julewire::Core::Integration::Values::Read` for hash/object value reads
|
|
122
|
+
including `value`, `hash_value`, `nested_value`, `path_value`,
|
|
123
|
+
`first_value`, and `blank?`.
|
|
124
|
+
- `Julewire::Core::Integration::Values::Shape` for normalized timestamps, payload
|
|
125
|
+
normalization, field appends, and source-location shaping.
|
|
126
|
+
- `Julewire::Core::Integration::Lifecycle` for optional require containment and
|
|
127
|
+
process-local `after_fork` hooks.
|
|
128
|
+
- `Julewire::Core::Integration` helper classes/modules for one-time ivar state,
|
|
129
|
+
subscriber install helpers, event-subscriber health wrappers, config settings
|
|
130
|
+
helpers, and subscription handles.
|
|
131
|
+
- `Julewire::Core::Integration::DestinationHealth` for destination-style
|
|
132
|
+
integrations that need Julewire-shaped counters, failure snapshots, and loss
|
|
133
|
+
snapshots without exposing core's internal health cells.
|
|
134
|
+
- `Julewire::Core::Destinations::WriteStep` for destination-style integrations
|
|
135
|
+
that reuse core's format, encode, bound, write, and counter sequence while
|
|
136
|
+
keeping their own lifecycle and failure policy.
|
|
137
|
+
- `Julewire::Core::CLI::LogFormats` for provider-owned CLI file-tail decoding
|
|
138
|
+
and transcoding formats.
|
|
139
|
+
- `Julewire::Core::Processing.register` for integration-owned processor kinds,
|
|
140
|
+
and `Julewire::Core::Processing::RecordFieldTransform` for processors that
|
|
141
|
+
need core's normalized record-section walk while keeping their own filtering
|
|
142
|
+
policy.
|
|
143
|
+
- `Julewire::Core::Fields::AttributeKeys` for provider-neutral formatter
|
|
144
|
+
coordination attributes.
|
|
145
|
+
- `Julewire::Core::Fields::Bags` for record field-bag capabilities such as
|
|
146
|
+
emitted output sections, transform containers, propagation, and delete-path
|
|
147
|
+
support.
|
|
148
|
+
- `Julewire::Core.sentinel(:name)` for readable, frozen identity markers when
|
|
149
|
+
an integration needs a private empty or missing-value sentinel.
|
|
150
|
+
- `Julewire::Core.deep_compact_empty` for core-compatible empty-field pruning.
|
|
151
|
+
- `Julewire::Core::Scheduling::DeadlineScheduler` for integration-local timeout callbacks
|
|
152
|
+
that need Julewire's fork-reset behavior.
|
|
153
|
+
- `Julewire::Core::Scheduling::SharedScheduler` for process-wide main-ractor
|
|
154
|
+
timeouts shared by integrations that should not own separate scheduler
|
|
155
|
+
threads.
|
|
156
|
+
- `Julewire::Core::Validation` for shared option and limit validation.
|
|
157
|
+
- `Julewire::Core::Serialization::BoundedTransform` for processors and integrations that need
|
|
158
|
+
core-compatible bounded traversal before core serializes the result.
|
|
159
|
+
- `Julewire::Core::Diagnostics::CallbackNotifier` and
|
|
160
|
+
`Julewire::Core::Diagnostics::FailureSnapshot` for destination-style
|
|
161
|
+
integrations that need core-compatible callback and health failure shape.
|
|
162
|
+
- `Julewire::Core::Records::DisplayMessage` and
|
|
163
|
+
`Julewire::Core::Records::Metadata` for formatters and destination-style
|
|
164
|
+
integrations that need the same display-message fallback or safe record
|
|
165
|
+
coordinates as core.
|
|
166
|
+
- `Julewire::Core::Records::Severity` for integrations that normalize
|
|
167
|
+
framework-native levels before handing records to core.
|
|
168
|
+
- `Julewire::Serializer.truncation_metadata` and serializer constants that
|
|
169
|
+
define core-compatible truncation markers when an integration must shape
|
|
170
|
+
bounded data before core serializes it.
|
|
171
|
+
Integrations may use this SPI when they need to match core behavior exactly.
|
|
172
|
+
New SPI use should be covered by either core contract tests or integration
|
|
173
|
+
tests that would fail on incompatible core changes.
|
|
174
|
+
|
|
175
|
+
## Bridge SPI
|
|
176
|
+
|
|
177
|
+
These APIs are for runtime bridges that need to run Julewire facade calls in a
|
|
178
|
+
different Ruby isolation boundary:
|
|
179
|
+
|
|
180
|
+
- `Julewire::Core::RuntimeLocator.current=` to install the bridge runtime.
|
|
181
|
+
- `Julewire::Core::UNSET`, `Julewire::Core.emit_input`, and
|
|
182
|
+
`Julewire::Core::Records::LazyEmitInput` to mirror facade emit semantics
|
|
183
|
+
across an isolation boundary.
|
|
184
|
+
- `Julewire::Core::Execution::Boundary` for shared execution boundary behavior.
|
|
185
|
+
- `Julewire::Core::ContextStore.current`,
|
|
186
|
+
`Julewire::Core::Fields::ContextProxy`,
|
|
187
|
+
`Julewire::Core::Fields::AttributesProxy`,
|
|
188
|
+
`Julewire::Core::Fields::CarryProxy`, and
|
|
189
|
+
`Julewire::Core::Fields::SummaryProxy` for bridge-local field bags.
|
|
190
|
+
- `Julewire::Core::Execution::ScopeSnapshot` for detached execution scope transfer.
|
|
191
|
+
- Parent-runtime hooks: `emit_envelope`, `emit_summary_record`, and `flush`.
|
|
192
|
+
`emit_envelope` accepts detached input, context, carry, attributes, neutral,
|
|
193
|
+
scope snapshot, and an `enforce_level:` flag.
|
|
194
|
+
|
|
195
|
+
Bridge runtimes may expose `emit_without_level` when integration code inside
|
|
196
|
+
the bridge has already applied its own level gate. Parent runtime labels,
|
|
197
|
+
processors, destinations, and outputs remain parent-owned.
|
|
198
|
+
|
|
199
|
+
## Internal implementation
|
|
200
|
+
|
|
201
|
+
These details are intentionally private and can move freely:
|
|
202
|
+
|
|
203
|
+
- `Runtime`, `Processing::Pipeline`, destination-set, and lifecycle state layout.
|
|
204
|
+
- Field stacks, local storage, and ractor lookup internals.
|
|
205
|
+
- Counter storage, callback-notifier plumbing, and health implementation
|
|
206
|
+
mechanics.
|
|
207
|
+
- Serializer traversal internals and value-copy helpers.
|
|
208
|
+
|
|
209
|
+
Tests may characterize internal behavior only when it protects a public or
|
|
210
|
+
extension contract, such as exception fidelity, cycle safety, fork safety, or
|
|
211
|
+
wire-shape stability.
|
data/docs/development.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Install dependencies:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bundle install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Run the default checks:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bundle exec rake
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The default task runs:
|
|
16
|
+
|
|
17
|
+
- Minitest
|
|
18
|
+
- RuboCop
|
|
19
|
+
- Flay
|
|
20
|
+
- Debride
|
|
21
|
+
- Bundler Audit
|
|
22
|
+
|
|
23
|
+
## Contracts
|
|
24
|
+
|
|
25
|
+
Before changing public APIs or extension seams, read `docs/contracts.md`.
|
|
26
|
+
Breaking changes are allowed while Julewire is pre-release, but every change
|
|
27
|
+
should be explicit about whether it is changing application API, extension
|
|
28
|
+
contract, or private implementation.
|
|
29
|
+
|
|
30
|
+
## Coverage
|
|
31
|
+
|
|
32
|
+
Tests run with SimpleCov gates for line and branch coverage. Core keeps
|
|
33
|
+
remote-envelope contract coverage, not bridge implementation tests.
|
|
34
|
+
|
|
35
|
+
Race tests use queue handshakes where possible. Avoid sleep-based waits unless
|
|
36
|
+
the sleep is the thing under test.
|
|
37
|
+
|
|
38
|
+
## Lockfile
|
|
39
|
+
|
|
40
|
+
`Gemfile.lock` is committed for reproducible local development and CI. Runtime
|
|
41
|
+
dependencies still belong in the gemspec.
|
|
42
|
+
|
|
43
|
+
## Packaging Notes
|
|
44
|
+
|
|
45
|
+
The gemspec is the runtime dependency contract. `Gemfile` is for development
|
|
46
|
+
tooling.
|
|
47
|
+
|
|
48
|
+
The gem is a library. Executables are not part of the public package unless
|
|
49
|
+
they are intentionally moved under `exe/` and documented.
|