julewire-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/docs/advanced-configuration.md +66 -0
  6. data/docs/attribute-keys.md +74 -0
  7. data/docs/configuration.md +327 -0
  8. data/docs/context-and-propagation.md +353 -0
  9. data/docs/contracts.md +211 -0
  10. data/docs/development.md +49 -0
  11. data/docs/extensions-and-api.md +567 -0
  12. data/docs/health-schema.md +104 -0
  13. data/docs/instrumentation-cheatsheet.md +29 -0
  14. data/docs/internals.md +135 -0
  15. data/docs/outputs-and-lifecycle.md +206 -0
  16. data/docs/quickstart.md +133 -0
  17. data/docs/record-sources.md +17 -0
  18. data/docs/records-and-data-policy.md +230 -0
  19. data/docs/security-and-wire.md +45 -0
  20. data/docs/tail.md +91 -0
  21. data/exe/julewire +6 -0
  22. data/julewire-core.gemspec +41 -0
  23. data/lib/julewire/core/cli/doctor.rb +143 -0
  24. data/lib/julewire/core/cli/line_helpers.rb +77 -0
  25. data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
  26. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
  27. data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
  28. data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
  29. data/lib/julewire/core/cli/log_formats.rb +123 -0
  30. data/lib/julewire/core/cli/tail.rb +153 -0
  31. data/lib/julewire/core/cli/transcode.rb +105 -0
  32. data/lib/julewire/core/cli.rb +73 -0
  33. data/lib/julewire/core/configuration.rb +99 -0
  34. data/lib/julewire/core/context_store.rb +384 -0
  35. data/lib/julewire/core/destinations/chaos_output.rb +91 -0
  36. data/lib/julewire/core/destinations/collection.rb +177 -0
  37. data/lib/julewire/core/destinations/definition.rb +125 -0
  38. data/lib/julewire/core/destinations/destination.rb +268 -0
  39. data/lib/julewire/core/destinations/registry.rb +81 -0
  40. data/lib/julewire/core/destinations/sink.rb +35 -0
  41. data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
  42. data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
  43. data/lib/julewire/core/destinations/write_step.rb +119 -0
  44. data/lib/julewire/core/destinations.rb +33 -0
  45. data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
  46. data/lib/julewire/core/diagnostics/doctor.rb +114 -0
  47. data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
  48. data/lib/julewire/core/diagnostics/health.rb +144 -0
  49. data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
  50. data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
  51. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
  52. data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
  53. data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
  54. data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
  55. data/lib/julewire/core/diagnostics/tail.rb +168 -0
  56. data/lib/julewire/core/diagnostics.rb +8 -0
  57. data/lib/julewire/core/error.rb +7 -0
  58. data/lib/julewire/core/execution/boundary.rb +106 -0
  59. data/lib/julewire/core/execution/handle.rb +77 -0
  60. data/lib/julewire/core/execution/lineage.rb +192 -0
  61. data/lib/julewire/core/execution/measurement_handle.rb +28 -0
  62. data/lib/julewire/core/execution/no_current_error.rb +9 -0
  63. data/lib/julewire/core/execution/scope.rb +246 -0
  64. data/lib/julewire/core/execution/scope_fields.rb +76 -0
  65. data/lib/julewire/core/execution/scope_identity.rb +71 -0
  66. data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
  67. data/lib/julewire/core/execution/summary_state.rb +206 -0
  68. data/lib/julewire/core/execution/view.rb +56 -0
  69. data/lib/julewire/core/facade_methods.rb +181 -0
  70. data/lib/julewire/core/fields/attribute_keys.rb +54 -0
  71. data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
  72. data/lib/julewire/core/fields/bags.rb +123 -0
  73. data/lib/julewire/core/fields/carry_proxy.rb +22 -0
  74. data/lib/julewire/core/fields/context_proxy.rb +11 -0
  75. data/lib/julewire/core/fields/field_set.rb +78 -0
  76. data/lib/julewire/core/fields/field_stack.rb +269 -0
  77. data/lib/julewire/core/fields/internal/deletion.rb +68 -0
  78. data/lib/julewire/core/fields/internal.rb +87 -0
  79. data/lib/julewire/core/fields/lookup.rb +35 -0
  80. data/lib/julewire/core/fields/section_proxy.rb +88 -0
  81. data/lib/julewire/core/fields/stack_set.rb +69 -0
  82. data/lib/julewire/core/fields/static_labels.rb +43 -0
  83. data/lib/julewire/core/fields/summary_proxy.rb +62 -0
  84. data/lib/julewire/core/integration/configurable.rb +52 -0
  85. data/lib/julewire/core/integration/destination_health.rb +43 -0
  86. data/lib/julewire/core/integration/event_subscriber.rb +62 -0
  87. data/lib/julewire/core/integration/facade.rb +131 -0
  88. data/lib/julewire/core/integration/fork_hooks.rb +79 -0
  89. data/lib/julewire/core/integration/health.rb +41 -0
  90. data/lib/julewire/core/integration/ivar_state.rb +38 -0
  91. data/lib/julewire/core/integration/lifecycle.rb +22 -0
  92. data/lib/julewire/core/integration/scoped.rb +34 -0
  93. data/lib/julewire/core/integration/settings.rb +92 -0
  94. data/lib/julewire/core/integration/subscriber_install.rb +39 -0
  95. data/lib/julewire/core/integration/subscription.rb +29 -0
  96. data/lib/julewire/core/integration/values.rb +192 -0
  97. data/lib/julewire/core/lifecycle_error.rb +7 -0
  98. data/lib/julewire/core/local_storage.rb +91 -0
  99. data/lib/julewire/core/processing/level_threshold.rb +53 -0
  100. data/lib/julewire/core/processing/match.rb +74 -0
  101. data/lib/julewire/core/processing/pipeline.rb +360 -0
  102. data/lib/julewire/core/processing/processor_chain.rb +69 -0
  103. data/lib/julewire/core/processing/processor_registry.rb +115 -0
  104. data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
  105. data/lib/julewire/core/processing/record_field_transform.rb +124 -0
  106. data/lib/julewire/core/processing/sampling.rb +109 -0
  107. data/lib/julewire/core/processing.rb +41 -0
  108. data/lib/julewire/core/propagation/carrier.rb +93 -0
  109. data/lib/julewire/core/propagation.rb +50 -0
  110. data/lib/julewire/core/records/console_formatter.rb +24 -0
  111. data/lib/julewire/core/records/deconstruct.rb +19 -0
  112. data/lib/julewire/core/records/display_message.rb +166 -0
  113. data/lib/julewire/core/records/draft.rb +576 -0
  114. data/lib/julewire/core/records/formatter.rb +14 -0
  115. data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
  116. data/lib/julewire/core/records/metadata.rb +23 -0
  117. data/lib/julewire/core/records/public_projection.rb +51 -0
  118. data/lib/julewire/core/records/raw_input.rb +41 -0
  119. data/lib/julewire/core/records/record.rb +175 -0
  120. data/lib/julewire/core/records/severity.rb +44 -0
  121. data/lib/julewire/core/runtime.rb +515 -0
  122. data/lib/julewire/core/runtime_locator.rb +20 -0
  123. data/lib/julewire/core/runtime_registry.rb +48 -0
  124. data/lib/julewire/core/runtime_state.rb +39 -0
  125. data/lib/julewire/core/scheduling/deadline.rb +24 -0
  126. data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
  127. data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
  128. data/lib/julewire/core/sentinel.rb +18 -0
  129. data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
  130. data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
  131. data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
  132. data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
  133. data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
  134. data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
  135. data/lib/julewire/core/serialization/exception_shape.rb +88 -0
  136. data/lib/julewire/core/serialization/json_encoder.rb +69 -0
  137. data/lib/julewire/core/serialization/serializer.rb +233 -0
  138. data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
  139. data/lib/julewire/core/serialization/text_encoder.rb +147 -0
  140. data/lib/julewire/core/serialization/value_copy.rb +209 -0
  141. data/lib/julewire/core/serialization/value_traversal.rb +150 -0
  142. data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
  143. data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
  144. data/lib/julewire/core/testing/chaos/destination.rb +55 -0
  145. data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
  146. data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
  147. data/lib/julewire/core/testing/chaos.rb +80 -0
  148. data/lib/julewire/core/testing/contracts/component.rb +162 -0
  149. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
  150. data/lib/julewire/core/testing/contracts/integration.rb +166 -0
  151. data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
  152. data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
  153. data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
  154. data/lib/julewire/core/testing/contracts/wire.rb +60 -0
  155. data/lib/julewire/core/testing/contracts.rb +24 -0
  156. data/lib/julewire/core/testing/coverage.rb +58 -0
  157. data/lib/julewire/core/testing/test_reports.rb +78 -0
  158. data/lib/julewire/core/testing.rb +122 -0
  159. data/lib/julewire/core/validation.rb +69 -0
  160. data/lib/julewire/core/version.rb +7 -0
  161. data/lib/julewire/core.rb +80 -0
  162. data/lib/julewire/error.rb +5 -0
  163. data/lib/julewire-core.rb +3 -0
  164. metadata +237 -0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ module LogFormats
7
+ module CoreJsonDecoder
8
+ class << self
9
+ CORE_KINDS = {
10
+ "point" => true,
11
+ "summary" => true
12
+ }.freeze
13
+
14
+ def match?(payload)
15
+ payload.key?("timestamp") &&
16
+ payload.key?("severity") &&
17
+ CORE_KINDS.key?(payload["kind"].to_s)
18
+ end
19
+
20
+ def call(payload)
21
+ record_base(payload).merge(record_sections(payload), error: RecordDecoder.error(payload["error"]))
22
+ end
23
+
24
+ private
25
+
26
+ def record_base(source)
27
+ {
28
+ timestamp: source["timestamp"],
29
+ severity: Records::Severity.normalize(source["severity"] || :info),
30
+ kind: RecordDecoder.kind(source["kind"] || :point),
31
+ event: source["event"],
32
+ message: source["message"],
33
+ logger: source["logger"],
34
+ source: source["source"]
35
+ }
36
+ end
37
+
38
+ def record_sections(source)
39
+ RecordDecoder.sections(source)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ module LogFormats
7
+ module CoreJsonEncoder
8
+ class << self
9
+ def call(record)
10
+ json_encoder.call(Records::Formatter.new.call(record))
11
+ end
12
+
13
+ private
14
+
15
+ def json_encoder = @json_encoder ||= Serialization::JsonEncoder.new
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ module LogFormats
7
+ module RecordDecoder
8
+ class << self
9
+ def kind(value)
10
+ case value.to_s
11
+ when "point" then :point
12
+ when "summary" then :summary
13
+ else value.to_sym
14
+ end
15
+ end
16
+
17
+ def section(value)
18
+ return {} unless value.is_a?(Hash)
19
+
20
+ Fields::FieldSet.deep_symbolize_keys(value)
21
+ end
22
+
23
+ def sections(source, sections: Fields::Bags.record_hash_sections)
24
+ sections.to_h do |name|
25
+ value = block_given? ? yield(name, source) : source[name.to_s]
26
+
27
+ [name, section(value)]
28
+ end
29
+ end
30
+
31
+ def error(value)
32
+ section(value) unless value.nil?
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ module LogFormats
7
+ Entry = Data.define(:name, :decoder, :encoder, :priority)
8
+ AUTO_FORMAT = :auto
9
+ FORMAT_NAME_PATTERN = /\A[a-z][a-z0-9_]*\z/
10
+
11
+ @entries = []
12
+
13
+ class << self
14
+ def register(name, decoder: nil, encoder: nil, priority: 0)
15
+ name = normalize(name)
16
+ validate_component(decoder, :decoder) if decoder
17
+ validate_component(encoder, :encoder) if encoder
18
+ existing = @entries.find { it.name == name }
19
+ priority = priority.to_i
20
+ entry = Entry.new(
21
+ name: name,
22
+ decoder: decoder || existing&.decoder,
23
+ encoder: encoder || existing&.encoder,
24
+ priority: priority.zero? && existing ? existing.priority : priority
25
+ )
26
+ @entries = @entries.reject { it.name == name } + [entry]
27
+ entry
28
+ end
29
+
30
+ def decode(payload, format: AUTO_FORMAT)
31
+ raise TypeError, "log entry must be a JSON object" unless payload.is_a?(Hash)
32
+
33
+ format = normalize(format)
34
+ entry = format == AUTO_FORMAT ? auto_decode_entry(payload) : named_decode_entry(format, payload)
35
+ entry.decoder.call(payload)
36
+ end
37
+
38
+ def encode(record, format:)
39
+ name = normalize(format)
40
+ entry = named_encode_entry(name)
41
+ entry.encoder.call(record)
42
+ end
43
+
44
+ def record_from_json_line(line, line_number:, format: AUTO_FORMAT)
45
+ payload = JSON.parse(line)
46
+ Records::Record.from_normalized_hash(decode(payload, format: format))
47
+ rescue JSON::ParserError => e
48
+ raise ArgumentError, "line #{line_number}: invalid JSON: #{e.message}"
49
+ rescue TypeError, ArgumentError => e
50
+ raise ArgumentError, "line #{line_number}: #{e.message}"
51
+ end
52
+
53
+ def normalize(value)
54
+ name = Core.normalize_name(value, name: "log format")
55
+ return name if name.to_s.match?(FORMAT_NAME_PATTERN)
56
+
57
+ raise ArgumentError, "log format must contain lowercase letters, digits, or underscores"
58
+ end
59
+
60
+ def load(name)
61
+ path = "julewire/#{name}"
62
+ require path
63
+ rescue LoadError => e
64
+ raise unless e.path == path
65
+ end
66
+
67
+ private
68
+
69
+ def validate_component(component, name)
70
+ Validation.validate_callable!(component, name: name)
71
+ end
72
+
73
+ def auto_decode_entry(payload)
74
+ entry = decode_entries.find { decoder_match?(it.decoder, payload) }
75
+ raise TypeError, "no log decoder accepted JSON object" unless entry
76
+
77
+ entry
78
+ end
79
+
80
+ def named_decode_entry(name, payload)
81
+ load_format(name)
82
+ entry = @entries.find { it.name == name && it.decoder }
83
+ raise ArgumentError, "log format #{name} is not available" unless entry
84
+ unless decoder_match?(entry.decoder, payload)
85
+ raise TypeError, "log format #{name} did not accept JSON object"
86
+ end
87
+
88
+ entry
89
+ end
90
+
91
+ def named_encode_entry(name)
92
+ load_format(name)
93
+ entry = @entries.find { it.name == name && it.encoder }
94
+ raise ArgumentError, "log format #{name} is not available" unless entry
95
+
96
+ entry
97
+ end
98
+
99
+ def load_format(name)
100
+ return if @entries.any? { it.name == name }
101
+
102
+ load(name)
103
+ end
104
+
105
+ def decode_entries
106
+ @entries.select(&:decoder).sort_by { [-it.priority, @entries.index(it)] }
107
+ end
108
+
109
+ def decoder_match?(decoder, payload)
110
+ return decoder.match?(payload) if decoder.respond_to?(:match?)
111
+
112
+ true
113
+ rescue StandardError
114
+ false
115
+ end
116
+ end
117
+
118
+ register(:core, decoder: CoreJsonDecoder, encoder: CoreJsonEncoder)
119
+ register(:console, encoder: ConsoleText.new)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ class Tail
7
+ include LineHelpers
8
+
9
+ DEFAULT_MAX_VALUE_BYTES = Serialization::TextEncoder::DEFAULT_MAX_VALUE_BYTES
10
+ DEFAULT_POLL_INTERVAL = 0.1
11
+ FLAGS = {
12
+ "--color" => [:color, true],
13
+ "--no-color" => [:color, false],
14
+ "--follow" => [:follow, true],
15
+ "--once" => [:follow, false],
16
+ "--plain" => %i[theme plain],
17
+ "--punk" => %i[theme punk],
18
+ "--skip-invalid" => %i[invalid skip],
19
+ "--raw-invalid" => %i[invalid raw]
20
+ }.freeze
21
+
22
+ def initialize(argv:, stdin:, stdout:)
23
+ @argv = argv
24
+ @stdin = stdin
25
+ @stdout = stdout
26
+ end
27
+
28
+ def call
29
+ options = tail_options
30
+ renderer = tail_renderer(options)
31
+ options.fetch(:path) == "-" ? tail_stdin(options, renderer) : tail_file(options, renderer)
32
+ 0
33
+ end
34
+
35
+ private
36
+
37
+ def tail_options
38
+ parse_command_options(default_tail_options, command: "tail") do |options, value|
39
+ apply_tail_option(options, value)
40
+ end
41
+ end
42
+
43
+ def default_tail_options
44
+ {
45
+ color: @stdout.respond_to?(:tty?) && @stdout.tty?,
46
+ format: :auto,
47
+ follow: true,
48
+ invalid: :fail,
49
+ limit: nil,
50
+ max_value_bytes: DEFAULT_MAX_VALUE_BYTES,
51
+ path: nil,
52
+ poll_interval: DEFAULT_POLL_INTERVAL,
53
+ theme: :plain
54
+ }
55
+ end
56
+
57
+ def apply_tail_option(options, value)
58
+ if (assignment = FLAGS[value])
59
+ 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"
63
+ options[:format] = next_symbol_option("--format")
64
+ elsif value == "--theme"
65
+ options[:theme] = next_symbol_option("--theme")
66
+ elsif value == "--limit"
67
+ options[:limit] = positive_integer_option("--limit")
68
+ elsif value == "--max-value-bytes"
69
+ options[:max_value_bytes] = positive_integer_option("--max-value-bytes")
70
+ else
71
+ apply_path_option(options, value, command: "tail")
72
+ end
73
+ end
74
+
75
+ def tail_renderer(options)
76
+ encoder = console_text_encoder(options)
77
+ proc do |line, line_number|
78
+ write_encoded_record_line(
79
+ line,
80
+ line_number,
81
+ input_format: options.fetch(:format),
82
+ invalid: options.fetch(:invalid),
83
+ encoder: encoder
84
+ )
85
+ end
86
+ end
87
+
88
+ def tail_stdin(options, renderer)
89
+ limit = options.fetch(:limit)
90
+ return render_limited_stdin(limit, renderer) if limit
91
+
92
+ render_stream(@stdin.each_line, renderer)
93
+ end
94
+
95
+ def tail_file(options, renderer)
96
+ File.open(options.fetch(:path), "r") do |file|
97
+ line_number = render_file_snapshot(file, options, renderer)
98
+ follow_file(file, line_number, options, renderer) if options.fetch(:follow)
99
+ end
100
+ end
101
+
102
+ def render_file_snapshot(file, options, renderer)
103
+ entries = indexed_lines(file.each_line)
104
+ render_entries(limit_entries(entries, options.fetch(:limit)), renderer)
105
+ file.seek(0, IO::SEEK_END)
106
+ entries.last&.fetch(0) || 0
107
+ end
108
+
109
+ def follow_file(file, line_number, options, renderer)
110
+ loop do
111
+ if (line = file.gets)
112
+ line_number += 1
113
+ render_entries([[line_number, line]], renderer)
114
+ else
115
+ line_number = reset_follow_position(file) if file.stat.size < file.pos
116
+ sleep(options.fetch(:poll_interval))
117
+ end
118
+ end
119
+ end
120
+
121
+ def reset_follow_position(file)
122
+ file.seek(0)
123
+ 0
124
+ end
125
+
126
+ def render_limited_stdin(limit, renderer)
127
+ entries = indexed_lines(@stdin.each_line)
128
+ render_entries(limit_entries(entries, limit), renderer)
129
+ end
130
+
131
+ def render_stream(lines, renderer)
132
+ line_number = 0
133
+ lines.each do |line|
134
+ line_number += 1
135
+ next if line.strip.empty?
136
+
137
+ renderer.call(line, line_number)
138
+ end
139
+ end
140
+
141
+ def limit_entries(entries, limit)
142
+ limit ? entries.last(limit) : entries
143
+ end
144
+
145
+ def render_entries(entries, renderer)
146
+ entries.each do |line_number, line|
147
+ renderer.call(line, line_number)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ class Transcode
7
+ include LineHelpers
8
+
9
+ DEFAULT_MAX_VALUE_BYTES = Serialization::TextEncoder::DEFAULT_MAX_VALUE_BYTES
10
+ FLAGS = {
11
+ "--color" => [:color, true],
12
+ "--no-color" => [:color, false],
13
+ "--plain" => %i[theme plain],
14
+ "--punk" => %i[theme punk],
15
+ "--skip-invalid" => %i[invalid skip],
16
+ "--raw-invalid" => %i[invalid raw]
17
+ }.freeze
18
+
19
+ def initialize(argv:, stdin:, stdout:)
20
+ @argv = argv
21
+ @stdin = stdin
22
+ @stdout = stdout
23
+ end
24
+
25
+ def call
26
+ options = transcode_options
27
+ encoder = encoder_for(options)
28
+ each_entry(options.fetch(:path)) do |line_number, line|
29
+ write_encoded_record_line(
30
+ line,
31
+ line_number,
32
+ input_format: options.fetch(:from),
33
+ invalid: options.fetch(:invalid),
34
+ encoder: encoder
35
+ )
36
+ end
37
+ 0
38
+ end
39
+
40
+ private
41
+
42
+ def transcode_options
43
+ parse_command_options(default_transcode_options, command: "transcode") do |options, value|
44
+ apply_transcode_option(options, value)
45
+ end
46
+ end
47
+
48
+ def default_transcode_options
49
+ {
50
+ color: @stdout.respond_to?(:tty?) && @stdout.tty?,
51
+ from: :auto,
52
+ invalid: :fail,
53
+ max_value_bytes: DEFAULT_MAX_VALUE_BYTES,
54
+ path: nil,
55
+ theme: :plain,
56
+ to: :core
57
+ }
58
+ end
59
+
60
+ def apply_transcode_option(options, value)
61
+ if (assignment = FLAGS[value])
62
+ options[assignment.fetch(0)] = assignment.fetch(1)
63
+ elsif value.start_with?("--")
64
+ apply_named_option(options, value)
65
+ else
66
+ apply_path_option(options, value, command: "transcode")
67
+ end
68
+ end
69
+
70
+ def apply_named_option(options, value)
71
+ if value.start_with?("--from=")
72
+ options[:from] = value.delete_prefix("--from=").to_sym
73
+ elsif value.start_with?("--to=")
74
+ options[:to] = value.delete_prefix("--to=").to_sym
75
+ else
76
+ apply_separate_option(options, value)
77
+ end
78
+ end
79
+
80
+ def apply_separate_option(options, value)
81
+ case value
82
+ when "--from" then options[:from] = next_symbol_option("--from")
83
+ when "--to" then options[:to] = next_symbol_option("--to")
84
+ when "--theme" then options[:theme] = next_symbol_option("--theme")
85
+ when "--max-value-bytes" then options[:max_value_bytes] = positive_integer_option("--max-value-bytes")
86
+ else
87
+ apply_path_option(options, value, command: "transcode")
88
+ end
89
+ end
90
+
91
+ def encoder_for(options)
92
+ return console_text_encoder(options) if options.fetch(:to) == :console
93
+
94
+ ->(record) { LogFormats.encode(record, format: options.fetch(:to)) }
95
+ end
96
+
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(&) }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class CLI
6
+ INTERRUPTED_STATUS = 130
7
+
8
+ class << self
9
+ def call(argv: ARGV, stdin: $stdin, stdout: $stdout, stderr: $stderr)
10
+ new(argv: argv, stdin: stdin, stdout: stdout, stderr: stderr).call
11
+ end
12
+ end
13
+
14
+ def initialize(argv:, stdin:, stdout:, stderr:)
15
+ @argv = argv.dup
16
+ @stdin = stdin
17
+ @stdout = stdout
18
+ @stderr = stderr
19
+ end
20
+
21
+ def call
22
+ case command = @argv.shift
23
+ when "tail" then tail
24
+ when "transcode" then transcode
25
+ when "doctor" then doctor
26
+ when "-v", "--version", "version" then version
27
+ when nil, "-h", "--help", "help" then help
28
+ else
29
+ fail_with("unknown command #{command.inspect}")
30
+ end
31
+ rescue Interrupt
32
+ INTERRUPTED_STATUS
33
+ rescue ArgumentError, Errno::ENOENT => e
34
+ fail_with(e.message)
35
+ end
36
+
37
+ private
38
+
39
+ def tail
40
+ Tail.new(argv: @argv, stdin: @stdin, stdout: @stdout).call
41
+ end
42
+
43
+ def transcode
44
+ Transcode.new(argv: @argv, stdin: @stdin, stdout: @stdout).call
45
+ end
46
+
47
+ def doctor
48
+ Doctor.new(argv: @argv, stdout: @stdout).call
49
+ end
50
+
51
+ def version
52
+ @stdout.write("julewire #{Core::VERSION}\n")
53
+ 0
54
+ end
55
+
56
+ def help
57
+ @stdout.write(<<~HELP)
58
+ Usage:
59
+ julewire tail [--follow|--once] [--format auto|core|NAME] [--skip-invalid|--raw-invalid] [--color|--no-color] [--theme plain|punk|--plain|--punk] [--limit N] [--max-value-bytes N] LOGFILE|-
60
+ julewire transcode [--from auto|core|NAME] [--to core|console|NAME] [--skip-invalid|--raw-invalid] [--color|--no-color] [--theme plain|punk|--plain|--punk] [--max-value-bytes N] LOGFILE|-
61
+ julewire doctor [--json|--text|--punk] [--color|--no-color]
62
+ julewire --version
63
+ HELP
64
+ 0
65
+ end
66
+
67
+ def fail_with(message)
68
+ @stderr.write("julewire: #{message}\n")
69
+ 1
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class Configuration
6
+ ATTRIBUTES = %i[
7
+ destinations
8
+ emit_non_standard_exception_summaries
9
+ error_backtrace_lines
10
+ labels
11
+ level
12
+ on_drop
13
+ on_failure
14
+ pipeline_close_timeout
15
+ processors
16
+ ].freeze
17
+ REGISTRY_ATTRIBUTES = %i[destinations labels processors].freeze
18
+ SCALAR_ATTRIBUTES = (ATTRIBUTES - REGISTRY_ATTRIBUTES).freeze
19
+
20
+ attr_accessor(*SCALAR_ATTRIBUTES)
21
+ attr_reader(*REGISTRY_ATTRIBUTES)
22
+
23
+ def initialize(**options)
24
+ reject_unknown_options!(options)
25
+ @destinations = options.fetch(:destinations) { Destinations::Registry.new }
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)
28
+ @labels = options.fetch(:labels) { Fields::StaticLabels.new }
29
+ @level = options.fetch(:level, :debug)
30
+ @on_drop = options.fetch(:on_drop, nil)
31
+ @on_failure = options.fetch(:on_failure, nil)
32
+ @pipeline_close_timeout = options.fetch(:pipeline_close_timeout, 1)
33
+ @processors = options.fetch(:processors) { Processing::ProcessorRegistry.new }
34
+ end
35
+
36
+ def validate!
37
+ validate_contracts!
38
+ Validation.validate_non_negative_integer!(
39
+ error_backtrace_lines,
40
+ name: :error_backtrace_lines
41
+ )
42
+ Validation.validate_timeout!(
43
+ pipeline_close_timeout,
44
+ name: :pipeline_close_timeout
45
+ )
46
+ Records::Severity.normalize(level)
47
+ self
48
+ end
49
+
50
+ def snapshot
51
+ validate!
52
+ copy.tap do |configuration|
53
+ configuration.level = Records::Severity.normalize(level)
54
+ configuration.freeze
55
+ end
56
+ end
57
+
58
+ def copy
59
+ self.class.new(**copy_options)
60
+ end
61
+
62
+ def build_pipeline(invalid_severity_reporter: Diagnostics::InvalidSeverityReporter.counter)
63
+ Processing::Pipeline.new(configuration: self, invalid_severity_reporter: invalid_severity_reporter)
64
+ end
65
+
66
+ def freeze
67
+ destinations.freeze
68
+ labels.freeze
69
+ processors.freeze
70
+ super
71
+ end
72
+
73
+ private
74
+
75
+ def reject_unknown_options!(options)
76
+ Validation.validate_options!(options, ATTRIBUTES, name: :configuration)
77
+ end
78
+
79
+ def validate_contracts!
80
+ Validation.validate_callable!(on_drop, name: :on_drop, allow_nil: true)
81
+ Validation.validate_callable!(on_failure, name: :on_failure, allow_nil: true)
82
+ end
83
+
84
+ def copy_options
85
+ {
86
+ destinations: destinations.copy,
87
+ emit_non_standard_exception_summaries: emit_non_standard_exception_summaries,
88
+ error_backtrace_lines: error_backtrace_lines,
89
+ labels: labels.copy,
90
+ level: level,
91
+ on_drop: on_drop,
92
+ on_failure: on_failure,
93
+ pipeline_close_timeout: pipeline_close_timeout,
94
+ processors: processors.copy
95
+ }
96
+ end
97
+ end
98
+ end
99
+ end