julewire-core 1.0.0 → 1.1.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 +4 -4
- data/CHANGELOG.md +29 -0
- data/docs/advanced-configuration.md +2 -1
- data/docs/configuration.md +14 -7
- data/docs/context-and-propagation.md +26 -4
- data/docs/contracts.md +25 -50
- data/docs/extensions-and-api.md +87 -88
- data/docs/health-schema.md +12 -2
- data/docs/internals.md +24 -23
- data/docs/outputs-and-lifecycle.md +15 -8
- data/docs/records-and-data-policy.md +38 -7
- data/lib/julewire/core/cli/doctor.rb +20 -16
- data/lib/julewire/core/cli/line_helpers.rb +23 -12
- data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
- data/lib/julewire/core/cli/log_formats.rb +9 -9
- data/lib/julewire/core/cli/tail.rb +19 -14
- data/lib/julewire/core/cli/transcode.rb +4 -5
- data/lib/julewire/core/cli.rb +1 -1
- data/lib/julewire/core/configuration.rb +1 -1
- data/lib/julewire/core/context_store.rb +56 -87
- data/lib/julewire/core/destinations/chaos_output.rb +2 -2
- data/lib/julewire/core/destinations/collection.rb +8 -16
- data/lib/julewire/core/destinations/definition.rb +1 -9
- data/lib/julewire/core/destinations/destination.rb +16 -66
- data/lib/julewire/core/destinations/processor_handling.rb +78 -0
- data/lib/julewire/core/destinations/registry.rb +1 -1
- data/lib/julewire/core/destinations/sink.rb +0 -1
- data/lib/julewire/core/destinations/synchronized_output.rb +54 -17
- data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
- data/lib/julewire/core/destinations/write_step.rb +5 -5
- data/lib/julewire/core/destinations.rb +0 -7
- data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
- data/lib/julewire/core/diagnostics/doctor.rb +22 -15
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +9 -3
- data/lib/julewire/core/diagnostics/health.rb +41 -57
- data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
- data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
- data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
- data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
- data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
- data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
- data/lib/julewire/core/diagnostics/tail.rb +18 -22
- data/lib/julewire/core/execution/boundary.rb +2 -8
- data/lib/julewire/core/execution/handle.rb +14 -17
- data/lib/julewire/core/execution/lineage.rb +26 -62
- data/lib/julewire/core/execution/measurement_handle.rb +6 -10
- data/lib/julewire/core/execution/scope.rb +17 -22
- data/lib/julewire/core/execution/scope_fields.rb +4 -13
- data/lib/julewire/core/execution/scope_identity.rb +1 -9
- data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
- data/lib/julewire/core/execution/summary_state.rb +16 -19
- data/lib/julewire/core/execution/view.rb +8 -17
- data/lib/julewire/core/facade_methods.rb +67 -65
- data/lib/julewire/core/fields/attribute_keys.rb +0 -2
- data/lib/julewire/core/fields/bags.rb +23 -27
- data/lib/julewire/core/fields/field_set.rb +52 -16
- data/lib/julewire/core/fields/field_stack.rb +69 -109
- data/lib/julewire/core/fields/internal/deletion.rb +6 -9
- data/lib/julewire/core/fields/internal.rb +35 -26
- data/lib/julewire/core/fields/lookup.rb +9 -13
- data/lib/julewire/core/fields/section_proxy.rb +1 -9
- data/lib/julewire/core/fields/stack_set.rb +9 -14
- data/lib/julewire/core/fields/static_labels.rb +1 -1
- data/lib/julewire/core/integration/configurable.rb +4 -3
- data/lib/julewire/core/integration/destination_health.rb +28 -7
- data/lib/julewire/core/integration/event_subscriber.rb +1 -1
- data/lib/julewire/core/integration/facade.rb +12 -15
- data/lib/julewire/core/integration/fork_hooks.rb +12 -36
- data/lib/julewire/core/integration/health.rb +0 -3
- data/lib/julewire/core/integration/ivar_state.rb +0 -5
- data/lib/julewire/core/integration/protocol.rb +20 -0
- data/lib/julewire/core/integration/settings.rb +4 -4
- data/lib/julewire/core/integration/subscriber_install.rb +2 -2
- data/lib/julewire/core/integration/subscription.rb +3 -2
- data/lib/julewire/core/integration/values.rb +0 -17
- data/lib/julewire/core/local_storage.rb +3 -19
- data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
- data/lib/julewire/core/processing/level_threshold.rb +7 -14
- data/lib/julewire/core/processing/match.rb +6 -4
- data/lib/julewire/core/processing/pipeline.rb +46 -40
- data/lib/julewire/core/processing/processor_chain.rb +22 -11
- data/lib/julewire/core/processing/processor_registry.rb +3 -4
- data/lib/julewire/core/processing/record_field_transform.rb +0 -4
- data/lib/julewire/core/processing/sampling.rb +1 -3
- data/lib/julewire/core/processing.rb +2 -2
- data/lib/julewire/core/propagation/carrier.rb +73 -19
- data/lib/julewire/core/propagation.rb +18 -4
- data/lib/julewire/core/records/build_input.rb +75 -0
- data/lib/julewire/core/records/console_formatter.rb +6 -6
- data/lib/julewire/core/records/deconstruct.rb +1 -1
- data/lib/julewire/core/records/display_message.rb +2 -4
- data/lib/julewire/core/records/draft.rb +127 -258
- data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
- data/lib/julewire/core/records/metadata.rb +4 -3
- data/lib/julewire/core/records/public_projection.rb +2 -2
- data/lib/julewire/core/records/raw_input.rb +1 -1
- data/lib/julewire/core/records/record.rb +30 -18
- data/lib/julewire/core/records/severity.rb +7 -15
- data/lib/julewire/core/runtime.rb +87 -72
- data/lib/julewire/core/runtime_registry.rb +20 -15
- data/lib/julewire/core/runtime_state.rb +5 -3
- data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
- data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
- data/lib/julewire/core/serialization/bounded_transform.rb +3 -2
- data/lib/julewire/core/serialization/bounded_traversal.rb +72 -83
- data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
- data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
- data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
- data/lib/julewire/core/serialization/exception_shape.rb +7 -6
- data/lib/julewire/core/serialization/json_encoder.rb +10 -13
- data/lib/julewire/core/serialization/serializer.rb +22 -49
- data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
- data/lib/julewire/core/serialization/text_encoder.rb +5 -5
- data/lib/julewire/core/serialization/truncation_metadata.rb +157 -0
- data/lib/julewire/core/serialization/value_copy.rb +183 -109
- data/lib/julewire/core/serialization/value_traversal.rb +10 -127
- data/lib/julewire/core/testing.rb +6 -56
- data/lib/julewire/core/validation.rb +2 -3
- data/lib/julewire/core/version.rb +1 -1
- data/lib/julewire/core.rb +2 -7
- metadata +7 -19
- data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
- data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
- data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
- data/lib/julewire/core/testing/chaos/destination.rb +0 -55
- data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
- data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
- data/lib/julewire/core/testing/chaos.rb +0 -80
- data/lib/julewire/core/testing/contracts/component.rb +0 -162
- data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
- data/lib/julewire/core/testing/contracts/integration.rb +0 -166
- data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
- data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
- data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
- data/lib/julewire/core/testing/contracts/wire.rb +0 -60
- data/lib/julewire/core/testing/contracts.rb +0 -24
- data/lib/julewire/core/testing/coverage.rb +0 -58
- data/lib/julewire/core/testing/test_reports.rb +0 -78
data/docs/internals.md
CHANGED
|
@@ -11,12 +11,14 @@ The runtime stores immutable snapshots of:
|
|
|
11
11
|
- pipeline
|
|
12
12
|
- whether the active pipeline has been closed
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
Main-ractor hot-path reads use the current frozen runtime state without taking
|
|
15
|
+
a global runtime lock. State transitions replace that snapshot atomically;
|
|
16
|
+
configure and reset are additionally serialized with the configure mutex while
|
|
17
|
+
close may overlap them. The successful atomic transition owns the previous
|
|
18
|
+
pipeline lifecycle, so concurrent close/reset operations cannot close the same
|
|
19
|
+
pipeline twice. Health and counters use their own synchronization. Non-main
|
|
20
|
+
ractors use ractor-local runtime storage because they cannot touch the main
|
|
21
|
+
runtime object directly.
|
|
20
22
|
|
|
21
23
|
Configuration, reconfigure, reset, and close are state transitions. They should
|
|
22
24
|
stay boring and explicit.
|
|
@@ -67,10 +69,15 @@ extensions do not pay repeated equivalent-key scans.
|
|
|
67
69
|
|
|
68
70
|
`Records::RawInput` reads app-facing emit input before record construction.
|
|
69
71
|
`Integration::Values::Read` extracts best-effort values from framework objects.
|
|
70
|
-
`Integration::Values::Shape` normalizes adapter-built hashes
|
|
71
|
-
owns
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
`Integration::Values::Shape` normalizes adapter-built hashes at framework
|
|
73
|
+
ingress. `Fields::FieldSet` owns public field-bag copying, merging, and key
|
|
74
|
+
normalization at application or framework ingress. Once data is owned,
|
|
75
|
+
`Integration::Protocol` and strict deep validation require recursively Symbol-
|
|
76
|
+
keyed hashes; cross-gem code does not repair String keys. `Fields::Lookup` is
|
|
77
|
+
for best-effort display reads with the exact requested key and returns `nil` for
|
|
78
|
+
unreadable inputs; it does not bridge Symbol/String key forms inside normalized
|
|
79
|
+
records. Presentation code that reads decoded wire payloads uses its separate
|
|
80
|
+
`wire_value` helper at that restore boundary.
|
|
74
81
|
|
|
75
82
|
## Context Storage
|
|
76
83
|
|
|
@@ -90,24 +97,18 @@ custom destination objects.
|
|
|
90
97
|
|
|
91
98
|
## Scheduling
|
|
92
99
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
Main-process diagnostics and framework integrations use the process-owned
|
|
101
|
+
`Scheduling::SharedScheduler` so they do not each keep a background timer
|
|
102
|
+
thread. Ractor integrations own their stdlib timeout threads because a worker
|
|
103
|
+
Ractor cannot access the main scheduler's concurrent-ruby objects.
|
|
97
104
|
|
|
98
105
|
## Remote Envelope Hook
|
|
99
106
|
|
|
100
|
-
Core keeps `Runtime#emit_envelope(input:, context:, attributes:, carry:, neutral:, scope:, enforce_level:)`
|
|
107
|
+
Core keeps `Runtime#emit_envelope(input:, context:, attributes:, carry:, neutral:, scope:, enforce_level:, owned:)`
|
|
101
108
|
for bridge code. The bridge reconstructs the scope snapshot, and core rebuilds
|
|
102
109
|
a normal record from input, context, attributes, carry, neutral, and that
|
|
103
|
-
snapshot before emitting through the active pipeline.
|
|
104
|
-
application API.
|
|
105
|
-
|
|
106
|
-
## Test Seams
|
|
107
|
-
|
|
108
|
-
Some private methods are intentionally reachable through `Julewire::Testing`.
|
|
109
|
-
They reset process-global registries or storage that normal applications should
|
|
110
|
-
not touch directly.
|
|
110
|
+
snapshot before emitting through the active pipeline. Bridges pass `owned: true`
|
|
111
|
+
only for Julewire-owned wire data. It is not a public application API.
|
|
111
112
|
|
|
112
113
|
## Emit Entrypoints
|
|
113
114
|
|
|
@@ -40,7 +40,10 @@ strings.
|
|
|
40
40
|
|
|
41
41
|
Processors may intentionally return `:drop`. Core counts those records in
|
|
42
42
|
`health[:pipeline][:counts][:processor_dropped]`; processor failures are counted
|
|
43
|
-
separately as `processor_error`.
|
|
43
|
+
separately as `processor_error`. Unsupported non-`nil`/non-draft return values
|
|
44
|
+
keep the current draft and increment `processor_invalid`. A processor that
|
|
45
|
+
leaves an invalid draft also increments `processor_invalid`, but that record is
|
|
46
|
+
dropped before the next processor and also increments `processor_dropped`.
|
|
44
47
|
|
|
45
48
|
Async queues, files, fanout appenders, batching, retries, acknowledgements,
|
|
46
49
|
reopen, rotation, shutdown hooks, and delivery policy belong in custom
|
|
@@ -74,8 +77,11 @@ for `$stdout`, caller-owned loggers, and shared objects.
|
|
|
74
77
|
|
|
75
78
|
## Synchronous Output
|
|
76
79
|
|
|
77
|
-
Core wraps direct
|
|
78
|
-
individual encoded records, but a slow sink blocks the emitting
|
|
80
|
+
Core wraps direct output writes with one mutex. Concurrent emitters do not
|
|
81
|
+
interleave individual encoded records, but a slow sink blocks the emitting
|
|
82
|
+
thread. `flush` is lifecycle-only and may overlap writes so slow flushes do not
|
|
83
|
+
stall the emit path; direct outputs should be IO-like or internally safe for
|
|
84
|
+
flush/write overlap. `close` is terminal and serialized with writes.
|
|
79
85
|
|
|
80
86
|
A synchronous output failure is contained. The destination records output
|
|
81
87
|
failure counters and calls `on_failure`. Core does not retry, back off, or
|
|
@@ -146,11 +152,12 @@ out, `Julewire.close` returns `false`, but later emits still drop as
|
|
|
146
152
|
`runtime_closed` until the next `configure` or `reset!`.
|
|
147
153
|
|
|
148
154
|
`Julewire.after_fork!` resets process-local counters, failure snapshots,
|
|
149
|
-
current context, warning state, and
|
|
150
|
-
It also forwards `after_fork!` to destinations
|
|
151
|
-
then runs integration after-fork hooks
|
|
152
|
-
queue, and async transports should
|
|
153
|
-
destination or output `after_fork!`
|
|
155
|
+
current context, warning state, schedulers, and registries that cannot be
|
|
156
|
+
shared with the child process. It also forwards `after_fork!` to destinations
|
|
157
|
+
and outputs that implement it, then runs integration after-fork hooks
|
|
158
|
+
registered through core. File, socket, queue, and async transports should
|
|
159
|
+
reopen worker-local resources from their destination or output `after_fork!`
|
|
160
|
+
method.
|
|
154
161
|
|
|
155
162
|
Core does not install an `at_exit` hook. Small scripts should call close from
|
|
156
163
|
their own shutdown path:
|
|
@@ -27,10 +27,11 @@ string or symbol keys defensively. Processors receive a mutable
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Raw input is normalized through `Julewire::RecordDraft.build`, then processors
|
|
30
|
-
mutate the draft owned by the current emit.
|
|
31
|
-
|
|
32
|
-
before destinations and formatters see it.
|
|
33
|
-
|
|
30
|
+
mutate the draft owned by the current emit. Core validates the draft after
|
|
31
|
+
every processor, then freezes it through `Julewire::RecordDraft#to_record`
|
|
32
|
+
before destinations and formatters see it. A malformed processor result is
|
|
33
|
+
attributed to that processor and never reaches the next one. Immutable
|
|
34
|
+
`Record` objects are not the public raw-input construction API.
|
|
34
35
|
|
|
35
36
|
`Julewire::RecordFormatter` turns that normalized record into a public log
|
|
36
37
|
projection. It omits internal keys such as `:carry` and execution lineage
|
|
@@ -59,6 +60,11 @@ source/event metadata when available. Level filtering then applies normally, so
|
|
|
59
60
|
an invalid explicit severity can still be dropped when `config.level` is above
|
|
60
61
|
`:info`. Configuration severities remain strict.
|
|
61
62
|
|
|
63
|
+
Severity Symbols are the canonical lowercase values `:debug`, `:info`, `:warn`,
|
|
64
|
+
`:error`, `:fatal`, and `:unknown`. String ingress is case-insensitive and
|
|
65
|
+
standard-library Logger severity integers are accepted. Non-canonical Symbols
|
|
66
|
+
are invalid rather than silently rewritten at an internal or cross-gem boundary.
|
|
67
|
+
|
|
62
68
|
## Structured Sections
|
|
63
69
|
|
|
64
70
|
Structured sections prefer hashes:
|
|
@@ -87,13 +93,38 @@ Julewire.emit(payload: "raw")
|
|
|
87
93
|
```
|
|
88
94
|
|
|
89
95
|
Runtime mutation helpers are forgiving for the same reason: logging should not
|
|
90
|
-
crash the app.
|
|
96
|
+
crash the app when a positional field value is not a hash.
|
|
91
97
|
|
|
92
98
|
String keys inside structured sections are normalized to symbols before
|
|
93
99
|
processors and destinations run. Encoders and custom destinations may convert
|
|
94
100
|
the record to string-key payloads, but the core Ruby record contract stays
|
|
95
101
|
symbol-keyed.
|
|
96
102
|
|
|
103
|
+
## Key Boundaries
|
|
104
|
+
|
|
105
|
+
Public application input and decoded propagation envelopes may use String or
|
|
106
|
+
Symbol keys. Core converts String keys to Symbols once as those values enter
|
|
107
|
+
core. Other key types are invalid; they are never stringified or silently
|
|
108
|
+
accepted.
|
|
109
|
+
|
|
110
|
+
Integration-owned, bridge-owned, processor-owned, and normalized-record sections
|
|
111
|
+
are different: each section must be a Hash and every recursive hash key must
|
|
112
|
+
already be a Symbol. They are strict contracts, not another normalization
|
|
113
|
+
boundary. Core does not convert their String keys. JSON carrier extraction is
|
|
114
|
+
the explicit exception because JSON decode is wire ingress and necessarily
|
|
115
|
+
produces String keys.
|
|
116
|
+
|
|
117
|
+
Direct constructors and integration field APIs raise `TypeError` for a non-Hash
|
|
118
|
+
section or invalid key. Strict owned validation visits every recursive
|
|
119
|
+
container iteratively, including data deeper than the later copy bound. A
|
|
120
|
+
normal `emit` contains such a failure: it drops the invalid
|
|
121
|
+
record, records the failure in health and `on_failure`, and does not raise into
|
|
122
|
+
application code. Integration input is validated recursively before draft
|
|
123
|
+
construction or any configured processor. Unknown integration top-level fields
|
|
124
|
+
are errors instead of being ignored; integrations put custom data in
|
|
125
|
+
`:payload`, `:attributes`, or another documented section. An unconfigured
|
|
126
|
+
pipeline drops before record construction, as it does for every emit.
|
|
127
|
+
|
|
97
128
|
## Optional Metadata
|
|
98
129
|
|
|
99
130
|
`logger` is the logical logger entrypoint, for example `"app"` or
|
|
@@ -181,8 +212,8 @@ Put redaction in processors or a separate policy gem before formatting.
|
|
|
181
212
|
|
|
182
213
|
Truncated containers, containers pruned by `max_depth`, and circular container
|
|
183
214
|
references get `_julewire_truncation` metadata. Serializer keys beginning with
|
|
184
|
-
`_julewire_` are reserved
|
|
185
|
-
|
|
215
|
+
`_julewire_` are reserved for core metadata; public field ingress rejects the
|
|
216
|
+
truncation marker as a user key. Public Julewire record contracts use symbol keys
|
|
186
217
|
internally. Payloads should also use one JSON field name per value; mixed key
|
|
187
218
|
types that stringify to the same JSON field are outside the serializer contract.
|
|
188
219
|
Long strings use a `...[Truncated]` suffix. User data that already contains that
|
|
@@ -23,21 +23,22 @@ module Julewire
|
|
|
23
23
|
def call
|
|
24
24
|
options = doctor_options
|
|
25
25
|
report = Julewire.doctor
|
|
26
|
-
options
|
|
26
|
+
if options[:format] == :text
|
|
27
|
+
write_text(report, options)
|
|
28
|
+
else
|
|
29
|
+
write_json(report)
|
|
30
|
+
end
|
|
27
31
|
0
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
private
|
|
31
35
|
|
|
32
36
|
def doctor_options
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
else
|
|
39
|
-
raise ArgumentError, "unknown option #{value}"
|
|
40
|
-
end
|
|
37
|
+
@argv.each_with_object(color: color_output?) do |value, options|
|
|
38
|
+
if (assignment = FLAGS[value])
|
|
39
|
+
apply_option(options, assignment)
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "unknown option #{value}"
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
end
|
|
@@ -46,7 +47,7 @@ module Julewire
|
|
|
46
47
|
key = assignment.fetch(0)
|
|
47
48
|
value = assignment.fetch(1)
|
|
48
49
|
options[key] = value
|
|
49
|
-
options[:format] = :text if
|
|
50
|
+
options[:format] = :text if value == :punk
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def color_output?
|
|
@@ -59,18 +60,19 @@ module Julewire
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def write_text(report, options)
|
|
62
|
-
theme = options
|
|
63
|
+
theme = options[:theme]
|
|
63
64
|
color = options.fetch(:color)
|
|
65
|
+
pipeline = report.fetch(:pipeline)
|
|
64
66
|
lines = [
|
|
65
67
|
title(theme),
|
|
66
68
|
status_line(report, theme: theme, color: color),
|
|
67
69
|
runtime_line(report.fetch(:runtime)),
|
|
68
|
-
pipeline_line(
|
|
69
|
-
component_line("destinations",
|
|
70
|
+
pipeline_line(pipeline),
|
|
71
|
+
component_line("destinations", pipeline.fetch(:destinations)),
|
|
70
72
|
component_line("runtime_integrations", report.fetch(:integrations)),
|
|
71
73
|
component_line("process_integrations", report.fetch(:process_integrations)),
|
|
72
74
|
warning_lines(report.fetch(:warnings), theme: theme)
|
|
73
|
-
]
|
|
75
|
+
]
|
|
74
76
|
@stdout.write("#{lines.join("\n")}\n")
|
|
75
77
|
end
|
|
76
78
|
|
|
@@ -108,7 +110,9 @@ module Julewire
|
|
|
108
110
|
return "warnings=none" if warnings.empty?
|
|
109
111
|
|
|
110
112
|
header = theme == :punk ? "!! warnings=#{warnings.length}" : "warnings=#{warnings.length}"
|
|
111
|
-
[header
|
|
113
|
+
lines = [header]
|
|
114
|
+
warnings.each { lines << warning_line(it, theme: theme) }
|
|
115
|
+
lines
|
|
112
116
|
end
|
|
113
117
|
|
|
114
118
|
def warning_line(warning, theme:)
|
|
@@ -125,7 +129,7 @@ module Julewire
|
|
|
125
129
|
return value unless color
|
|
126
130
|
|
|
127
131
|
styles = severity_styles(theme)
|
|
128
|
-
"\e[#{styles.fetch(severity
|
|
132
|
+
"\e[#{styles.fetch(severity)}m#{value}\e[0m"
|
|
129
133
|
end
|
|
130
134
|
|
|
131
135
|
def severity_for_status(status)
|
|
@@ -7,8 +7,12 @@ module Julewire
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def parse_command_options(options, command:)
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
@argv.length.times do
|
|
11
|
+
break if @argv.empty?
|
|
12
|
+
|
|
13
|
+
yield(options, @argv.shift)
|
|
14
|
+
end
|
|
15
|
+
raise ArgumentError, "#{command} log path is required" unless options.fetch(:path)
|
|
12
16
|
|
|
13
17
|
options
|
|
14
18
|
end
|
|
@@ -24,26 +28,33 @@ module Julewire
|
|
|
24
28
|
value = @argv.shift
|
|
25
29
|
raise ArgumentError, "#{name} value is required" unless value
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
begin
|
|
32
|
+
integer = Integer(value, 10)
|
|
33
|
+
raise ArgumentError unless integer.positive?
|
|
34
|
+
|
|
35
|
+
integer
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
raise ArgumentError, "#{name} must be a positive integer"
|
|
38
|
+
end
|
|
30
39
|
end
|
|
31
40
|
|
|
32
41
|
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
|
|
42
|
+
raise ArgumentError, "unknown option #{value}" if value.start_with?("-") && !value.eql?("-")
|
|
43
|
+
raise ArgumentError, "#{command} accepts one log path" if options.fetch(:path)
|
|
35
44
|
|
|
36
45
|
options[:path] = value
|
|
37
46
|
end
|
|
38
47
|
|
|
39
|
-
def handle_invalid_line(line,
|
|
48
|
+
def handle_invalid_line(line, mode)
|
|
40
49
|
case mode
|
|
41
50
|
when :skip
|
|
42
51
|
nil
|
|
43
52
|
when :raw
|
|
44
53
|
@stdout.write(raw_line(line))
|
|
54
|
+
when :fail
|
|
55
|
+
raise
|
|
45
56
|
else
|
|
46
|
-
raise
|
|
57
|
+
raise ArgumentError, "invalid line policy must be fail, raw, or skip"
|
|
47
58
|
end
|
|
48
59
|
end
|
|
49
60
|
|
|
@@ -53,7 +64,7 @@ module Julewire
|
|
|
53
64
|
|
|
54
65
|
def indexed_lines(lines)
|
|
55
66
|
lines.each_with_index.filter_map do |line, index|
|
|
56
|
-
[index + 1, line]
|
|
67
|
+
[index + 1, line] if line.match?(/\S/)
|
|
57
68
|
end
|
|
58
69
|
end
|
|
59
70
|
|
|
@@ -68,8 +79,8 @@ module Julewire
|
|
|
68
79
|
def write_encoded_record_line(line, line_number, input_format:, invalid:, encoder:)
|
|
69
80
|
record = LogFormats.record_from_json_line(line, line_number: line_number, format: input_format)
|
|
70
81
|
@stdout.write(encoder.call(record))
|
|
71
|
-
rescue ArgumentError
|
|
72
|
-
handle_invalid_line(line,
|
|
82
|
+
rescue ArgumentError
|
|
83
|
+
handle_invalid_line(line, invalid)
|
|
73
84
|
end
|
|
74
85
|
end
|
|
75
86
|
end
|
|
@@ -25,9 +25,9 @@ module Julewire
|
|
|
25
25
|
|
|
26
26
|
def record_base(source)
|
|
27
27
|
{
|
|
28
|
-
timestamp: source
|
|
29
|
-
severity: Records::Severity.normalize(source
|
|
30
|
-
kind: RecordDecoder.kind(source
|
|
28
|
+
timestamp: source.fetch("timestamp"),
|
|
29
|
+
severity: Records::Severity.normalize(source.fetch("severity")),
|
|
30
|
+
kind: RecordDecoder.kind(source.fetch("kind")),
|
|
31
31
|
event: source["event"],
|
|
32
32
|
message: source["message"],
|
|
33
33
|
logger: source["logger"],
|
|
@@ -11,17 +11,17 @@ module Julewire
|
|
|
11
11
|
@entries = []
|
|
12
12
|
|
|
13
13
|
class << self
|
|
14
|
-
def register(name, decoder: nil, encoder: nil, priority:
|
|
14
|
+
def register(name, decoder: nil, encoder: nil, priority: nil)
|
|
15
15
|
name = normalize(name)
|
|
16
16
|
validate_component(decoder, :decoder) if decoder
|
|
17
17
|
validate_component(encoder, :encoder) if encoder
|
|
18
18
|
existing = @entries.find { it.name == name }
|
|
19
|
-
priority = priority.
|
|
19
|
+
priority = priority.nil? ? existing&.priority || 0 : Integer(priority)
|
|
20
20
|
entry = Entry.new(
|
|
21
21
|
name: name,
|
|
22
22
|
decoder: decoder || existing&.decoder,
|
|
23
23
|
encoder: encoder || existing&.encoder,
|
|
24
|
-
priority: priority
|
|
24
|
+
priority: priority
|
|
25
25
|
)
|
|
26
26
|
@entries = @entries.reject { it.name == name } + [entry]
|
|
27
27
|
entry
|
|
@@ -43,21 +43,21 @@ module Julewire
|
|
|
43
43
|
|
|
44
44
|
def record_from_json_line(line, line_number:, format: AUTO_FORMAT)
|
|
45
45
|
payload = JSON.parse(line)
|
|
46
|
-
|
|
46
|
+
Record.from_normalized_hash(decode(payload, format: format))
|
|
47
47
|
rescue JSON::ParserError => e
|
|
48
|
-
raise ArgumentError, "line #{line_number}: invalid JSON: #{e
|
|
48
|
+
raise ArgumentError, "line #{line_number}: invalid JSON: #{e}"
|
|
49
49
|
rescue TypeError, ArgumentError => e
|
|
50
|
-
raise ArgumentError, "line #{line_number}: #{e
|
|
50
|
+
raise ArgumentError, "line #{line_number}: #{e}"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def normalize(value)
|
|
54
54
|
name = Core.normalize_name(value, name: "log format")
|
|
55
|
-
return name if name.
|
|
55
|
+
return name if name.match?(FORMAT_NAME_PATTERN)
|
|
56
56
|
|
|
57
57
|
raise ArgumentError, "log format must contain lowercase letters, digits, or underscores"
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def
|
|
60
|
+
def load_gem_format(name)
|
|
61
61
|
path = "julewire/#{name}"
|
|
62
62
|
require path
|
|
63
63
|
rescue LoadError => e
|
|
@@ -99,7 +99,7 @@ module Julewire
|
|
|
99
99
|
def load_format(name)
|
|
100
100
|
return if @entries.any? { it.name == name }
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
load_gem_format(name)
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def decode_entries
|
|
@@ -55,17 +55,19 @@ module Julewire
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def apply_tail_option(options, value)
|
|
58
|
-
|
|
58
|
+
case value
|
|
59
|
+
when *FLAGS.keys
|
|
60
|
+
assignment = FLAGS.fetch(value)
|
|
59
61
|
options[assignment.fetch(0)] = assignment.fetch(1)
|
|
60
|
-
|
|
61
|
-
options[:format] =
|
|
62
|
-
|
|
62
|
+
when /\A--format=(.+)\z/
|
|
63
|
+
options[:format] = Regexp.last_match(1)
|
|
64
|
+
when "--format"
|
|
63
65
|
options[:format] = next_symbol_option("--format")
|
|
64
|
-
|
|
66
|
+
when "--theme"
|
|
65
67
|
options[:theme] = next_symbol_option("--theme")
|
|
66
|
-
|
|
68
|
+
when "--limit"
|
|
67
69
|
options[:limit] = positive_integer_option("--limit")
|
|
68
|
-
|
|
70
|
+
when "--max-value-bytes"
|
|
69
71
|
options[:max_value_bytes] = positive_integer_option("--max-value-bytes")
|
|
70
72
|
else
|
|
71
73
|
apply_path_option(options, value, command: "tail")
|
|
@@ -74,7 +76,7 @@ module Julewire
|
|
|
74
76
|
|
|
75
77
|
def tail_renderer(options)
|
|
76
78
|
encoder = console_text_encoder(options)
|
|
77
|
-
|
|
79
|
+
lambda do |line, line_number|
|
|
78
80
|
write_encoded_record_line(
|
|
79
81
|
line,
|
|
80
82
|
line_number,
|
|
@@ -87,13 +89,16 @@ module Julewire
|
|
|
87
89
|
|
|
88
90
|
def tail_stdin(options, renderer)
|
|
89
91
|
limit = options.fetch(:limit)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
if limit
|
|
93
|
+
render_limited_stdin(limit, renderer)
|
|
94
|
+
else
|
|
95
|
+
render_stream(@stdin.each_line, renderer)
|
|
96
|
+
end
|
|
93
97
|
end
|
|
94
98
|
|
|
95
99
|
def tail_file(options, renderer)
|
|
96
|
-
|
|
100
|
+
descriptor = IO.sysopen(options.fetch(:path))
|
|
101
|
+
IO.open(descriptor) do |file|
|
|
97
102
|
line_number = render_file_snapshot(file, options, renderer)
|
|
98
103
|
follow_file(file, line_number, options, renderer) if options.fetch(:follow)
|
|
99
104
|
end
|
|
@@ -113,7 +118,7 @@ module Julewire
|
|
|
113
118
|
render_entries([[line_number, line]], renderer)
|
|
114
119
|
else
|
|
115
120
|
line_number = reset_follow_position(file) if file.stat.size < file.pos
|
|
116
|
-
sleep(options.fetch(:poll_interval))
|
|
121
|
+
Kernel.sleep(options.fetch(:poll_interval))
|
|
117
122
|
end
|
|
118
123
|
end
|
|
119
124
|
end
|
|
@@ -132,7 +137,7 @@ module Julewire
|
|
|
132
137
|
line_number = 0
|
|
133
138
|
lines.each do |line|
|
|
134
139
|
line_number += 1
|
|
135
|
-
next
|
|
140
|
+
next unless line.match?(/\S/)
|
|
136
141
|
|
|
137
142
|
renderer.call(line, line_number)
|
|
138
143
|
end
|
|
@@ -69,7 +69,7 @@ module Julewire
|
|
|
69
69
|
|
|
70
70
|
def apply_named_option(options, value)
|
|
71
71
|
if value.start_with?("--from=")
|
|
72
|
-
options[:from] = value.delete_prefix("--from=")
|
|
72
|
+
options[:from] = value.delete_prefix("--from=")
|
|
73
73
|
elsif value.start_with?("--to=")
|
|
74
74
|
options[:to] = value.delete_prefix("--to=").to_sym
|
|
75
75
|
else
|
|
@@ -84,7 +84,7 @@ module Julewire
|
|
|
84
84
|
when "--theme" then options[:theme] = next_symbol_option("--theme")
|
|
85
85
|
when "--max-value-bytes" then options[:max_value_bytes] = positive_integer_option("--max-value-bytes")
|
|
86
86
|
else
|
|
87
|
-
|
|
87
|
+
raise ArgumentError, "unknown option #{value}"
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -95,9 +95,8 @@ module Julewire
|
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def each_entry(path, &)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
File.open(path, "r") { |file| indexed_lines(file.each_line).each(&) }
|
|
98
|
+
lines = path == "-" ? @stdin.each_line : File.foreach(path)
|
|
99
|
+
indexed_lines(lines).each(&)
|
|
101
100
|
end
|
|
102
101
|
end
|
|
103
102
|
end
|
data/lib/julewire/core/cli.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Julewire
|
|
|
24
24
|
reject_unknown_options!(options)
|
|
25
25
|
@destinations = options.fetch(:destinations) { Destinations::Registry.new }
|
|
26
26
|
@emit_non_standard_exception_summaries = options.fetch(:emit_non_standard_exception_summaries, false)
|
|
27
|
-
@error_backtrace_lines = options.fetch(:error_backtrace_lines,
|
|
27
|
+
@error_backtrace_lines = options.fetch(:error_backtrace_lines, MAX_BACKTRACE_LINES)
|
|
28
28
|
@labels = options.fetch(:labels) { Fields::StaticLabels.new }
|
|
29
29
|
@level = options.fetch(:level, :debug)
|
|
30
30
|
@on_drop = options.fetch(:on_drop, nil)
|