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,230 @@
1
+ # Records and Data Policy
2
+
3
+ Core records are symbol-key hashes before formatting. Public ingress accepts
4
+ string or symbol keys defensively. Processors receive a mutable
5
+ `Julewire::RecordDraft`; destinations and formatters receive an immutable
6
+ `Julewire::Record`. The canonical symbol-key shape is:
7
+
8
+ ```ruby
9
+ {
10
+ timestamp: Time.now.utc,
11
+ severity: :info,
12
+ kind: :point,
13
+ event: "log",
14
+ message: "message",
15
+ logger: nil,
16
+ source: nil,
17
+ execution: {},
18
+ context: {},
19
+ carry: {},
20
+ neutral: {},
21
+ attributes: {},
22
+ labels: {},
23
+ payload: {},
24
+ metrics: {},
25
+ error: nil
26
+ }
27
+ ```
28
+
29
+ Raw input is normalized through `Julewire::RecordDraft.build`, then processors
30
+ mutate the draft owned by the current emit. After processors finish, core
31
+ validates the draft and freezes it through `Julewire::RecordDraft#to_record`
32
+ before destinations and formatters see it. Immutable `Record` objects are not
33
+ the public raw-input construction API.
34
+
35
+ `Julewire::RecordFormatter` turns that normalized record into a public log
36
+ projection. It omits internal keys such as `:carry` and execution lineage
37
+ internals such as `:depth` and `:root`. Direct core destinations pass formatter
38
+ payloads to an encoder. The default encoder serializes, compacts, and writes one
39
+ JSON object plus a newline.
40
+
41
+ Records built from log-safe field values are `Ractor.shareable?` after
42
+ finalization. Use `Serializer.call` before crossing ractor, process, or network
43
+ boundaries with arbitrary application objects.
44
+
45
+ ## Record Kinds
46
+
47
+ Core supports:
48
+
49
+ - `:point` for immediate records from `emit`
50
+ - `:summary` for final execution summaries
51
+
52
+ Unknown kinds are treated as extension bugs and contained as Julewire
53
+ normalization failures. They are not silently coerced.
54
+
55
+ Invalid explicit record severity is treated as caller data quality trouble, not
56
+ a logging outage. Core normalizes the record severity to `:info`, writes one
57
+ process warning, and counts the normalization in health with value class plus
58
+ source/event metadata when available. Level filtering then applies normally, so
59
+ an invalid explicit severity can still be dropped when `config.level` is above
60
+ `:info`. Configuration severities remain strict.
61
+
62
+ ## Structured Sections
63
+
64
+ Structured sections prefer hashes:
65
+
66
+ ```ruby
67
+ execution context carry neutral attributes labels payload metrics
68
+ ```
69
+
70
+ Labels are operator metadata, not payload storage. Treat them as non-sensitive,
71
+ low-cardinality dimensions that are safe to copy into diagnostic records and
72
+ indexes. PII and secrets belong in payload/context fields that a processor can
73
+ handle before formatting.
74
+
75
+ Carry is propagated correlation data. It is present on normalized records and
76
+ carried through propagation envelopes, but it is not emitted by the default
77
+ formatter and is not execution identity. Use it for small facts that
78
+ integrations or formatters need on every record. Put large diagnostic snapshots
79
+ in summary payloads instead.
80
+
81
+ If application code supplies a non-hash value for a structured section, core
82
+ preserves it under `:value` instead of dropping it:
83
+
84
+ ```ruby
85
+ Julewire.emit(payload: "raw")
86
+ # payload: { value: "raw" }
87
+ ```
88
+
89
+ Runtime mutation helpers are forgiving for the same reason: logging should not
90
+ crash the app.
91
+
92
+ String keys inside structured sections are normalized to symbols before
93
+ processors and destinations run. Encoders and custom destinations may convert
94
+ the record to string-key payloads, but the core Ruby record contract stays
95
+ symbol-keyed.
96
+
97
+ ## Optional Metadata
98
+
99
+ `logger` is the logical logger entrypoint, for example `"app"` or
100
+ `"billing.audit"`.
101
+
102
+ `source` is the producer source. Raw application emits leave it `nil`;
103
+ integrations should set it.
104
+
105
+ `timestamp` defaults to `Time.now.utc` when omitted. If caller code supplies an
106
+ explicit timestamp, core preserves that value as caller data. Output code that
107
+ requires a time-like timestamp should validate or coerce it before export.
108
+
109
+ ## Execution Lineage
110
+
111
+ Normalized nested executions include cheap relationship metadata:
112
+
113
+ ```ruby
114
+ execution: {
115
+ type: "job",
116
+ id: "job-1",
117
+ depth: 2,
118
+ root: { type: "request", id: "req-1" },
119
+ parent: { type: "request", id: "req-1" }
120
+ }
121
+ ```
122
+
123
+ The full ancestor chain is available through the explicit lineage accessor:
124
+
125
+ ```ruby
126
+ record.lineage.ancestors
127
+ record.lineage.truncated?
128
+ ```
129
+
130
+ Core keeps at most 42 serialized ancestors when that accessor is used. Live
131
+ context inheritance still works across all active levels. The default formatter
132
+ omits lineage internals and keeps only public execution identity plus caller
133
+ fields.
134
+
135
+ Lineage is still available to processors. Processors may deliberately promote
136
+ selected relationship fields into output-facing
137
+ sections before formatting:
138
+
139
+ ```ruby
140
+ Julewire.configure do |config|
141
+ config.processors.use do |draft|
142
+ root_id = draft.lineage.root_reference&.fetch(:id, nil)
143
+ depth = draft.lineage.depth
144
+ ancestor_count = draft.lineage.ancestors.length
145
+
146
+ draft[:labels][:root_execution_id] = root_id if root_id
147
+ draft[:payload][:execution_depth] = depth if depth
148
+ draft[:payload][:ancestor_count] = ancestor_count
149
+ end
150
+ end
151
+ ```
152
+
153
+ That promotion is explicit policy. Core does not emit full lineage by default.
154
+
155
+ ## Raw by Policy
156
+
157
+ Core does not redact. Normalized records and propagation envelopes keep
158
+ application data raw, including `error.message`, `error.to_s`, payload fields,
159
+ context fields, and carry fields. Default formatting may omit carry, but it does
160
+ not sanitize values it does emit.
161
+
162
+ The built-in serializer is an error-pruning layer, not a privacy layer. It makes
163
+ values safe to format, caps worst cases, and avoids recursive crashes. It does
164
+ not decide which values are secrets.
165
+
166
+ Put redaction in processors or a separate policy gem before formatting.
167
+
168
+ ## Serializer Bounds
169
+
170
+ `Julewire::Serializer` applies:
171
+
172
+ - max nesting depth
173
+ - circular reference detection
174
+ - max string bytes
175
+ - max array items
176
+ - max hash keys
177
+ - invalid UTF-8 repair
178
+ - non-finite float sentinels
179
+ - non-primitive numeric strings for `BigDecimal`, `Rational`, `Complex`, etc.
180
+ - bounded exception backtraces and cause chains
181
+
182
+ Truncated containers, containers pruned by `max_depth`, and circular container
183
+ references get `_julewire_truncation` metadata. Serializer keys beginning with
184
+ `_julewire_` are reserved by convention for core metadata; user payloads must
185
+ not use that namespace. Public Julewire record contracts use symbol keys
186
+ internally. Payloads should also use one JSON field name per value; mixed key
187
+ types that stringify to the same JSON field are outside the serializer contract.
188
+ Long strings use a `...[Truncated]` suffix. User data that already contains that
189
+ suffix is preserved as user data.
190
+
191
+ Unknown objects collapse to safe class markers instead of dumping arbitrary
192
+ `inspect` output. If object serialization itself raises, the value becomes a
193
+ bounded marker such as `"[Unserializable: RuntimeError]"`.
194
+
195
+ Exceptions are shaped through `Julewire::Core::Serialization::ExceptionShape`
196
+ before encoding:
197
+
198
+ ```ruby
199
+ {
200
+ class: "RuntimeError",
201
+ message: "wrapper",
202
+ backtrace: ["app.rb:1:in ..."],
203
+ cause: {
204
+ class: "ArgumentError",
205
+ message: "root"
206
+ }
207
+ }
208
+ ```
209
+
210
+ Cause chains are bounded and cycle-safe.
211
+ Set `config.error_backtrace_lines = 0` to omit `backtrace` fields from
212
+ core-shaped exceptions in records and the default formatter output.
213
+ The same limit is applied when core receives a core-shaped error hash with
214
+ `backtrace` fields at the top level or inside nested `cause` hashes. This keeps
215
+ the contract consistent for integrations that pre-shape errors before handing
216
+ them to core.
217
+
218
+ ## Internal Error Records
219
+
220
+ When core can still format and write a replacement record, internal failures may
221
+ produce minimal records such as:
222
+
223
+ - `julewire.emit_error`
224
+ - `julewire.processor_error`
225
+
226
+ Those records intentionally omit original payloads. They include bounded,
227
+ serializer-scrubbed exception class details and safe record metadata such as
228
+ source, event, severity, logger, and labels when available. They do not include
229
+ raw exception messages by default. Use `on_failure` for local diagnostics when
230
+ you need the original exception object.
@@ -0,0 +1,45 @@
1
+ # Security and Wire Keys
2
+
3
+ Julewire separates safety, privacy, and propagation. Core bounds output shape
4
+ and contains logger-path failures. It does not redact application data by
5
+ itself; redaction is processor policy.
6
+
7
+ ## Trust Boundaries
8
+
9
+ Treat inbound carriers as trust-boundary data:
10
+
11
+ - Request headers are not restored automatically. Integrations choose explicit
12
+ outbound carry headers.
13
+ - Job carriers live inside serialized job data. That is usually an internal
14
+ queue boundary.
15
+ - Message headers can come from other producers. Use an integration-level
16
+ carrier filter when topics accept external producers.
17
+
18
+ Carry is baggage-shaped: small propagated facts, not log content. Keep large
19
+ payloads in record `payload` or `attributes`, where processors and formatters
20
+ can apply policy before output.
21
+
22
+ ## Capture and Redaction
23
+
24
+ Body and broad header capture are opt-in because they can contain secrets.
25
+ Framework parameter filtering may cover framework event payloads, but full
26
+ Julewire record filtering is a processor decision. Install a record-filtering
27
+ processor before enabling broad capture.
28
+
29
+ Health snapshots, internal error records, and invalid-severity diagnostics omit
30
+ raw exception messages and raw payloads. They carry safe coordinates such as
31
+ exception class, event, source, severity, phase, component, and timestamp.
32
+
33
+ ## Wire Keys
34
+
35
+ | Key | Location | Purpose |
36
+ | --- | --- | --- |
37
+ | `julewire` | Generic carrier key and message header | Serialized propagation carrier. |
38
+ | `julewire.carrier` | Serialized job data | Propagation carrier inside job payloads. |
39
+ | `neutral` | record section | Neutral formatter-coordination fields. Default JSON strips it. |
40
+ | `_julewire_truncation` | Serialized hashes | Truncation metadata inserted by bounded serializers/transforms. |
41
+
42
+ These names are similar by design, but they live at different boundaries:
43
+ wire carriers move context across processes, `neutral` coordinates formatters
44
+ inside one record, and `_julewire_truncation` reports output
45
+ bounding.
data/docs/tail.md ADDED
@@ -0,0 +1,91 @@
1
+ # Developer Tail
2
+
3
+ `Julewire::Tail` is a bounded in-memory destination for local inspection. It is
4
+ not delivery, buffering, or audit storage.
5
+
6
+ ```ruby
7
+ tail = Julewire::Tail.attach!(capacity: 200)
8
+
9
+ Julewire.info("booted", service: "api")
10
+
11
+ puts tail.render(limit: 20)
12
+ ```
13
+
14
+ `Julewire.tail` attaches a tail destination to the default runtime:
15
+
16
+ ```ruby
17
+ tail = Julewire.tail(capacity: 100)
18
+ ```
19
+
20
+ The tail stores public formatter output after Julewire's normal processors have
21
+ run. It omits hidden carry data and execution-lineage internals, and it applies
22
+ core serializer bounds before records enter the ring buffer. `tail.records`
23
+ returns frozen snapshots; `tail.render` returns one-line text; `tail.write(io)`
24
+ writes that text to an IO.
25
+
26
+ Use `Julewire::ConsoleFormatter` with `Julewire::TextEncoder` for normal
27
+ stdout text destinations. Tail uses the same text encoder for its rendered
28
+ view.
29
+
30
+ ## CLI Tail
31
+
32
+ `julewire tail` reads JSON logs from a file or stdin and renders compact text:
33
+
34
+ ```sh
35
+ julewire tail --format core log/development.jsonl
36
+ ```
37
+
38
+ Provider gems can register their own file decoders, so mixed platform stdout can
39
+ be tailed without teaching core about provider log envelopes:
40
+
41
+ ```sh
42
+ platform logs -f service/my-app | julewire tail --format provider_json --raw-invalid -
43
+ ```
44
+
45
+ Core decoders own Julewire's core JSON shape; provider gems own provider wire
46
+ shapes.
47
+
48
+ `julewire --version` prints the core CLI version.
49
+
50
+ Parsing is strict by default. Use `--skip-invalid` to ignore non-JSON lines or
51
+ `--raw-invalid` to print boot noise and platform lines unchanged while decoded
52
+ Julewire records are rendered normally.
53
+
54
+ `julewire transcode` uses the same provider-owned decoders, then writes another
55
+ registered output format. Core ships `core` JSON and `console` text; provider
56
+ gems can register their own output formats:
57
+
58
+ ```sh
59
+ platform logs service/my-app | julewire transcode --from provider_json --to core --raw-invalid -
60
+ ```
61
+
62
+ `Julewire::TextEncoder.new(theme: :punk)` is a high-contrast local console
63
+ style. `Julewire.punk!` configures a named runtime with that formatter/encoder
64
+ pair.
65
+
66
+ `julewire doctor --punk` renders the doctor report as loud terminal text. Plain
67
+ `julewire doctor` stays JSON for scripts.
68
+
69
+ `Julewire.dev!` configures the same console and attaches a tail in one core-only
70
+ call:
71
+
72
+ ```ruby
73
+ tail = Julewire.dev!(tail: { capacity: 500 })
74
+ ```
75
+
76
+ Use a custom formatter when the local view should differ from default output:
77
+
78
+ ```ruby
79
+ tail = Julewire::Tail.attach!(
80
+ formatter: ->(record) { { severity: record.fetch(:severity), message: record.fetch(:message) } }
81
+ )
82
+ ```
83
+
84
+ The tail is a normal custom destination. `Julewire.health` reports its size,
85
+ capacity, capture count, and formatter failures under the tail destination name.
86
+
87
+ `Julewire::TailSampling` is separate from the developer tail. It wraps another
88
+ destination and buffers records until an execution summary decides whether to
89
+ keep the execution. Errors and slow executions are kept; the rest are sampled
90
+ with Julewire's deterministic sampling hash. Pass `decider:` for a custom callable
91
+ policy; it receives the summary record and `key:`.
data/exe/julewire ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "julewire/core"
5
+
6
+ exit Julewire::Core::CLI.call
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/julewire/core/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "julewire-core"
7
+ spec.version = Julewire::Core::VERSION
8
+ spec.authors = ["Alexander Grebennik"]
9
+ spec.email = ["slbug@users.noreply.github.com", "sl.bug.sl@gmail.com"]
10
+
11
+ spec.summary = "Execution-scoped structured logging core for Ruby applications."
12
+ spec.description =
13
+ "Provider-neutral records, execution context, processors, destinations, " \
14
+ "and formatters for Julewire."
15
+ spec.homepage = "https://github.com/slbug/julewire"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 3.4"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/slbug/julewire/tree/main/gems/core"
20
+ spec.metadata["changelog_uri"] = "https://github.com/slbug/julewire/blob/main/gems/core/CHANGELOG.md"
21
+
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ spec.files = Dir.chdir(__dir__) do
25
+ Dir[
26
+ "CHANGELOG.md",
27
+ "LICENSE.txt",
28
+ "README.md",
29
+ "docs/**/*.md",
30
+ "exe/*",
31
+ "julewire-core.gemspec",
32
+ "lib/**/*.rb"
33
+ ]
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = ["julewire"]
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_dependency "concurrent-ruby", ">= 1.3"
40
+ spec.add_dependency "zeitwerk", ">= 2.8.1"
41
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Julewire
6
+ module Core
7
+ class CLI
8
+ class Doctor
9
+ FLAGS = {
10
+ "--color" => [:color, true],
11
+ "--json" => %i[format json],
12
+ "--no-color" => [:color, false],
13
+ "--plain" => %i[theme plain],
14
+ "--punk" => %i[theme punk],
15
+ "--text" => %i[format text]
16
+ }.freeze
17
+
18
+ def initialize(argv:, stdout:)
19
+ @argv = argv
20
+ @stdout = stdout
21
+ end
22
+
23
+ def call
24
+ options = doctor_options
25
+ report = Julewire.doctor
26
+ options.fetch(:format) == :json ? write_json(report) : write_text(report, options)
27
+ 0
28
+ end
29
+
30
+ private
31
+
32
+ def doctor_options
33
+ { color: color_output?, format: :json, theme: :plain }.tap do |options|
34
+ until @argv.empty?
35
+ value = @argv.shift
36
+ if (assignment = FLAGS[value])
37
+ apply_option(options, assignment)
38
+ else
39
+ raise ArgumentError, "unknown option #{value}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def apply_option(options, assignment)
46
+ key = assignment.fetch(0)
47
+ value = assignment.fetch(1)
48
+ options[key] = value
49
+ options[:format] = :text if key == :theme && value == :punk
50
+ end
51
+
52
+ def color_output?
53
+ @stdout.respond_to?(:tty?) && @stdout.tty?
54
+ end
55
+
56
+ def write_json(report)
57
+ @stdout.write(JSON.pretty_generate(report))
58
+ @stdout.write("\n")
59
+ end
60
+
61
+ def write_text(report, options)
62
+ theme = options.fetch(:theme)
63
+ color = options.fetch(:color)
64
+ lines = [
65
+ title(theme),
66
+ status_line(report, theme: theme, color: color),
67
+ runtime_line(report.fetch(:runtime)),
68
+ pipeline_line(report.fetch(:pipeline)),
69
+ component_line("destinations", report.dig(:pipeline, :destinations) || {}),
70
+ component_line("runtime_integrations", report.fetch(:integrations)),
71
+ component_line("process_integrations", report.fetch(:process_integrations)),
72
+ warning_lines(report.fetch(:warnings), theme: theme)
73
+ ].flatten.compact
74
+ @stdout.write("#{lines.join("\n")}\n")
75
+ end
76
+
77
+ def title(theme)
78
+ theme == :punk ? "!! JULEWIRE DOCTOR !!" : "Julewire Doctor"
79
+ end
80
+
81
+ def status_line(report, theme:, color:)
82
+ status = report.fetch(:status).to_s
83
+ label = theme == :punk ? punk_label(status) : "status=#{status}"
84
+ colorize(label, severity_for_status(status), color: color, theme: theme)
85
+ end
86
+
87
+ def runtime_line(runtime)
88
+ parts = [
89
+ "level=#{runtime.fetch(:level)}",
90
+ "generation=#{runtime.fetch(:generation)}",
91
+ "closed=#{runtime.fetch(:closed)}"
92
+ ]
93
+ "runtime #{parts.join(" ")}"
94
+ end
95
+
96
+ def pipeline_line(pipeline)
97
+ "pipeline configured=#{pipeline.fetch(:configured)} status=#{pipeline.fetch(:status)}"
98
+ end
99
+
100
+ def component_line(name, components)
101
+ return "#{name}=none" if components.empty?
102
+
103
+ values = components.map { |key, value| "#{key}:#{value.fetch(:status)}" }
104
+ "#{name}=#{values.join(",")}"
105
+ end
106
+
107
+ def warning_lines(warnings, theme:)
108
+ return "warnings=none" if warnings.empty?
109
+
110
+ header = theme == :punk ? "!! warnings=#{warnings.length}" : "warnings=#{warnings.length}"
111
+ [header, *warnings.map { warning_line(it, theme: theme) }]
112
+ end
113
+
114
+ def warning_line(warning, theme:)
115
+ prefix = theme == :punk ? "!!" : "-"
116
+ "#{prefix} #{warning.fetch(:code)}: #{warning.fetch(:message)}"
117
+ end
118
+
119
+ def punk_label(status)
120
+ glyph = Serialization::TextEncoder.punk_glyph(severity_for_status(status))
121
+ "#{glyph} status=#{status.upcase} #{glyph}"
122
+ end
123
+
124
+ def colorize(value, severity, color:, theme:)
125
+ return value unless color
126
+
127
+ styles = severity_styles(theme)
128
+ "\e[#{styles.fetch(severity.to_s, styles.fetch("unknown"))}m#{value}\e[0m"
129
+ end
130
+
131
+ def severity_for_status(status)
132
+ status == "ok" ? "info" : "error"
133
+ end
134
+
135
+ def severity_styles(theme)
136
+ return Serialization::TextEncoder::PUNK_SEVERITY_STYLES if theme == :punk
137
+
138
+ Serialization::TextEncoder::SEVERITY_STYLES
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ module LineHelpers
7
+ private
8
+
9
+ def parse_command_options(options, command:)
10
+ yield(options, @argv.shift) until @argv.empty?
11
+ raise ArgumentError, "#{command} log path is required" unless options[:path]
12
+
13
+ options
14
+ end
15
+
16
+ def next_symbol_option(name)
17
+ value = @argv.shift
18
+ raise ArgumentError, "#{name} value is required" unless value
19
+
20
+ value.to_sym
21
+ end
22
+
23
+ def positive_integer_option(name)
24
+ value = @argv.shift
25
+ raise ArgumentError, "#{name} value is required" unless value
26
+
27
+ Validation.validate_integer_limit!(Integer(value, 10), name: name.delete_prefix("--"), positive: true)
28
+ rescue ArgumentError
29
+ raise ArgumentError, "#{name} must be a positive integer"
30
+ end
31
+
32
+ def apply_path_option(options, value, command:)
33
+ raise ArgumentError, "unknown option #{value}" if value.start_with?("-") && value != "-"
34
+ raise ArgumentError, "#{command} accepts one log path" if options[:path]
35
+
36
+ options[:path] = value
37
+ end
38
+
39
+ def handle_invalid_line(line, error, mode)
40
+ case mode
41
+ when :skip
42
+ nil
43
+ when :raw
44
+ @stdout.write(raw_line(line))
45
+ else
46
+ raise error
47
+ end
48
+ end
49
+
50
+ def raw_line(line)
51
+ line.end_with?("\n") ? line : "#{line}\n"
52
+ end
53
+
54
+ def indexed_lines(lines)
55
+ lines.each_with_index.filter_map do |line, index|
56
+ [index + 1, line] unless line.strip.empty?
57
+ end
58
+ end
59
+
60
+ def console_text_encoder(options)
61
+ LogFormats::ConsoleText.new(
62
+ color: options.fetch(:color),
63
+ max_value_bytes: options.fetch(:max_value_bytes),
64
+ theme: options.fetch(:theme)
65
+ )
66
+ end
67
+
68
+ def write_encoded_record_line(line, line_number, input_format:, invalid:, encoder:)
69
+ record = LogFormats.record_from_json_line(line, line_number: line_number, format: input_format)
70
+ @stdout.write(encoder.call(record))
71
+ rescue ArgumentError => e
72
+ handle_invalid_line(line, e, invalid)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ module LogFormats
7
+ class ConsoleText
8
+ def initialize(color: false, max_value_bytes: Serialization::TextEncoder::DEFAULT_MAX_VALUE_BYTES,
9
+ theme: :plain)
10
+ @formatter = Records::ConsoleFormatter.new
11
+ @encoder = Serialization::TextEncoder.new(
12
+ color: color,
13
+ max_value_bytes: max_value_bytes,
14
+ theme: theme
15
+ )
16
+ end
17
+
18
+ def call(record)
19
+ @encoder.call(@formatter.call(record))
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end