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