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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e20df02ec73ceafd282ab52015e46a28f9aa7b1d69461e2622afc301f9e98d40
4
+ data.tar.gz: 4e12bb12f787534aa5318084f6fa69fc1888590d85874d253c12dbfc6d6701f7
5
+ SHA512:
6
+ metadata.gz: 222e5041e3d984b1e3d50a989c461522cf7622257d1698e7e9fdee9f10f2837537a883fbfe16466ac31abf868558807e1a07642e52c894ac020bf71c628a1896
7
+ data.tar.gz: 75f19494c48c6d46ae6bdf997db3605abc93323180589804d0e6338e1c7e592cd58150fbef9be4ccb5bfba577fb1086745ee8b779b6588aa7996572a55a5563f
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ ## Unreleased
2
+
3
+ ## 1.0.0 - 2026-06-21
4
+
5
+ - Initial release: execution-scoped structured logging, propagation, bounded
6
+ serialization, processors, destinations, health, tail, doctor, and CLI.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alexander Grebennik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Julewire Core
2
+
3
+ `julewire-core` is the synchronous structured logging runtime behind Julewire.
4
+
5
+ It builds provider-neutral records, execution context, propagated carry,
6
+ non-propagated attributes, final summaries, processor chains, and synchronous
7
+ direct outputs. It does not know about app frameworks, privacy policy, provider
8
+ schemas, async queues, or delivery.
9
+
10
+ ## Install
11
+
12
+ ```ruby
13
+ gem "julewire-core"
14
+ ```
15
+
16
+ ## Quickstart
17
+
18
+ ```ruby
19
+ Julewire.configure do |config|
20
+ config.destinations.use(:default, output: $stdout)
21
+ end
22
+
23
+ Julewire.with_execution(type: :job, id: "job-1") do
24
+ Julewire.context.add(tenant_id: "tenant-1")
25
+ Julewire.summary.increment(:records_seen)
26
+
27
+ Julewire.measure(:process_record) do
28
+ Julewire.emit("processed record", id: 123)
29
+ end
30
+ end
31
+ ```
32
+
33
+ By default, direct output is newline-delimited JSON.
34
+
35
+ ## Field Bags
36
+
37
+ | Bag | Use it for | Propagates? | Default JSON? |
38
+ | --- | --- | --- | --- |
39
+ | `context` | queryable facts copied onto records in the current scope | yes | yes |
40
+ | `carry` | hidden forwarding metadata for integrations and formatters | yes | no |
41
+ | `neutral` | provider-neutral formatter-coordination facts | no | no |
42
+ | `attributes` | integration and application namespaces | no | yes |
43
+ | `summary` | final-only counters, timings, and completion facts | no | summary only |
44
+ | `labels` | operator-safe routing/grouping metadata | no | yes |
45
+ | `metrics` | numeric measurements such as duration | no | yes |
46
+
47
+ ## Docs
48
+
49
+ - [Quickstart](docs/quickstart.md)
50
+ - [Configuration](docs/configuration.md)
51
+ - [Advanced Configuration](docs/advanced-configuration.md)
52
+ - [Context and Propagation](docs/context-and-propagation.md)
53
+ - [Instrumentation Cheatsheet](docs/instrumentation-cheatsheet.md)
54
+ - [Attribute Keys](docs/attribute-keys.md)
55
+ - [Records and Data Policy](docs/records-and-data-policy.md)
56
+ - [Outputs and Lifecycle](docs/outputs-and-lifecycle.md)
57
+ - [Developer Tail](docs/tail.md)
58
+ - [Health Schema](docs/health-schema.md)
59
+ - [Security and Wire Keys](docs/security-and-wire.md)
60
+ - [Extension Contracts](docs/contracts.md)
61
+ - [Extensions and API](docs/extensions-and-api.md)
62
+ - [Record Sources](docs/record-sources.md)
63
+ - [Internals](docs/internals.md)
64
+ - [Development](docs/development.md)
65
+
66
+ ## Runtime Promise
67
+
68
+ Julewire is best-effort logging infrastructure. `StandardError` failures inside
69
+ Julewire's own normalization, processing, formatting, encoding, and output path
70
+ are contained so application code keeps running.
71
+
72
+ Core stays synchronous. Custom destinations own async queues, files, fanout,
73
+ batching, retries, and delivery health.
@@ -0,0 +1,66 @@
1
+ # Advanced Configuration
2
+
3
+ ## Runtime Configuration
4
+
5
+ `configure` yields a mutable `Julewire::Core::Configuration` builder, then
6
+ stores a frozen copy with frozen registry containers. Core builds and installs
7
+ the staged pipeline against that frozen copy before swapping runtime state.
8
+
9
+ The active configuration containers exposed by `Julewire.config` and
10
+ `Julewire.labels` are read-only. Mutating runtime configuration after the fact
11
+ is unsupported. Call `Julewire.configure` again.
12
+
13
+ User-supplied destination formatter, encoder, output, and processor instances
14
+ are shared by reference. Processors, formatters, and encoders must be stateless
15
+ or otherwise reentrant; core does not synchronize them. Direct outputs are
16
+ wrapped with a per-destination mutex.
17
+
18
+ When a reconfigure reuses the same output or destination object, core keeps that
19
+ resource open for the new active pipeline and skips teardown through the old
20
+ pipeline. Replacing an output object still flushes or closes the previous one
21
+ according to its `close_output` setting.
22
+
23
+ `configure` is non-reentrant. Do not call runtime APIs such as `emit`,
24
+ `configure`, or `reset!` from inside a `configure` block.
25
+
26
+ ## Failure and Drop Callbacks
27
+
28
+ `on_failure` receives contained `StandardError` instances from Julewire's own
29
+ logging path, plus one metadata hash. Metadata may include `:phase`,
30
+ `:record_metadata`, `:action`, and output class when known.
31
+
32
+ `on_drop` receives a symbolic reason for operational drops after processing has
33
+ started, such as oversized encoded records, formatter errors, encoder errors,
34
+ output failures, output rejections, custom destination failures/rejections, and
35
+ post-close `:runtime_closed` drops.
36
+
37
+ Level filtering and intentional no-output mode are counted in health but do not
38
+ call `on_drop`; those are configuration or policy outcomes, not output-loss
39
+ callbacks. Drop callbacks receive the same metadata-hash shape.
40
+
41
+ Callbacks are best effort. Exceptions raised by callbacks are swallowed and
42
+ counted where health can report them. Core health keeps those counters small;
43
+ callbacks that need richer diagnostics should record them locally.
44
+ The recursion guard uses fiber storage, so work handed to a child fiber from
45
+ inside a callback stays inside the same callback-suppression scope.
46
+
47
+ Callbacks are called with two positional arguments:
48
+ `on_failure.call(error, metadata)` and `on_drop.call(reason, metadata)`.
49
+ Core uses Ruby duck typing at configuration time; it only requires callback
50
+ objects to respond to `call`. Arity or keyword mistakes are contained when the
51
+ callback is invoked and are counted as callback failures in health.
52
+
53
+ Drop counters count every dropped record. Use health counters for alerting;
54
+ callback frequency is intentionally not a metric. Applications that need
55
+ callback throttling should implement it in the callback.
56
+
57
+ ## Non-Standard Exception Summaries
58
+
59
+ `emit_non_standard_exception_summaries` defaults to `false`.
60
+
61
+ When false, core suppresses execution summaries while unwinding exceptions
62
+ outside `StandardError`, such as `SystemExit` and signal exceptions. When true,
63
+ core tries to emit those summaries too.
64
+
65
+ The application exception still wins; execution boundaries re-raise application
66
+ exits after recording what they can.
@@ -0,0 +1,74 @@
1
+ # Attribute Keys
2
+
3
+ Attributes are record-local fields for integrations, processors, and
4
+ formatters. They do not propagate. Integration-specific detail belongs under a
5
+ namespace such as `attributes.web`, `attributes.job`, or
6
+ `attributes.messaging`; those namespaces are emitted by default.
7
+
8
+ Neutral keys are formatter coordination fields stored in the record's `neutral`
9
+ section. Formatters can promote them to provider-native output, but the default
10
+ formatter strips the section from emitted JSON so the same fact is not logged
11
+ twice. User attributes such as `my_app.some_key` remain normal emitted
12
+ attributes.
13
+
14
+ Neutral keys track current OpenTelemetry semantic-convention names where those
15
+ names fit Julewire records. Julewire intentionally follows current/edge names
16
+ instead of pinning this page to one semconv release. Julewire still owns this
17
+ contract: upstream semconv changes make a key worth rechecking, not
18
+ automatically emitted.
19
+
20
+ ## HTTP
21
+
22
+ | Key | Meaning |
23
+ | --- | --- |
24
+ | `http.request.method` | HTTP request method. |
25
+ | `url.full` | Full filtered request URL when available. |
26
+ | `url.path` | Request path. |
27
+ | `http.response.status_code` | HTTP response status. |
28
+ | `user_agent.original` | Raw user-agent value. |
29
+ | `client.address` | Client address or remote IP. |
30
+ | `http.response.body.size` | Response body size in bytes. |
31
+
32
+ ## Code
33
+
34
+ | Key | Meaning |
35
+ | --- | --- |
36
+ | `code.file.path` | Source file path. |
37
+ | `code.line.number` | Source line number. |
38
+ | `code.function.name` | Source function, method, or label. |
39
+
40
+ ## Jobs
41
+
42
+ Current OpenTelemetry semconv does not provide a general job namespace that fits
43
+ ActiveJob-style execution summaries, so these are Julewire neutral job keys.
44
+
45
+ | Key | Meaning |
46
+ | --- | --- |
47
+ | `job.system` | Job framework or runtime, such as `active_job`. |
48
+ | `job.name` | Job class or logical job name. |
49
+ | `job.id` | Framework job id. |
50
+ | `job.provider_id` | Backend provider job id. |
51
+ | `job.queue.name` | Queue name. |
52
+ | `job.priority` | Queue priority. |
53
+ | `job.execution_count` | Framework execution count or attempt count. |
54
+ | `job.enqueued_at` | Enqueue timestamp. |
55
+ | `job.scheduled_at` | Scheduled timestamp. |
56
+ | `job.status` | Summary status such as `ok` or `error`. |
57
+
58
+ ## Messaging
59
+
60
+ | Key | Meaning |
61
+ | --- | --- |
62
+ | `messaging.system` | Messaging system, such as `kafka`. |
63
+ | `messaging.operation.name` | Operation name from the integration event. |
64
+ | `messaging.operation.type` | Generic type such as `process`, `receive`, or `send`. |
65
+ | `messaging.destination.name` | Topic, stream, or queue name. |
66
+ | `messaging.destination.partition.id` | Partition id. |
67
+ | `messaging.batch.message_count` | Message count in a batch. |
68
+ | `messaging.consumer.group.name` | Consumer group name. |
69
+ | `messaging.kafka.offset` | Kafka offset. |
70
+ | `messaging.kafka.message.key` | Kafka message key. |
71
+
72
+ Formatters should read neutral keys from `record.neutral` when producing
73
+ provider-native fields. They should not inspect integration-specific namespaces
74
+ unless they explicitly document that integration coupling.
@@ -0,0 +1,327 @@
1
+ # Configuration
2
+
3
+ `Julewire.configure` is the only mutation path for runtime configuration:
4
+
5
+ ```ruby
6
+ Julewire.configure do |config|
7
+ config.level = :info
8
+ config.destinations.use(
9
+ :default,
10
+ formatter: Julewire::RecordFormatter.new,
11
+ output: $stdout,
12
+ close_output: false
13
+ )
14
+
15
+ config.labels.add(service: "billing", env: "production")
16
+
17
+ config.on_failure = ->(error, metadata) {
18
+ warn("julewire failure phase=#{metadata[:phase]} error=#{error.class}")
19
+ }
20
+
21
+ config.on_drop = ->(reason, metadata) {
22
+ warn("julewire dropped reason=#{reason} phase=#{metadata[:phase]}")
23
+ }
24
+ end
25
+ ```
26
+
27
+ ## Core Settings
28
+
29
+ | Setting | Meaning |
30
+ | --- | --- |
31
+ | `level` | Global minimum severity. Defaults to `:debug`. |
32
+ | `pipeline_close_timeout` | Default timeout for pipeline close. Defaults to `1`. |
33
+ | `emit_non_standard_exception_summaries` | Emit summaries while unwinding `SystemExit`/signals. Defaults to `false`. |
34
+ | `error_backtrace_lines` | Exception backtrace line cap. Defaults to `20`; set `0` to omit backtraces. |
35
+ | `labels` | Static labels merged into every record. |
36
+ | `processors` | Chain that mutates/enriches records before formatting. |
37
+ | `destinations` | Named formatter/encoder/output graph or custom destinations. |
38
+ | `on_failure` | Best-effort callback for contained core failures. |
39
+ | `on_drop` | Best-effort callback for operational drops after a record reaches destination/output handling. |
40
+
41
+ Timeout values must be `nil` or non-negative finite numerics.
42
+ `error_backtrace_lines` must be a non-negative integer.
43
+
44
+ `error_backtrace_lines` applies at core's error-shaping boundaries. When a
45
+ record enters core with an `Exception` under `error`, core builds the error hash
46
+ with that limit before the record is frozen. When a formatter/serializer later
47
+ sees an `Exception`, it applies the same limit while shaping it. Integrations
48
+ should pass real `Exception` objects or core-shaped error hashes; core also
49
+ trims `backtrace` fields on those error hashes at record ingress so a forgotten
50
+ adapter limit cannot leak a full stack by accident.
51
+
52
+ Async queues, files, fanout, batching, retries, and delivery policy belong in
53
+ custom destinations. Core owns sync destination writes only.
54
+
55
+ ## Levels
56
+
57
+ Supported severities:
58
+
59
+ ```ruby
60
+ debug info warn error fatal unknown
61
+ ```
62
+
63
+ Core accepts symbols, strings, and stdlib `Logger::Severity` integers. String
64
+ and symbol names are case-insensitive.
65
+
66
+ Records with eager severity below `config.level` are dropped before context
67
+ lookup, processors, formatting, and output writes. For lazy `emit` blocks with
68
+ no eager severity, core evaluates the block first, then applies the level check
69
+ to the final record.
70
+
71
+ When `Julewire.emit` receives both a positional hash and keyword fields, keyword
72
+ fields win for duplicate top-level keys before record normalization. In lazy
73
+ emits, the block result wins over eager non-severity fields; an eager severity
74
+ from a severity helper such as `Julewire.warn` remains authoritative.
75
+
76
+ ## Static Labels
77
+
78
+ ```ruby
79
+ Julewire.configure do |config|
80
+ config.labels.add(service: "billing")
81
+ config.labels.remove(:debug_label)
82
+ config.labels.clear
83
+ end
84
+ ```
85
+
86
+ Labels follow the core field contract: public helpers accept string or symbol
87
+ keys and normalize strings to symbols at the boundary. Per-record labels
88
+ override configured static labels with the same symbol key.
89
+
90
+ Labels are meant for non-sensitive, low-cardinality dimensions such as service,
91
+ environment, shard, region, or runtime. Core may copy labels into internal
92
+ diagnostic records because labels are treated as operator-safe routing/grouping
93
+ metadata. Do not put PII, secrets, or high-cardinality payload data in labels.
94
+
95
+ ## Processors
96
+
97
+ Processors run before destinations:
98
+
99
+ ```ruby
100
+ Julewire.configure do |config|
101
+ config.processors.use MyProcessor
102
+ config.processors.use MyProcessorWithArgs, "constructor-arg", enabled: true
103
+ config.processors.use EnrichmentProcessor, on_error: :fail_open
104
+ config.processors.prepend FirstProcessor
105
+ config.processors.use Julewire::Match.new { on(severity: :debug) { :drop } }
106
+ config.processors.use :sampling, rate: 0.1
107
+ config.processors.use do |draft|
108
+ draft[:attributes][:app] = { source: "app" }
109
+ end
110
+ end
111
+ ```
112
+
113
+ Core ships no default policy processors. It does include small processor
114
+ helpers such as `Julewire::Match` and deterministic head sampling through the
115
+ registered `:sampling` processor. Redaction and enrichment belong in
116
+ processors. Output-specific shape belongs in destination formatters. Processors
117
+ are the mutation stage.
118
+
119
+ Processors receive the current `Julewire::RecordDraft`. Mutate the draft
120
+ directly. Return `:drop` to stop delivery or a different `Julewire::RecordDraft` to
121
+ replace the current draft; any other return value is ignored. Processors own
122
+ the draft until the final record boundary, so direct mutation is the normal hot
123
+ path:
124
+
125
+ ```ruby
126
+ class AddTenantLabel
127
+ def call(draft)
128
+ tenant = draft.fetch(:context).fetch(:tenant_id, nil)
129
+ return unless tenant
130
+
131
+ draft[:labels][:tenant] = tenant
132
+ end
133
+ end
134
+
135
+ Julewire.configure do |config|
136
+ config.processors.prepend AddTenantLabel.new
137
+ end
138
+ ```
139
+
140
+ The `:sampling` processor hashes a deterministic key and returns `:drop` for
141
+ unsampled records. By default it uses execution/root identifiers when present,
142
+ then `context.request_id`, then deterministic record fields. Pass `key:` to choose an
143
+ application-specific key:
144
+
145
+ ```ruby
146
+ config.processors.use(
147
+ :sampling,
148
+ rate: 0.05,
149
+ key: ->(draft) { draft.dig(:context, :tenant_id) }
150
+ )
151
+ ```
152
+
153
+ Processor exceptions use the registration policy:
154
+
155
+ | `on_error` | Behavior |
156
+ | --- | --- |
157
+ | `:fail_closed` | Default. Emit a `julewire.processor_error` replacement record and stop the chain. |
158
+ | `:fail_open` | Record the failure, keep the current draft, and continue with later processors. |
159
+ | `:drop` | Record the failure and suppress the record. |
160
+
161
+ Public emit input remains defensive. The final immutable record boundary
162
+ validates the Julewire record shape before destinations see it.
163
+
164
+ Destination-local processors run after the global processor chain and before
165
+ one destination formats the record. Use them for sink-specific policy such as
166
+ audit-only enrichment or destination-only sampling:
167
+
168
+ ```ruby
169
+ Julewire.configure do |config|
170
+ config.destinations.use(:stdout, output: $stdout)
171
+ config.destinations.use(
172
+ :audit,
173
+ output: audit_io,
174
+ processors: [
175
+ ->(draft) { draft[:labels][:sink] = "audit" }
176
+ ]
177
+ )
178
+ end
179
+ ```
180
+
181
+ A destination-local drop or processor failure affects only that destination.
182
+
183
+ ## Destinations
184
+
185
+ Direct destinations pair a formatter, encoder, and output:
186
+
187
+ ```ruby
188
+ Julewire.configure do |config|
189
+ config.destinations.use(
190
+ :default,
191
+ formatter: Julewire::RecordFormatter.new,
192
+ encoder: Julewire::JsonEncoder.new,
193
+ output: $stdout
194
+ )
195
+ end
196
+ ```
197
+
198
+ Integration gems may register destination kinds with adapter-specific options:
199
+
200
+ ```ruby
201
+ Julewire.configure do |config|
202
+ config.destinations.use(:provider_json, output: $stdout)
203
+ config.destinations.use(:transport, formatter: formatter, io: $stdout)
204
+ end
205
+ ```
206
+
207
+ For local human-readable output, pair the console formatter with the text
208
+ encoder:
209
+
210
+ ```ruby
211
+ Julewire.configure do |config|
212
+ config.destinations.use(
213
+ :console,
214
+ formatter: Julewire::ConsoleFormatter.new,
215
+ encoder: Julewire::TextEncoder.new(color: $stdout.tty?),
216
+ output: $stdout
217
+ )
218
+ end
219
+ ```
220
+
221
+ For a loud local console, `Julewire.punk!` replaces the named runtime's
222
+ destinations with the console formatter and the punk text theme:
223
+
224
+ ```ruby
225
+ Julewire.punk!(color: $stdout.tty?)
226
+ ```
227
+
228
+ For local containment drills, add `chaos: true`. It wraps the output with a
229
+ small chaos sink that occasionally raises, rejects, or stalls writes so the
230
+ runtime health path visibly degrades while application calls stay contained.
231
+
232
+ ```ruby
233
+ Julewire.punk!(color: $stdout.tty?, chaos: true)
234
+ ```
235
+
236
+ `Julewire.dev!` is the same core-local dev shape with a bounded in-memory tail
237
+ attached:
238
+
239
+ ```ruby
240
+ tail = Julewire.dev!(chaos: true, tail: { capacity: 500 })
241
+ ```
242
+
243
+ Additional destinations are explicit:
244
+
245
+ ```ruby
246
+ Julewire.configure do |config|
247
+ config.destinations.use(
248
+ :json_stdout,
249
+ formatter: Julewire::RecordFormatter.new,
250
+ encoder: Julewire::JsonEncoder.new,
251
+ output: $stdout
252
+ )
253
+
254
+ config.destinations.use(
255
+ :debug_file,
256
+ formatter: Julewire::RecordFormatter.new,
257
+ encoder: Julewire::JsonEncoder.new,
258
+ output: File.open("log/debug.json", "a"),
259
+ close_output: true
260
+ )
261
+ end
262
+ ```
263
+
264
+ Destination options:
265
+
266
+ - `formatter`
267
+ - `encoder`
268
+ - `output`
269
+ - `close_output`
270
+ - `max_record_bytes` (defaults to `1 MiB`)
271
+ - `on_failure`
272
+ - `on_drop`
273
+ - `processors`
274
+
275
+ Extensions can also install an already-built destination object:
276
+
277
+ ```ruby
278
+ config.destinations.add(MyDestination.new(name: :custom))
279
+ ```
280
+
281
+ Custom destination objects must respond to `name`, `emit(record)`, `flush`,
282
+ `close`, and `health`.
283
+
284
+ `Julewire.configure` starts from the active configuration. To replace the output
285
+ graph in a later configure call, clear destinations first:
286
+
287
+ ```ruby
288
+ Julewire.configure do |config|
289
+ config.destinations.clear
290
+ config.destinations.use(:default, output: $stdout)
291
+ end
292
+ ```
293
+
294
+ ## Split Pipelines
295
+
296
+ The top-level `Julewire` facade uses the default runtime. Use named runtimes
297
+ when one process needs separate pipeline policy, such as redacted stdout and a
298
+ locked-down audit sink:
299
+
300
+ ```ruby
301
+ Julewire.runtime(:audit).configure do |config|
302
+ config.destinations.use(:audit, output: audit_io)
303
+ end
304
+
305
+ Julewire.runtime(:audit).emit(message: "audit-only")
306
+ ```
307
+
308
+ Named runtimes have their own configuration, processors, destinations, health,
309
+ and close/flush lifecycle. `Julewire.after_fork!` resets every known runtime.
310
+
311
+ Destinations receive the immutable processed `Julewire::Record`. Hashes,
312
+ arrays, and copied strings inside the record are frozen; arbitrary app objects
313
+ inside fields are not deep-frozen. Formatters must treat the record as read-only
314
+ and return a payload object. Encoders turn formatter payloads into strings
315
+ before writing to direct outputs. The default encoder is
316
+ `Julewire::JsonEncoder`, which applies Julewire serialization before
317
+ `JSON.generate`. A formatter that mutates frozen containers raises and is
318
+ handled as a formatter failure.
319
+
320
+ Destination callbacks inherit from the global callbacks unless overridden.
321
+
322
+ For reconfigure semantics, callback details, and other low-level knobs, see
323
+ [Advanced Configuration](advanced-configuration.md).
324
+
325
+ Calling `Julewire.emit` from callbacks may still run the nested emit, but nested
326
+ callback delivery is suppressed as callback recursion. Components that track
327
+ callback failures count the suppression.