julewire-core 1.0.1 → 1.1.1

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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/docs/configuration.md +14 -7
  4. data/docs/context-and-propagation.md +20 -0
  5. data/docs/contracts.md +23 -49
  6. data/docs/extensions-and-api.md +78 -82
  7. data/docs/health-schema.md +12 -2
  8. data/docs/internals.md +21 -20
  9. data/docs/outputs-and-lifecycle.md +10 -6
  10. data/docs/records-and-data-policy.md +36 -5
  11. data/lib/julewire/core/cli/doctor.rb +20 -16
  12. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  13. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  14. data/lib/julewire/core/cli/log_formats.rb +9 -9
  15. data/lib/julewire/core/cli/tail.rb +19 -14
  16. data/lib/julewire/core/cli/transcode.rb +4 -5
  17. data/lib/julewire/core/cli.rb +1 -1
  18. data/lib/julewire/core/configuration.rb +1 -1
  19. data/lib/julewire/core/context_store.rb +51 -87
  20. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  21. data/lib/julewire/core/destinations/collection.rb +8 -16
  22. data/lib/julewire/core/destinations/definition.rb +1 -9
  23. data/lib/julewire/core/destinations/destination.rb +13 -63
  24. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  25. data/lib/julewire/core/destinations/registry.rb +1 -1
  26. data/lib/julewire/core/destinations/sink.rb +0 -1
  27. data/lib/julewire/core/destinations/synchronized_output.rb +14 -13
  28. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  29. data/lib/julewire/core/destinations/write_step.rb +5 -5
  30. data/lib/julewire/core/destinations.rb +0 -7
  31. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  32. data/lib/julewire/core/diagnostics/doctor.rb +16 -9
  33. data/lib/julewire/core/diagnostics/failure_snapshot.rb +6 -2
  34. data/lib/julewire/core/diagnostics/health.rb +41 -57
  35. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  36. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  37. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  38. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  39. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  40. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  41. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  42. data/lib/julewire/core/execution/boundary.rb +2 -8
  43. data/lib/julewire/core/execution/handle.rb +12 -17
  44. data/lib/julewire/core/execution/lineage.rb +16 -56
  45. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  46. data/lib/julewire/core/execution/scope.rb +15 -20
  47. data/lib/julewire/core/execution/scope_fields.rb +3 -12
  48. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  49. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  50. data/lib/julewire/core/execution/summary_state.rb +16 -19
  51. data/lib/julewire/core/execution/view.rb +8 -17
  52. data/lib/julewire/core/facade_methods.rb +67 -65
  53. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  54. data/lib/julewire/core/fields/bags.rb +23 -27
  55. data/lib/julewire/core/fields/field_set.rb +29 -19
  56. data/lib/julewire/core/fields/field_stack.rb +68 -134
  57. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  58. data/lib/julewire/core/fields/internal.rb +19 -38
  59. data/lib/julewire/core/fields/lookup.rb +9 -13
  60. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  61. data/lib/julewire/core/fields/stack_set.rb +7 -12
  62. data/lib/julewire/core/fields/static_labels.rb +1 -1
  63. data/lib/julewire/core/integration/configurable.rb +4 -3
  64. data/lib/julewire/core/integration/destination_health.rb +17 -7
  65. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  66. data/lib/julewire/core/integration/facade.rb +12 -15
  67. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  68. data/lib/julewire/core/integration/health.rb +0 -3
  69. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  70. data/lib/julewire/core/integration/protocol.rb +20 -0
  71. data/lib/julewire/core/integration/settings.rb +4 -4
  72. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  73. data/lib/julewire/core/integration/subscription.rb +3 -2
  74. data/lib/julewire/core/integration/values.rb +0 -17
  75. data/lib/julewire/core/local_storage.rb +3 -19
  76. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  77. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  78. data/lib/julewire/core/processing/match.rb +6 -4
  79. data/lib/julewire/core/processing/pipeline.rb +46 -40
  80. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  81. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  82. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  83. data/lib/julewire/core/processing/sampling.rb +1 -3
  84. data/lib/julewire/core/processing.rb +2 -2
  85. data/lib/julewire/core/propagation/carrier.rb +14 -14
  86. data/lib/julewire/core/propagation.rb +15 -2
  87. data/lib/julewire/core/records/build_input.rb +75 -0
  88. data/lib/julewire/core/records/console_formatter.rb +6 -6
  89. data/lib/julewire/core/records/deconstruct.rb +1 -1
  90. data/lib/julewire/core/records/display_message.rb +2 -4
  91. data/lib/julewire/core/records/draft.rb +125 -262
  92. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  93. data/lib/julewire/core/records/metadata.rb +4 -3
  94. data/lib/julewire/core/records/public_projection.rb +2 -2
  95. data/lib/julewire/core/records/raw_input.rb +1 -1
  96. data/lib/julewire/core/records/record.rb +19 -37
  97. data/lib/julewire/core/records/severity.rb +7 -15
  98. data/lib/julewire/core/runtime.rb +67 -71
  99. data/lib/julewire/core/runtime_registry.rb +20 -15
  100. data/lib/julewire/core/runtime_state.rb +5 -3
  101. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  102. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  103. data/lib/julewire/core/serialization/bounded_transform.rb +2 -12
  104. data/lib/julewire/core/serialization/bounded_traversal.rb +49 -66
  105. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  106. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  107. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  108. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  109. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  110. data/lib/julewire/core/serialization/serializer.rb +23 -50
  111. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  112. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  113. data/lib/julewire/core/serialization/truncation_metadata.rb +63 -48
  114. data/lib/julewire/core/serialization/value_copy.rb +60 -227
  115. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  116. data/lib/julewire/core/testing.rb +6 -56
  117. data/lib/julewire/core/validation.rb +2 -3
  118. data/lib/julewire/core/version.rb +1 -1
  119. data/lib/julewire/core.rb +2 -7
  120. metadata +6 -19
  121. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  122. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  123. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  124. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  125. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  126. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  127. data/lib/julewire/core/testing/chaos.rb +0 -80
  128. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  129. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  130. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  131. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  132. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  133. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  134. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  135. data/lib/julewire/core/testing/contracts.rb +0 -24
  136. data/lib/julewire/core/testing/coverage.rb +0 -58
  137. data/lib/julewire/core/testing/test_reports.rb +0 -78
@@ -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
@@ -149,11 +152,12 @@ out, `Julewire.close` returns `false`, but later emits still drop as
149
152
  `runtime_closed` until the next `configure` or `reset!`.
150
153
 
151
154
  `Julewire.after_fork!` resets process-local counters, failure snapshots,
152
- current context, warning state, and mutexes inherited from the parent process.
153
- It also forwards `after_fork!` to destinations and outputs that implement it,
154
- then runs integration after-fork hooks registered through core. File, socket,
155
- queue, and async transports should reopen worker-local resources from their
156
- 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.
157
161
 
158
162
  Core does not install an `at_exit` hook. Small scripts should call close from
159
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
@@ -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"],
@@ -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)