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,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.
@@ -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.