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,29 @@
|
|
|
1
|
+
# Instrumentation Cheatsheet
|
|
2
|
+
|
|
3
|
+
Use the smallest entrypoint that matches the work:
|
|
4
|
+
|
|
5
|
+
| Need | API |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Add ambient fields | `Julewire.context.add(...)`, `Julewire.attributes.add(...)`, `Julewire.carry.add(...)` |
|
|
8
|
+
| Emit a point record | `Julewire.info("message", attributes: {...})` |
|
|
9
|
+
| Wrap work with a summary | `Julewire.with_execution(type: :job, id: id) { ... }` |
|
|
10
|
+
| Measure block-shaped work | `Julewire.measure(:db) { query }` |
|
|
11
|
+
| Measure split start/finish work | `handle = Julewire.measure_start(:db); handle.finish` |
|
|
12
|
+
| Install a destination | `config.destinations.use(:default, output: $stdout)` |
|
|
13
|
+
| Tail local records | `julewire tail log/julewire.jsonl --format=auto --follow` |
|
|
14
|
+
|
|
15
|
+
Field placement:
|
|
16
|
+
|
|
17
|
+
| Field bag | Use for |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `context` | Request/job/message identity useful on every record in scope. |
|
|
20
|
+
| `carry` | Propagation data that may cross process boundaries. |
|
|
21
|
+
| `neutral` | Provider-neutral semantic fields such as HTTP, job, code, or messaging keys. |
|
|
22
|
+
| `attributes` | Framework/app-specific structured data. |
|
|
23
|
+
| `payload` | User event payload for point records and summary counters. |
|
|
24
|
+
| `metrics` | Numeric summary measurements. |
|
|
25
|
+
| `labels` | Operator routing labels. |
|
|
26
|
+
|
|
27
|
+
`logger` names the entrypoint that produced the record, such as
|
|
28
|
+
`"framework.error"` or `"job.event"`. `source` names the integration or
|
|
29
|
+
runtime producer, such as `"web"`, `"job"`, or `"julewire"`.
|
data/docs/internals.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Internals
|
|
2
|
+
|
|
3
|
+
This file is for contributors and extension authors who need the shape of the
|
|
4
|
+
machine without reading every class first.
|
|
5
|
+
|
|
6
|
+
## Runtime State
|
|
7
|
+
|
|
8
|
+
The runtime stores immutable snapshots of:
|
|
9
|
+
|
|
10
|
+
- configuration
|
|
11
|
+
- pipeline
|
|
12
|
+
- whether the active pipeline has been closed
|
|
13
|
+
|
|
14
|
+
Writes are serialized where state transitions need serialization. Main-ractor
|
|
15
|
+
hot-path reads use the current frozen runtime state without taking a global
|
|
16
|
+
runtime lock; state transitions swap that reference under the runtime state
|
|
17
|
+
mutex. Health and counters use their own synchronization outside that mutex.
|
|
18
|
+
Non-main ractors use ractor-local runtime storage because they cannot touch the
|
|
19
|
+
main runtime object directly.
|
|
20
|
+
|
|
21
|
+
Configuration, reconfigure, reset, and close are state transitions. They should
|
|
22
|
+
stay boring and explicit.
|
|
23
|
+
|
|
24
|
+
Configure calls are serialized with a dedicated configure mutex. The staged
|
|
25
|
+
pipeline is built before runtime state is swapped. If runtime state changes
|
|
26
|
+
before a staged configure can swap in, the staged pipeline is closed and
|
|
27
|
+
configure fails.
|
|
28
|
+
|
|
29
|
+
Close is terminal for the active runtime state. Post-close emits are counted and
|
|
30
|
+
dropped before pipeline work. Reconfigure installs a fresh open pipeline.
|
|
31
|
+
|
|
32
|
+
## Pipeline
|
|
33
|
+
|
|
34
|
+
The normal emit path is:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
threshold precheck
|
|
38
|
+
record normalization
|
|
39
|
+
static labels
|
|
40
|
+
processors
|
|
41
|
+
destinations
|
|
42
|
+
formatter/encoder/output write per destination
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Below-threshold records with eager severity are dropped before context lookup
|
|
46
|
+
and record building. Lazy `emit` blocks without eager severity are evaluated
|
|
47
|
+
first so block-provided severity can be checked correctly.
|
|
48
|
+
|
|
49
|
+
Pipeline counters are operational counters, not delivery guarantees.
|
|
50
|
+
Destination `output_accepted` means the configured output accepted the encoded
|
|
51
|
+
string according to core's output contract.
|
|
52
|
+
|
|
53
|
+
## Fields::FieldSet
|
|
54
|
+
|
|
55
|
+
`Fields::FieldSet` owns three invariants:
|
|
56
|
+
|
|
57
|
+
- defensive copying of hashes, arrays, and strings at trust boundaries
|
|
58
|
+
- cycle-safe duplication of those container graphs
|
|
59
|
+
- symbol-key normalization
|
|
60
|
+
|
|
61
|
+
Public ingress accepts string or symbol keys because JSON-style hashes and
|
|
62
|
+
Ruby-style hashes both happen in app code. Core normalizes those keys once at
|
|
63
|
+
the boundary and uses symbol keys internally so processors, destinations, and
|
|
64
|
+
extensions do not pay repeated equivalent-key scans.
|
|
65
|
+
|
|
66
|
+
## Value Readers
|
|
67
|
+
|
|
68
|
+
`Records::RawInput` reads app-facing emit input before record construction.
|
|
69
|
+
`Integration::Values::Read` extracts best-effort values from framework objects.
|
|
70
|
+
`Integration::Values::Shape` normalizes adapter-built hashes. `Fields::FieldSet`
|
|
71
|
+
owns trusted field-bag copying, merging, and key normalization after values have
|
|
72
|
+
entered core. `Fields::Lookup` is for display reads that tolerate symbol/string
|
|
73
|
+
keys and return `nil` for unreadable inputs.
|
|
74
|
+
|
|
75
|
+
## Context Storage
|
|
76
|
+
|
|
77
|
+
Main runtime context uses storage on the current Ruby fiber.
|
|
78
|
+
|
|
79
|
+
Raw fibers do not inherit parent context. Julewire wrappers capture and restore
|
|
80
|
+
propagation envelopes explicitly.
|
|
81
|
+
|
|
82
|
+
Non-main ractors use thread-local storage because concurrent-ruby local-variable
|
|
83
|
+
helpers are not Ractor-shareable.
|
|
84
|
+
|
|
85
|
+
## Destination Boundary
|
|
86
|
+
|
|
87
|
+
Core writes synchronously to configured outputs. Async queues, files, fanout,
|
|
88
|
+
batching, retries, acknowledgements, and delivery health live in
|
|
89
|
+
custom destination objects.
|
|
90
|
+
|
|
91
|
+
## Scheduling
|
|
92
|
+
|
|
93
|
+
`Scheduling::DeadlineScheduler` is the small stdlib timer heap. Main-process
|
|
94
|
+
diagnostics and framework integrations use `Scheduling::SharedScheduler` so
|
|
95
|
+
they do not each keep a background timer thread. Ractor integrations keep their
|
|
96
|
+
own schedulers because worker ractors cannot share the main scheduler object.
|
|
97
|
+
|
|
98
|
+
## Remote Envelope Hook
|
|
99
|
+
|
|
100
|
+
Core keeps `Runtime#emit_envelope(input:, context:, attributes:, carry:, neutral:, scope:, enforce_level:)`
|
|
101
|
+
for bridge code. The bridge reconstructs the scope snapshot, and core rebuilds
|
|
102
|
+
a normal record from input, context, attributes, carry, neutral, and that
|
|
103
|
+
snapshot before emitting through the active pipeline. It is not a public
|
|
104
|
+
application API.
|
|
105
|
+
|
|
106
|
+
## Test Seams
|
|
107
|
+
|
|
108
|
+
Some private methods are intentionally reachable through `Julewire::Testing`.
|
|
109
|
+
They reset process-global registries or storage that normal applications should
|
|
110
|
+
not touch directly.
|
|
111
|
+
|
|
112
|
+
## Emit Entrypoints
|
|
113
|
+
|
|
114
|
+
| Entrypoint | Caller | Level gate | Input ownership |
|
|
115
|
+
| --- | --- | --- | --- |
|
|
116
|
+
| `Runtime#emit` | application facade | yes | normalized by core |
|
|
117
|
+
| `Runtime#emit_without_level` | host-process integrations | no | normalized by core |
|
|
118
|
+
| `Integration::Facade.emit` | integration SPI | configurable | adapter-owned record hash |
|
|
119
|
+
| `Runtime#emit_envelope` | bridge SPI | configurable | detached, adapter-owned envelope |
|
|
120
|
+
|
|
121
|
+
## Field Stack Layers
|
|
122
|
+
|
|
123
|
+
Field stacks keep immutable layers plus versioned read caches. A layer without
|
|
124
|
+
a parent builds a direct snapshot from its own fields. A layer whose parent
|
|
125
|
+
already has a snapshot merges on top of that cached parent snapshot. Otherwise
|
|
126
|
+
it walks the unsnapshotted source chain back to the nearest cached ancestor and
|
|
127
|
+
then replays fields plus delete paths forward.
|
|
128
|
+
|
|
129
|
+
## Best-Effort Rule
|
|
130
|
+
|
|
131
|
+
Core may contain `StandardError` from its own logging path. It must not swallow
|
|
132
|
+
application exceptions from user code.
|
|
133
|
+
|
|
134
|
+
That rule is the line between "logging must not crash the app" and "debugging
|
|
135
|
+
must not hide the app's real exception".
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Outputs and Lifecycle
|
|
2
|
+
|
|
3
|
+
Outputs are stdout-ish:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
output.write(string)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Optional methods:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
output.flush
|
|
13
|
+
output.close
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Lifecycle hooks are no-argument output hooks:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
flush
|
|
20
|
+
close
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Runtime timeouts provide one carried deadline while core walks destinations.
|
|
24
|
+
Core cannot interrupt arbitrary blocking output code, and plain outputs do not
|
|
25
|
+
receive the timeout value. After the first attempted resource, core skips later
|
|
26
|
+
resources when the deadline is already exhausted. Custom destination objects own
|
|
27
|
+
timeout-aware drain, retry, reopen, and rotation policy.
|
|
28
|
+
|
|
29
|
+
## Boundary
|
|
30
|
+
|
|
31
|
+
Core is synchronous output only:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
emit -> normalize -> processors -> destination(formatter -> encoder -> output)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Destinations format an immutable `Julewire::Record` into a payload object.
|
|
38
|
+
Encoders turn direct destination payload objects into strings. Outputs receive
|
|
39
|
+
strings.
|
|
40
|
+
|
|
41
|
+
Processors may intentionally return `:drop`. Core counts those records in
|
|
42
|
+
`health[:pipeline][:counts][:processor_dropped]`; processor failures are counted
|
|
43
|
+
separately as `processor_error`.
|
|
44
|
+
|
|
45
|
+
Async queues, files, fanout appenders, batching, retries, acknowledgements,
|
|
46
|
+
reopen, rotation, shutdown hooks, and delivery policy belong in custom
|
|
47
|
+
destination objects. Core keeps enough output lifecycle to be useful for
|
|
48
|
+
`$stdout`, tests, and small scripts.
|
|
49
|
+
|
|
50
|
+
## No Output
|
|
51
|
+
|
|
52
|
+
An empty destination registry means an intentional no-output pipeline. Records
|
|
53
|
+
are dropped before normalization, processors, and formatting. Core increments
|
|
54
|
+
`health[:pipeline][:counts][:no_output_dropped]` so accidental no-output mode is
|
|
55
|
+
visible in health.
|
|
56
|
+
|
|
57
|
+
Application and integration code should usually make no-output mode
|
|
58
|
+
explicit instead of inheriting it silently. Core keeps it available for tests,
|
|
59
|
+
scripts, and deliberate dry runs.
|
|
60
|
+
|
|
61
|
+
## Ownership
|
|
62
|
+
|
|
63
|
+
Outputs are caller-owned by default.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Julewire.configure do |config|
|
|
67
|
+
file = File.open("log/julewire.log", "a")
|
|
68
|
+
config.destinations.use(:default, output: file, close_output: true)
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use `close_output = true` for files or sockets core should close. Keep it false
|
|
73
|
+
for `$stdout`, caller-owned loggers, and shared objects.
|
|
74
|
+
|
|
75
|
+
## Synchronous Output
|
|
76
|
+
|
|
77
|
+
Core wraps direct outputs with one mutex. Concurrent emitters do not interleave
|
|
78
|
+
individual encoded records, but a slow sink blocks the emitting thread.
|
|
79
|
+
|
|
80
|
+
A synchronous output failure is contained. The destination records output
|
|
81
|
+
failure counters and calls `on_failure`. Core does not retry, back off, or
|
|
82
|
+
short-circuit broken outputs; those are destination policies.
|
|
83
|
+
|
|
84
|
+
Plain `write` outputs and custom destination `emit` calls are optimistic: any
|
|
85
|
+
non-raising result except `false` is accepted. A plain `false` is treated as a
|
|
86
|
+
rejected write or destination rejection and counted as destination loss. Custom
|
|
87
|
+
destination objects still own richer backpressure, retry, and partial acceptance
|
|
88
|
+
semantics.
|
|
89
|
+
|
|
90
|
+
## Destinations
|
|
91
|
+
|
|
92
|
+
Direct core output is always configured through destinations:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
emit -> normalize -> processors -> :default(formatter -> encoder -> output)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Multiple destinations are explicit:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
Julewire.configure do |config|
|
|
102
|
+
config.destinations.use(
|
|
103
|
+
:json_stdout,
|
|
104
|
+
formatter: Julewire::RecordFormatter.new,
|
|
105
|
+
encoder: Julewire::JsonEncoder.new,
|
|
106
|
+
output: $stdout
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
config.destinations.use(
|
|
110
|
+
:debug,
|
|
111
|
+
formatter: Julewire::RecordFormatter.new,
|
|
112
|
+
encoder: Julewire::JsonEncoder.new,
|
|
113
|
+
output: File.open("log/debug.json", "a"),
|
|
114
|
+
close_output: true
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Custom destinations may add a destination object directly with
|
|
120
|
+
`config.destinations.add(destination)`. That object receives the same immutable
|
|
121
|
+
processed `Julewire::Record` and owns its own formatter, serialization,
|
|
122
|
+
and delivery contract.
|
|
123
|
+
|
|
124
|
+
Multiple destinations receive the same immutable processed record object.
|
|
125
|
+
Hashes, arrays, and copied strings inside the record are frozen; arbitrary app
|
|
126
|
+
objects inside fields are not deep-frozen. Formatters are read-only mappers.
|
|
127
|
+
Mutation belongs in processors.
|
|
128
|
+
|
|
129
|
+
Destinations cannot share the same raw output object. Core sync output keeps one
|
|
130
|
+
mutex per destination and rejects shared sinks instead of guessing fanout
|
|
131
|
+
semantics. Use a custom destination when same-format multi-sink writes need
|
|
132
|
+
shared locking, async, buffering, rotation, batching, or fanout policy.
|
|
133
|
+
|
|
134
|
+
## Lifecycle
|
|
135
|
+
|
|
136
|
+
`Julewire.flush` and `Julewire.close` call the active pipeline's destinations
|
|
137
|
+
in order.
|
|
138
|
+
|
|
139
|
+
`flush` and `close` use `config.pipeline_close_timeout` when no timeout is
|
|
140
|
+
provided. A caller-provided `nil` timeout means unbounded wait. This timeout
|
|
141
|
+
does not make direct output calls interruptible; it only bounds whether core
|
|
142
|
+
attempts later destinations after a previous lifecycle call returns.
|
|
143
|
+
|
|
144
|
+
`close` is terminal for the active runtime state. If output close fails or times
|
|
145
|
+
out, `Julewire.close` returns `false`, but later emits still drop as
|
|
146
|
+
`runtime_closed` until the next `configure` or `reset!`.
|
|
147
|
+
|
|
148
|
+
`Julewire.after_fork!` resets process-local counters, failure snapshots,
|
|
149
|
+
current context, warning state, and mutexes inherited from the parent process.
|
|
150
|
+
It also forwards `after_fork!` to destinations and outputs that implement it,
|
|
151
|
+
then runs integration after-fork hooks registered through core. File, socket,
|
|
152
|
+
queue, and async transports should reopen worker-local resources from their
|
|
153
|
+
destination or output `after_fork!` method.
|
|
154
|
+
|
|
155
|
+
Core does not install an `at_exit` hook. Small scripts should call close from
|
|
156
|
+
their own shutdown path:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
begin
|
|
160
|
+
# work
|
|
161
|
+
ensure
|
|
162
|
+
Julewire.close(timeout: 1)
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Applications and integration code should install their own shutdown hooks with
|
|
167
|
+
finite timeouts.
|
|
168
|
+
|
|
169
|
+
## Health
|
|
170
|
+
|
|
171
|
+
Core health is a pipeline snapshot, not a delivery receipt.
|
|
172
|
+
|
|
173
|
+
Preferred alert fields:
|
|
174
|
+
|
|
175
|
+
- top-level `status`, `closed`, `generation`, `counts`, and `last_failure`
|
|
176
|
+
- `pipeline.configured`, `pipeline.counts`, and `pipeline.last_failure`
|
|
177
|
+
- `destinations.*.counts`
|
|
178
|
+
- `destinations.*.last_failure`
|
|
179
|
+
- `destinations.*.last_loss`
|
|
180
|
+
|
|
181
|
+
Destination `counts` includes direct-output loss counters such as formatter
|
|
182
|
+
failures, encoding failures, record-size drops, output exceptions, and output
|
|
183
|
+
rejections. Custom destination objects own their own queue, file, fanout,
|
|
184
|
+
retry, delivery, and lifecycle health fields.
|
|
185
|
+
|
|
186
|
+
Destination loss belongs to the active pipeline generation. Any destination
|
|
187
|
+
with non-zero active-generation loss is degraded until reconfigure or reset.
|
|
188
|
+
`destinations.*.last_loss` gives the latest safe loss reason and record
|
|
189
|
+
coordinates without exposing raw payloads or exception messages.
|
|
190
|
+
|
|
191
|
+
Integrations own concrete metric names. Core keeps health paths explicit for
|
|
192
|
+
contract tests, but does not prescribe external metrics mapping.
|
|
193
|
+
|
|
194
|
+
## Loss Model
|
|
195
|
+
|
|
196
|
+
Core is best effort:
|
|
197
|
+
|
|
198
|
+
- no output drops records intentionally
|
|
199
|
+
- level filtering drops records before normalization when eager severity is known
|
|
200
|
+
- oversized encoded records are dropped
|
|
201
|
+
- formatter and encoder failures drop the affected record
|
|
202
|
+
- output exceptions and `false` writes drop the affected record
|
|
203
|
+
- post-close emits are dropped and counted at runtime level
|
|
204
|
+
|
|
205
|
+
Core does not provide durable storage. Custom destinations that need delivery
|
|
206
|
+
semantics own batching, retry, acknowledgement, and shutdown drain behavior.
|
data/docs/quickstart.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
Start with one configured output:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Julewire.configure do |config|
|
|
7
|
+
config.destinations.use(:default, output: $stdout)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Julewire.emit(message: "hello")
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
By default, records are encoded as newline-delimited JSON.
|
|
14
|
+
|
|
15
|
+
## Point Records
|
|
16
|
+
|
|
17
|
+
`Julewire.emit` writes an immediate point record:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
Julewire.emit(
|
|
21
|
+
severity: :info,
|
|
22
|
+
event: "customer.charged",
|
|
23
|
+
message: "charged customer",
|
|
24
|
+
payload: { amount_cents: 1200 }
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The shorthand form is useful for scripts:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
Julewire.emit("starting import")
|
|
32
|
+
Julewire.warn("retrying import", attempt: 3)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Known record keywords stay at the record top level: `timestamp`, `severity`,
|
|
36
|
+
`kind`, `event`, `message`, `logger`, `source`, `execution`, `context`,
|
|
37
|
+
`carry`, `neutral`, `attributes`, `labels`, `payload`, `metrics`, and `error`.
|
|
38
|
+
Other fields are folded into `payload`. Explicit `payload` keys win over folded
|
|
39
|
+
fields when they collide.
|
|
40
|
+
|
|
41
|
+
For expensive low-level payloads, put the severity in the eager fields and
|
|
42
|
+
build the rest lazily:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Julewire.emit(severity: :debug) do
|
|
46
|
+
{ message: "expanded import state", payload: expensive_snapshot }
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The block is not called when the eager severity is below the configured level.
|
|
51
|
+
If eager severity is present, it is authoritative; a block cannot upgrade or
|
|
52
|
+
downgrade it. If eager severity is absent, the block may provide severity, but
|
|
53
|
+
the block must run before core can apply the level check. Severity helpers use
|
|
54
|
+
the eager-severity rule:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Julewire.debug { { message: "expanded import state", payload: expensive_snapshot } }
|
|
58
|
+
Julewire.warn("retrying")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`emit` returns `nil`. Use configured outputs, callbacks, and `Julewire.health`
|
|
62
|
+
for observation.
|
|
63
|
+
|
|
64
|
+
## Executions and Summaries
|
|
65
|
+
|
|
66
|
+
Use `with_execution` around work that has a beginning and an end:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Julewire.with_execution(type: :operation, id: "op-1") do
|
|
70
|
+
Julewire.context.add(tenant_id: "tenant-1")
|
|
71
|
+
Julewire.summary.add(plan: "pro")
|
|
72
|
+
|
|
73
|
+
Julewire.emit(message: "doing work")
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Context fields appear on point records and summaries. Summary fields appear only
|
|
78
|
+
on the final summary. `with_execution` emits one summary record by default on
|
|
79
|
+
completion or failure.
|
|
80
|
+
|
|
81
|
+
Nested executions are allowed. Each level gets its own summary, and point
|
|
82
|
+
records include cheap parent/root/depth metadata. Processors can use the
|
|
83
|
+
explicit lineage accessor when they need the bounded ancestor chain.
|
|
84
|
+
|
|
85
|
+
## No Output Means No-Op
|
|
86
|
+
|
|
87
|
+
This is valid:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
Julewire.configure do |config|
|
|
91
|
+
config.destinations.clear
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
With no destinations, core does not build, process, format, or write records. It
|
|
96
|
+
only increments `health[:pipeline][:counts][:no_output_dropped]`. This is useful
|
|
97
|
+
in tests and in deployments where custom code owns output setup.
|
|
98
|
+
|
|
99
|
+
## The Mental Model
|
|
100
|
+
|
|
101
|
+
Think of core as:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
emit -> normalize -> processors -> destination(formatter -> encoder -> output)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Processors own neutral policy and enrichment. A destination owns the final
|
|
108
|
+
formatter/output path. Its formatter sees an immutable processed
|
|
109
|
+
`Julewire::Record` and returns a payload object; its encoder turns that
|
|
110
|
+
payload into the output string.
|
|
111
|
+
|
|
112
|
+
For field placement, use this rule:
|
|
113
|
+
|
|
114
|
+
| Field bag | Best for |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `context` | queryable facts copied onto point records and summaries |
|
|
117
|
+
| `carry` | propagation metadata for integrations and custom formatters |
|
|
118
|
+
| `neutral` | provider-neutral facts read by formatters |
|
|
119
|
+
| `attributes` | integration namespaces and application metadata |
|
|
120
|
+
| `summary` | final-only counters, timings, and completion facts |
|
|
121
|
+
| `labels` | operator-safe routing/grouping metadata |
|
|
122
|
+
| `metrics` | numeric measurements such as duration |
|
|
123
|
+
|
|
124
|
+
Custom destination objects own async output, files, fanout, batching, retries,
|
|
125
|
+
and delivery policy:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
emit -> normalize -> processors -> custom destination
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Core stays synchronous. Processors and direct destination formatters run on the
|
|
132
|
+
emitting thread. Custom destinations own their own serialization and transport
|
|
133
|
+
work.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Record Sources
|
|
2
|
+
|
|
3
|
+
Core records come from several broad source classes. Integrations can map
|
|
4
|
+
their own runtime surfaces into these shapes, but core does not know those
|
|
5
|
+
runtime APIs.
|
|
6
|
+
|
|
7
|
+
| Source | Julewire shape | Rule |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| External event | Point record | Map runtime event data into `event`, `source`, `context`, and `attributes`; leave `payload` for caller data. |
|
|
10
|
+
| Logger call | Point record, usually `event: "log"` | Treat it as a real log item. Do not parse or deduplicate text. |
|
|
11
|
+
| Execution boundary | Summary record | Use `with_execution` and emit one final summary when enabled. |
|
|
12
|
+
| Error report | Point record | Map explicit error reports as point records with current context. |
|
|
13
|
+
| Rendered/runtime exception | Point record | Map structured exception surfaces. Do not parse human exception text. |
|
|
14
|
+
|
|
15
|
+
Core only normalizes, processes, formats, and writes records. Integrations own
|
|
16
|
+
subscription, suppression of duplicate upstream emitters, and any
|
|
17
|
+
runtime-specific policy.
|