julewire-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/docs/advanced-configuration.md +66 -0
- data/docs/attribute-keys.md +74 -0
- data/docs/configuration.md +327 -0
- data/docs/context-and-propagation.md +353 -0
- data/docs/contracts.md +211 -0
- data/docs/development.md +49 -0
- data/docs/extensions-and-api.md +567 -0
- data/docs/health-schema.md +104 -0
- data/docs/instrumentation-cheatsheet.md +29 -0
- data/docs/internals.md +135 -0
- data/docs/outputs-and-lifecycle.md +206 -0
- data/docs/quickstart.md +133 -0
- data/docs/record-sources.md +17 -0
- data/docs/records-and-data-policy.md +230 -0
- data/docs/security-and-wire.md +45 -0
- data/docs/tail.md +91 -0
- data/exe/julewire +6 -0
- data/julewire-core.gemspec +41 -0
- data/lib/julewire/core/cli/doctor.rb +143 -0
- data/lib/julewire/core/cli/line_helpers.rb +77 -0
- data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
- data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
- data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
- data/lib/julewire/core/cli/log_formats.rb +123 -0
- data/lib/julewire/core/cli/tail.rb +153 -0
- data/lib/julewire/core/cli/transcode.rb +105 -0
- data/lib/julewire/core/cli.rb +73 -0
- data/lib/julewire/core/configuration.rb +99 -0
- data/lib/julewire/core/context_store.rb +384 -0
- data/lib/julewire/core/destinations/chaos_output.rb +91 -0
- data/lib/julewire/core/destinations/collection.rb +177 -0
- data/lib/julewire/core/destinations/definition.rb +125 -0
- data/lib/julewire/core/destinations/destination.rb +268 -0
- data/lib/julewire/core/destinations/registry.rb +81 -0
- data/lib/julewire/core/destinations/sink.rb +35 -0
- data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
- data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
- data/lib/julewire/core/destinations/write_step.rb +119 -0
- data/lib/julewire/core/destinations.rb +33 -0
- data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
- data/lib/julewire/core/diagnostics/doctor.rb +114 -0
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
- data/lib/julewire/core/diagnostics/health.rb +144 -0
- data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
- data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
- data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
- data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
- data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
- data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
- data/lib/julewire/core/diagnostics/tail.rb +168 -0
- data/lib/julewire/core/diagnostics.rb +8 -0
- data/lib/julewire/core/error.rb +7 -0
- data/lib/julewire/core/execution/boundary.rb +106 -0
- data/lib/julewire/core/execution/handle.rb +77 -0
- data/lib/julewire/core/execution/lineage.rb +192 -0
- data/lib/julewire/core/execution/measurement_handle.rb +28 -0
- data/lib/julewire/core/execution/no_current_error.rb +9 -0
- data/lib/julewire/core/execution/scope.rb +246 -0
- data/lib/julewire/core/execution/scope_fields.rb +76 -0
- data/lib/julewire/core/execution/scope_identity.rb +71 -0
- data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
- data/lib/julewire/core/execution/summary_state.rb +206 -0
- data/lib/julewire/core/execution/view.rb +56 -0
- data/lib/julewire/core/facade_methods.rb +181 -0
- data/lib/julewire/core/fields/attribute_keys.rb +54 -0
- data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
- data/lib/julewire/core/fields/bags.rb +123 -0
- data/lib/julewire/core/fields/carry_proxy.rb +22 -0
- data/lib/julewire/core/fields/context_proxy.rb +11 -0
- data/lib/julewire/core/fields/field_set.rb +78 -0
- data/lib/julewire/core/fields/field_stack.rb +269 -0
- data/lib/julewire/core/fields/internal/deletion.rb +68 -0
- data/lib/julewire/core/fields/internal.rb +87 -0
- data/lib/julewire/core/fields/lookup.rb +35 -0
- data/lib/julewire/core/fields/section_proxy.rb +88 -0
- data/lib/julewire/core/fields/stack_set.rb +69 -0
- data/lib/julewire/core/fields/static_labels.rb +43 -0
- data/lib/julewire/core/fields/summary_proxy.rb +62 -0
- data/lib/julewire/core/integration/configurable.rb +52 -0
- data/lib/julewire/core/integration/destination_health.rb +43 -0
- data/lib/julewire/core/integration/event_subscriber.rb +62 -0
- data/lib/julewire/core/integration/facade.rb +131 -0
- data/lib/julewire/core/integration/fork_hooks.rb +79 -0
- data/lib/julewire/core/integration/health.rb +41 -0
- data/lib/julewire/core/integration/ivar_state.rb +38 -0
- data/lib/julewire/core/integration/lifecycle.rb +22 -0
- data/lib/julewire/core/integration/scoped.rb +34 -0
- data/lib/julewire/core/integration/settings.rb +92 -0
- data/lib/julewire/core/integration/subscriber_install.rb +39 -0
- data/lib/julewire/core/integration/subscription.rb +29 -0
- data/lib/julewire/core/integration/values.rb +192 -0
- data/lib/julewire/core/lifecycle_error.rb +7 -0
- data/lib/julewire/core/local_storage.rb +91 -0
- data/lib/julewire/core/processing/level_threshold.rb +53 -0
- data/lib/julewire/core/processing/match.rb +74 -0
- data/lib/julewire/core/processing/pipeline.rb +360 -0
- data/lib/julewire/core/processing/processor_chain.rb +69 -0
- data/lib/julewire/core/processing/processor_registry.rb +115 -0
- data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
- data/lib/julewire/core/processing/record_field_transform.rb +124 -0
- data/lib/julewire/core/processing/sampling.rb +109 -0
- data/lib/julewire/core/processing.rb +41 -0
- data/lib/julewire/core/propagation/carrier.rb +93 -0
- data/lib/julewire/core/propagation.rb +50 -0
- data/lib/julewire/core/records/console_formatter.rb +24 -0
- data/lib/julewire/core/records/deconstruct.rb +19 -0
- data/lib/julewire/core/records/display_message.rb +166 -0
- data/lib/julewire/core/records/draft.rb +576 -0
- data/lib/julewire/core/records/formatter.rb +14 -0
- data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
- data/lib/julewire/core/records/metadata.rb +23 -0
- data/lib/julewire/core/records/public_projection.rb +51 -0
- data/lib/julewire/core/records/raw_input.rb +41 -0
- data/lib/julewire/core/records/record.rb +175 -0
- data/lib/julewire/core/records/severity.rb +44 -0
- data/lib/julewire/core/runtime.rb +515 -0
- data/lib/julewire/core/runtime_locator.rb +20 -0
- data/lib/julewire/core/runtime_registry.rb +48 -0
- data/lib/julewire/core/runtime_state.rb +39 -0
- data/lib/julewire/core/scheduling/deadline.rb +24 -0
- data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
- data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
- data/lib/julewire/core/sentinel.rb +18 -0
- data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
- data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
- data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
- data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
- data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
- data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
- data/lib/julewire/core/serialization/exception_shape.rb +88 -0
- data/lib/julewire/core/serialization/json_encoder.rb +69 -0
- data/lib/julewire/core/serialization/serializer.rb +233 -0
- data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
- data/lib/julewire/core/serialization/text_encoder.rb +147 -0
- data/lib/julewire/core/serialization/value_copy.rb +209 -0
- data/lib/julewire/core/serialization/value_traversal.rb +150 -0
- data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
- data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
- data/lib/julewire/core/testing/chaos/destination.rb +55 -0
- data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
- data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
- data/lib/julewire/core/testing/chaos.rb +80 -0
- data/lib/julewire/core/testing/contracts/component.rb +162 -0
- data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
- data/lib/julewire/core/testing/contracts/integration.rb +166 -0
- data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
- data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
- data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
- data/lib/julewire/core/testing/contracts/wire.rb +60 -0
- data/lib/julewire/core/testing/contracts.rb +24 -0
- data/lib/julewire/core/testing/coverage.rb +58 -0
- data/lib/julewire/core/testing/test_reports.rb +78 -0
- data/lib/julewire/core/testing.rb +122 -0
- data/lib/julewire/core/validation.rb +69 -0
- data/lib/julewire/core/version.rb +7 -0
- data/lib/julewire/core.rb +80 -0
- data/lib/julewire/error.rb +5 -0
- data/lib/julewire-core.rb +3 -0
- metadata +237 -0
|
@@ -0,0 +1,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,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
|