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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/docs/advanced-configuration.md +2 -1
  4. data/docs/configuration.md +14 -7
  5. data/docs/context-and-propagation.md +26 -4
  6. data/docs/contracts.md +25 -50
  7. data/docs/extensions-and-api.md +87 -88
  8. data/docs/health-schema.md +12 -2
  9. data/docs/internals.md +24 -23
  10. data/docs/outputs-and-lifecycle.md +15 -8
  11. data/docs/records-and-data-policy.md +38 -7
  12. data/lib/julewire/core/cli/doctor.rb +20 -16
  13. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  14. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  15. data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
  16. data/lib/julewire/core/cli/log_formats.rb +9 -9
  17. data/lib/julewire/core/cli/tail.rb +19 -14
  18. data/lib/julewire/core/cli/transcode.rb +4 -5
  19. data/lib/julewire/core/cli.rb +1 -1
  20. data/lib/julewire/core/configuration.rb +1 -1
  21. data/lib/julewire/core/context_store.rb +56 -87
  22. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  23. data/lib/julewire/core/destinations/collection.rb +8 -16
  24. data/lib/julewire/core/destinations/definition.rb +1 -9
  25. data/lib/julewire/core/destinations/destination.rb +16 -66
  26. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  27. data/lib/julewire/core/destinations/registry.rb +1 -1
  28. data/lib/julewire/core/destinations/sink.rb +0 -1
  29. data/lib/julewire/core/destinations/synchronized_output.rb +54 -17
  30. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  31. data/lib/julewire/core/destinations/write_step.rb +5 -5
  32. data/lib/julewire/core/destinations.rb +0 -7
  33. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  34. data/lib/julewire/core/diagnostics/doctor.rb +22 -15
  35. data/lib/julewire/core/diagnostics/failure_snapshot.rb +9 -3
  36. data/lib/julewire/core/diagnostics/health.rb +41 -57
  37. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  38. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  39. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  40. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  41. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  42. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  43. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  44. data/lib/julewire/core/execution/boundary.rb +2 -8
  45. data/lib/julewire/core/execution/handle.rb +14 -17
  46. data/lib/julewire/core/execution/lineage.rb +26 -62
  47. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  48. data/lib/julewire/core/execution/scope.rb +17 -22
  49. data/lib/julewire/core/execution/scope_fields.rb +4 -13
  50. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  51. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  52. data/lib/julewire/core/execution/summary_state.rb +16 -19
  53. data/lib/julewire/core/execution/view.rb +8 -17
  54. data/lib/julewire/core/facade_methods.rb +67 -65
  55. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  56. data/lib/julewire/core/fields/bags.rb +23 -27
  57. data/lib/julewire/core/fields/field_set.rb +52 -16
  58. data/lib/julewire/core/fields/field_stack.rb +69 -109
  59. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  60. data/lib/julewire/core/fields/internal.rb +35 -26
  61. data/lib/julewire/core/fields/lookup.rb +9 -13
  62. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  63. data/lib/julewire/core/fields/stack_set.rb +9 -14
  64. data/lib/julewire/core/fields/static_labels.rb +1 -1
  65. data/lib/julewire/core/integration/configurable.rb +4 -3
  66. data/lib/julewire/core/integration/destination_health.rb +28 -7
  67. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  68. data/lib/julewire/core/integration/facade.rb +12 -15
  69. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  70. data/lib/julewire/core/integration/health.rb +0 -3
  71. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  72. data/lib/julewire/core/integration/protocol.rb +20 -0
  73. data/lib/julewire/core/integration/settings.rb +4 -4
  74. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  75. data/lib/julewire/core/integration/subscription.rb +3 -2
  76. data/lib/julewire/core/integration/values.rb +0 -17
  77. data/lib/julewire/core/local_storage.rb +3 -19
  78. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  79. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  80. data/lib/julewire/core/processing/match.rb +6 -4
  81. data/lib/julewire/core/processing/pipeline.rb +46 -40
  82. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  83. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  84. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  85. data/lib/julewire/core/processing/sampling.rb +1 -3
  86. data/lib/julewire/core/processing.rb +2 -2
  87. data/lib/julewire/core/propagation/carrier.rb +73 -19
  88. data/lib/julewire/core/propagation.rb +18 -4
  89. data/lib/julewire/core/records/build_input.rb +75 -0
  90. data/lib/julewire/core/records/console_formatter.rb +6 -6
  91. data/lib/julewire/core/records/deconstruct.rb +1 -1
  92. data/lib/julewire/core/records/display_message.rb +2 -4
  93. data/lib/julewire/core/records/draft.rb +127 -258
  94. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  95. data/lib/julewire/core/records/metadata.rb +4 -3
  96. data/lib/julewire/core/records/public_projection.rb +2 -2
  97. data/lib/julewire/core/records/raw_input.rb +1 -1
  98. data/lib/julewire/core/records/record.rb +30 -18
  99. data/lib/julewire/core/records/severity.rb +7 -15
  100. data/lib/julewire/core/runtime.rb +87 -72
  101. data/lib/julewire/core/runtime_registry.rb +20 -15
  102. data/lib/julewire/core/runtime_state.rb +5 -3
  103. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  104. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  105. data/lib/julewire/core/serialization/bounded_transform.rb +3 -2
  106. data/lib/julewire/core/serialization/bounded_traversal.rb +72 -83
  107. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  108. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  109. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  110. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  111. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  112. data/lib/julewire/core/serialization/serializer.rb +22 -49
  113. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  114. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  115. data/lib/julewire/core/serialization/truncation_metadata.rb +157 -0
  116. data/lib/julewire/core/serialization/value_copy.rb +183 -109
  117. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  118. data/lib/julewire/core/testing.rb +6 -56
  119. data/lib/julewire/core/validation.rb +2 -3
  120. data/lib/julewire/core/version.rb +1 -1
  121. data/lib/julewire/core.rb +2 -7
  122. metadata +7 -19
  123. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  124. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  125. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  126. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  127. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  128. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  129. data/lib/julewire/core/testing/chaos.rb +0 -80
  130. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  131. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  132. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  133. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  134. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  135. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  136. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  137. data/lib/julewire/core/testing/contracts.rb +0 -24
  138. data/lib/julewire/core/testing/coverage.rb +0 -58
  139. 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
- Writes are serialized where state transitions need serialization. Main-ractor
15
- hot-path reads use the current frozen runtime state without taking a global
16
- runtime lock; state transitions swap that reference under the runtime state
17
- mutex. Health and counters use their own synchronization outside that mutex.
18
- Non-main ractors use ractor-local runtime storage because they cannot touch the
19
- main runtime object directly.
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. `Fields::FieldSet`
71
- owns trusted field-bag copying, merging, and key normalization after values have
72
- entered core. `Fields::Lookup` is for display reads that tolerate symbol/string
73
- keys and return `nil` for unreadable inputs.
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
- `Scheduling::DeadlineScheduler` is the small stdlib timer heap. Main-process
94
- diagnostics and framework integrations use `Scheduling::SharedScheduler` so
95
- they do not each keep a background timer thread. Ractor integrations keep their
96
- own schedulers because worker ractors cannot share the main scheduler object.
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. It is not a public
104
- application API.
105
-
106
- ## Test Seams
107
-
108
- Some private methods are intentionally reachable through `Julewire::Testing`.
109
- They reset process-global registries or storage that normal applications should
110
- not touch directly.
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 outputs with one mutex. Concurrent emitters do not interleave
78
- individual encoded records, but a slow sink blocks the emitting thread.
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 mutexes inherited from the parent process.
150
- It also forwards `after_fork!` to destinations and outputs that implement it,
151
- then runs integration after-fork hooks registered through core. File, socket,
152
- queue, and async transports should reopen worker-local resources from their
153
- destination or output `after_fork!` method.
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. 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.
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 by convention for core metadata; user payloads must
185
- not use that namespace. Public Julewire record contracts use symbol keys
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.fetch(:format) == :json ? write_json(report) : write_text(report, 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
- { 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
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 key == :theme && value == :punk
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.fetch(:theme)
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(report.fetch(:pipeline)),
69
- component_line("destinations", report.dig(:pipeline, :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
- ].flatten.compact
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, *warnings.map { warning_line(it, theme: theme) }]
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.to_s, styles.fetch("unknown"))}m#{value}\e[0m"
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
- yield(options, @argv.shift) until @argv.empty?
11
- raise ArgumentError, "#{command} log path is required" unless options[:path]
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
- 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"
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[:path]
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, error, mode)
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 error
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] unless line.strip.empty?
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 => e
72
- handle_invalid_line(line, e, invalid)
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["timestamp"],
29
- severity: Records::Severity.normalize(source["severity"] || :info),
30
- kind: RecordDecoder.kind(source["kind"] || :point),
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"],
@@ -17,7 +17,7 @@ module Julewire
17
17
  def section(value)
18
18
  return {} unless value.is_a?(Hash)
19
19
 
20
- Fields::FieldSet.deep_symbolize_keys(value)
20
+ Fields::FieldSet.deep_symbolize_owned_keys(value)
21
21
  end
22
22
 
23
23
  def sections(source, sections: Fields::Bags.record_hash_sections)
@@ -11,17 +11,17 @@ module Julewire
11
11
  @entries = []
12
12
 
13
13
  class << self
14
- def register(name, decoder: nil, encoder: nil, priority: 0)
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.to_i
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.zero? && existing ? existing.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
- Records::Record.from_normalized_hash(decode(payload, format: format))
46
+ Record.from_normalized_hash(decode(payload, format: format))
47
47
  rescue JSON::ParserError => e
48
- raise ArgumentError, "line #{line_number}: invalid JSON: #{e.message}"
48
+ raise ArgumentError, "line #{line_number}: invalid JSON: #{e}"
49
49
  rescue TypeError, ArgumentError => e
50
- raise ArgumentError, "line #{line_number}: #{e.message}"
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.to_s.match?(FORMAT_NAME_PATTERN)
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 load(name)
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
- load(name)
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
- if (assignment = FLAGS[value])
58
+ case value
59
+ when *FLAGS.keys
60
+ assignment = FLAGS.fetch(value)
59
61
  options[assignment.fetch(0)] = assignment.fetch(1)
60
- elsif value.start_with?("--format=")
61
- options[:format] = value.delete_prefix("--format=").to_sym
62
- elsif value == "--format"
62
+ when /\A--format=(.+)\z/
63
+ options[:format] = Regexp.last_match(1)
64
+ when "--format"
63
65
  options[:format] = next_symbol_option("--format")
64
- elsif value == "--theme"
66
+ when "--theme"
65
67
  options[:theme] = next_symbol_option("--theme")
66
- elsif value == "--limit"
68
+ when "--limit"
67
69
  options[:limit] = positive_integer_option("--limit")
68
- elsif value == "--max-value-bytes"
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
- proc do |line, line_number|
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
- return render_limited_stdin(limit, renderer) if limit
91
-
92
- render_stream(@stdin.each_line, renderer)
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
- File.open(options.fetch(:path), "r") do |file|
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 if line.strip.empty?
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=").to_sym
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
- apply_path_option(options, value, command: "transcode")
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
- return indexed_lines(@stdin.each_line).each(&) if path == "-"
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
@@ -31,7 +31,7 @@ module Julewire
31
31
  rescue Interrupt
32
32
  INTERRUPTED_STATUS
33
33
  rescue ArgumentError, Errno::ENOENT => e
34
- fail_with(e.message)
34
+ fail_with(e)
35
35
  end
36
36
 
37
37
  private
@@ -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, Core::MAX_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)