legion-logging 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +19 -1
- data/CHANGELOG.md +62 -1
- data/Gemfile +1 -0
- data/lib/legion/logging/async_writer.rb +46 -11
- data/lib/legion/logging/builder.rb +74 -34
- data/lib/legion/logging/event_builder.rb +92 -6
- data/lib/legion/logging/helper.rb +555 -26
- data/lib/legion/logging/hooks.rb +72 -0
- data/lib/legion/logging/methods.rb +121 -53
- data/lib/legion/logging/multi_io.rb +0 -1
- data/lib/legion/logging/redactor.rb +33 -2
- data/lib/legion/logging/settings.rb +16 -0
- data/lib/legion/logging/shipper/file_transport.rb +9 -1
- data/lib/legion/logging/shipper/http_transport.rb +7 -3
- data/lib/legion/logging/shipper.rb +16 -12
- data/lib/legion/logging/tagged_logger.rb +129 -0
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +56 -6
- data/lib/legion/service.rb +28 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62c3c14ff8e6f67a11941a869f4f4ea2b682686c85c41bb1568368b448f23d0c
|
|
4
|
+
data.tar.gz: af9c1588f601a09f6b986d9dc47cc07b1c8e9b966e482b45d1a7242342e8d500
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66a26f844b44432886b1d703d93dbb9547134ede3542610861e123e240b8ec89746d7bdc522ae8e831c93e212f9e1a9bff5fc9b2fa19a78dfb47007373c5e0ab
|
|
7
|
+
data.tar.gz: add0a140cbe3c101dc83dcfb4b1c3eef385e4c778a4b6957be6d71b2ec95a9316c3b36d7b687906867046d482b2246ad0a44e8fd4cc98472ad1157994207146a
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -3,13 +3,31 @@ on:
|
|
|
3
3
|
push:
|
|
4
4
|
branches: [main]
|
|
5
5
|
pull_request:
|
|
6
|
+
schedule:
|
|
7
|
+
- cron: '0 9 * * 1'
|
|
6
8
|
|
|
7
9
|
jobs:
|
|
8
10
|
ci:
|
|
9
11
|
uses: LegionIO/.github/.github/workflows/ci.yml@main
|
|
10
12
|
|
|
13
|
+
lint:
|
|
14
|
+
uses: LegionIO/.github/.github/workflows/lint-patterns.yml@main
|
|
15
|
+
|
|
16
|
+
security:
|
|
17
|
+
uses: LegionIO/.github/.github/workflows/security-scan.yml@main
|
|
18
|
+
|
|
19
|
+
version-changelog:
|
|
20
|
+
uses: LegionIO/.github/.github/workflows/version-changelog.yml@main
|
|
21
|
+
|
|
22
|
+
dependency-review:
|
|
23
|
+
uses: LegionIO/.github/.github/workflows/dependency-review.yml@main
|
|
24
|
+
|
|
25
|
+
stale:
|
|
26
|
+
if: github.event_name == 'schedule'
|
|
27
|
+
uses: LegionIO/.github/.github/workflows/stale.yml@main
|
|
28
|
+
|
|
11
29
|
release:
|
|
12
|
-
needs: ci
|
|
30
|
+
needs: [ci, lint]
|
|
13
31
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
14
32
|
uses: LegionIO/.github/.github/workflows/release.yml@main
|
|
15
33
|
secrets:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Legion::Logging Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0] - 2026-04-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Logging.current_settings` and `.configuration_generation` so helper mixins can refresh memoized tagged loggers after runtime reconfiguration
|
|
7
|
+
- Component logger overrides from local `settings`, top-level `Legion::Settings[component]`, and `Legion::Settings.dig(:extensions, component)` for `log_level`, `trace`, `trace_size`, and `extended`
|
|
8
|
+
- `Methods#emit_tagged` / `TaggedLogger#dispatch` path so component-level loggers can emit with their own level while preserving tagged context
|
|
9
|
+
- Fallback exception event construction in `Helper#handle_exception` when structured exception support is unavailable
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- `setup` and `Builder#log_level` now default to `debug`
|
|
13
|
+
- Default helper/tagged logger behavior enables trace and extended metadata
|
|
14
|
+
- `Helper#log` rebuilds memoized `TaggedLogger` instances when logging configuration changes
|
|
15
|
+
- Runtime logger settings take precedence over loaded global settings for helper-mixed components
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `setup(async: true)` now tolerates boolean `logging.async` settings without probing for `buffer_size`
|
|
19
|
+
- Exception stdout/file output now falls back safely when singleton logger helpers are unavailable
|
|
20
|
+
- Structured exception publishing is skipped when the exception writer/EventBuilder path is unavailable
|
|
21
|
+
- `TaggedLogger#unknown` falls back to `debug` output when `Legion::Logging.unknown` is unavailable
|
|
22
|
+
|
|
23
|
+
## [1.4.3] - 2026-04-01
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `TaggedLogger` lightweight proxy: delegates to singleton for shared stdout/file/async output
|
|
27
|
+
- `Helper#derive_log_segments` with class-level `SEGMENT_CACHE` — auto-derives `[llm][router]` from namespace
|
|
28
|
+
- `Helper#with_log_context` for block-scoped method name thread-locals (`{dispatch}` in log output)
|
|
29
|
+
- `Helper#handle_exception` with direct EventBuilder calls, per-line Rainbow coloring, structured AMQP publish
|
|
30
|
+
- `Helper.current_log_method`, `.current_log_segments`, `.current_context` thread-local readers
|
|
31
|
+
- `Legion::Logging::Settings` module with logger defaults
|
|
32
|
+
- `COMPONENT_MAP` with 18 component types (runners, actors, hooks, absorbers, tools, adapters, middleware, etc.)
|
|
33
|
+
- `EXCEPTION_COLORS` map for per-level exception coloring (bold first line, faint backtrace)
|
|
34
|
+
- `Thread.current[:legion_context]` support for wire protocol fields (task_id, conversation_id, chain_id)
|
|
35
|
+
- Redaction applied to exception stdout output when redaction is enabled
|
|
36
|
+
- Method context (`legion_log_method`) included in structured exception events
|
|
37
|
+
- `AsyncWriter::LogEntry` carries `segments` and `method_ctx` for thread-local propagation to writer thread
|
|
38
|
+
- `Builder#resolve_lex_tag` and `#build_runner_trace` extracted from `text_format`
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- `Helper#log` returns `TaggedLogger` instead of `Logger.new` (shared output, one async thread)
|
|
42
|
+
- `Helper#log_name`/`gem_name`/`gem_spec` replace `log_lex_name`/`lex_gem_name`/`gem_spec_for_lex` with multi-prefix resolution
|
|
43
|
+
- `gem_name` and `gem_spec` memoized per instance
|
|
44
|
+
- `COMPONENT_REGEX` in Methods expanded from 5 to 18 component types
|
|
45
|
+
- `build_writer_context` reads `Thread.current[:legion_log_segments]` instead of stale `@lex_segments` ivar
|
|
46
|
+
- `Builder#output` delegates to `set_log` (was parallel implementation)
|
|
47
|
+
- `Builder#caller_locations` allocates single frame instead of full stack
|
|
48
|
+
- Unknown log level strings default to INFO instead of DEBUG
|
|
49
|
+
- `EventBuilder#legion_versions` and `#resolve_gem_spec` memoized
|
|
50
|
+
- `EXCEPTION_PRIORITY` extracted to frozen constant in Methods (was inline hash allocation per call)
|
|
51
|
+
- `text_format` and `json_format` in Builder read thread-locals for segments and method context
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- `fire_log_writer` rescue no longer references undefined `routing_key` variable
|
|
55
|
+
- Splunk auth header in `http_transport` — `apply_auth` receives actual URI instead of always evaluating against `URI('/')`
|
|
56
|
+
- `TaggedLogger#initialize` accepts `**_opts` splat for unexpected settings keys
|
|
57
|
+
- `TaggedLogger#trace` guards nil `size` to prevent `TypeError` on `caller_locations`
|
|
58
|
+
|
|
59
|
+
### Removed
|
|
60
|
+
- `TaggedLogger#runner_exception` (runner business logic, not logging concern)
|
|
61
|
+
- `TaggedLogger#log_exception` (use `Helper#handle_exception` instead)
|
|
62
|
+
- `Builder#log_level` no-op `@log = log` self-assignment
|
|
63
|
+
|
|
3
64
|
## [1.4.2] - 2026-03-28
|
|
4
65
|
|
|
5
66
|
### Added
|
|
@@ -135,4 +196,4 @@
|
|
|
135
196
|
- `format_for_elk` produces ELK-compatible event hashes
|
|
136
197
|
|
|
137
198
|
## v1.2.0
|
|
138
|
-
Moving from BitBucket to GitHub. All git history is reset from this point on
|
|
199
|
+
Moving from BitBucket to GitHub. All git history is reset from this point on
|
data/Gemfile
CHANGED
|
@@ -1,42 +1,62 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'methods'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Logging
|
|
5
7
|
class AsyncWriter
|
|
6
|
-
LogEntry = ::Data.define(:level, :message, :writer_context)
|
|
8
|
+
LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx, :caller_trace)
|
|
7
9
|
SHUTDOWN = :shutdown
|
|
8
10
|
|
|
11
|
+
attr_reader :logger
|
|
12
|
+
|
|
9
13
|
def initialize(logger, buffer_size: 10_000)
|
|
10
14
|
@logger = logger
|
|
15
|
+
@buffer_size = buffer_size
|
|
11
16
|
@queue = SizedQueue.new(buffer_size)
|
|
12
17
|
@thread = nil
|
|
18
|
+
@state_mutex = Mutex.new
|
|
19
|
+
@accepting = true
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
def start
|
|
16
23
|
return if @thread&.alive?
|
|
17
24
|
|
|
25
|
+
@state_mutex.synchronize { @accepting = true }
|
|
18
26
|
drain
|
|
27
|
+
@queue = SizedQueue.new(@buffer_size)
|
|
19
28
|
@thread = Thread.new { consume }
|
|
20
29
|
@thread.name = 'legion-log-writer'
|
|
21
30
|
@thread.abort_on_exception = false
|
|
22
31
|
end
|
|
23
32
|
|
|
33
|
+
# rubocop:disable Naming/PredicateMethod
|
|
24
34
|
def stop(timeout: 2)
|
|
25
|
-
|
|
35
|
+
@state_mutex.synchronize { @accepting = false }
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
unless @thread&.alive?
|
|
38
|
+
drain
|
|
39
|
+
@thread = nil
|
|
40
|
+
return true
|
|
31
41
|
end
|
|
32
|
-
|
|
33
|
-
@
|
|
34
|
-
|
|
42
|
+
|
|
43
|
+
@queue.close
|
|
44
|
+
timeout ? @thread.join(timeout) : @thread.join
|
|
45
|
+
return false if @thread&.alive?
|
|
46
|
+
|
|
47
|
+
@thread = nil
|
|
48
|
+
true
|
|
35
49
|
end
|
|
36
50
|
|
|
37
51
|
def push(entry)
|
|
52
|
+
return false unless accepting?
|
|
53
|
+
|
|
38
54
|
@queue.push(entry)
|
|
55
|
+
true
|
|
56
|
+
rescue ClosedQueueError
|
|
57
|
+
false
|
|
39
58
|
end
|
|
59
|
+
# rubocop:enable Naming/PredicateMethod
|
|
40
60
|
|
|
41
61
|
def alive?
|
|
42
62
|
@thread&.alive? || false
|
|
@@ -47,17 +67,27 @@ module Legion
|
|
|
47
67
|
def consume
|
|
48
68
|
loop do
|
|
49
69
|
entry = @queue.pop
|
|
50
|
-
break if entry == SHUTDOWN
|
|
70
|
+
break if entry.nil? || entry == SHUTDOWN
|
|
51
71
|
|
|
52
72
|
write_entry(entry)
|
|
53
73
|
end
|
|
54
74
|
end
|
|
55
75
|
|
|
56
76
|
def write_entry(entry)
|
|
77
|
+
prev_segments = Thread.current[:legion_log_segments]
|
|
78
|
+
prev_method_ctx = Thread.current[:legion_log_method]
|
|
79
|
+
prev_caller = Thread.current[:legion_log_caller]
|
|
80
|
+
Thread.current[:legion_log_segments] = entry.segments
|
|
81
|
+
Thread.current[:legion_log_method] = entry.method_ctx
|
|
82
|
+
Thread.current[:legion_log_caller] = entry.caller_trace
|
|
57
83
|
@logger.send(entry.level, entry.message)
|
|
58
84
|
fire_writer(entry) if entry.writer_context
|
|
59
85
|
rescue StandardError => e
|
|
60
86
|
warn("legion-log-writer error: #{e.message} (#{e.backtrace&.first})")
|
|
87
|
+
ensure
|
|
88
|
+
Thread.current[:legion_log_segments] = prev_segments
|
|
89
|
+
Thread.current[:legion_log_method] = prev_method_ctx
|
|
90
|
+
Thread.current[:legion_log_caller] = prev_caller
|
|
61
91
|
end
|
|
62
92
|
|
|
63
93
|
def drain
|
|
@@ -69,14 +99,19 @@ module Legion
|
|
|
69
99
|
nil
|
|
70
100
|
end
|
|
71
101
|
|
|
102
|
+
def accepting?
|
|
103
|
+
@state_mutex.synchronize { @accepting }
|
|
104
|
+
end
|
|
105
|
+
|
|
72
106
|
def fire_writer(entry)
|
|
73
107
|
ctx = entry.writer_context
|
|
74
108
|
event = ctx[:event]
|
|
75
109
|
level = ctx[:level]
|
|
76
110
|
lex_name = event[:lex] || 'core'
|
|
77
|
-
component = event.dig(:caller, :file).to_s[
|
|
111
|
+
component = event.dig(:caller, :file).to_s[Legion::Logging::Methods::COMPONENT_REGEX, 1] || 'unknown'
|
|
78
112
|
routing_key = "legion.logging.log.#{level}.#{lex_name}.#{component}"
|
|
79
113
|
Legion::Logging.log_writer.call(event, routing_key: routing_key)
|
|
114
|
+
Legion::Logging::Hooks.fire(level, entry.message, event) if defined?(Legion::Logging::Hooks)
|
|
80
115
|
rescue StandardError => e
|
|
81
116
|
warn("legion-log-writer writer error: #{e.message}")
|
|
82
117
|
end
|
|
@@ -27,6 +27,10 @@ module Legion
|
|
|
27
27
|
thread: Thread.current.object_id
|
|
28
28
|
}
|
|
29
29
|
entry[:pid] = ::Process.pid if include_pid
|
|
30
|
+
segments = Thread.current[:legion_log_segments]
|
|
31
|
+
entry[:segments] = segments if segments
|
|
32
|
+
method_ctx = Thread.current[:legion_log_method]
|
|
33
|
+
entry[:method] = method_ctx if method_ctx
|
|
30
34
|
"#{::JSON.generate(entry)}\n"
|
|
31
35
|
rescue StandardError => e
|
|
32
36
|
warn("Legion::Logging::Builder#json_format formatter failed: #{e.message}")
|
|
@@ -36,24 +40,12 @@ module Legion
|
|
|
36
40
|
|
|
37
41
|
def text_format(include_pid: false, **options)
|
|
38
42
|
log.formatter = proc do |severity, datetime, _progname, msg|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"[#{options[:lex]}]"
|
|
43
|
-
end
|
|
44
|
-
unless options[:lex_name].nil?
|
|
45
|
-
loc = caller_locations[4]
|
|
46
|
-
path = loc.to_s.split('/').last(2)
|
|
47
|
-
runner_trace = {
|
|
48
|
-
type: path[0],
|
|
49
|
-
file: File.basename(loc.path, '.*'),
|
|
50
|
-
function: loc.base_label,
|
|
51
|
-
line_number: loc.lineno
|
|
52
|
-
}
|
|
53
|
-
end
|
|
43
|
+
lex_name = resolve_lex_tag(options)
|
|
44
|
+
runner_trace = Thread.current[:legion_log_caller] || build_runner_trace if lex_name
|
|
45
|
+
|
|
54
46
|
string = "[#{datetime}]"
|
|
55
47
|
string.concat("[#{::Process.pid}]") if include_pid
|
|
56
|
-
string.concat(
|
|
48
|
+
string.concat(lex_name) if lex_name
|
|
57
49
|
if runner_trace.is_a?(Hash) && (options[:extended] || severity == 'debug')
|
|
58
50
|
string.concat("[#{runner_trace[:type]}:#{runner_trace[:file]}:#{runner_trace[:function]}:#{runner_trace[:line_number]}]")
|
|
59
51
|
end
|
|
@@ -62,17 +54,35 @@ module Legion
|
|
|
62
54
|
end
|
|
63
55
|
end
|
|
64
56
|
|
|
57
|
+
def resolve_lex_tag(options)
|
|
58
|
+
segments = Thread.current[:legion_log_segments]
|
|
59
|
+
tag = if segments
|
|
60
|
+
segments.map { |s| "[#{s}]" }.join
|
|
61
|
+
elsif options.key?(:lex_segments)
|
|
62
|
+
options[:lex_segments].map { |s| "[#{s}]" }.join
|
|
63
|
+
elsif options.key?(:lex) && !options[:lex].nil?
|
|
64
|
+
"[#{options[:lex]}]"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
method_ctx = Thread.current[:legion_log_method]
|
|
68
|
+
tag = "#{tag}{#{method_ctx}}" if tag && method_ctx
|
|
69
|
+
tag
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_runner_trace(loc = caller_locations(6, 1)&.first)
|
|
73
|
+
return unless loc
|
|
74
|
+
|
|
75
|
+
path = loc.to_s.split('/').last(2)
|
|
76
|
+
{
|
|
77
|
+
type: path[0],
|
|
78
|
+
file: File.basename(loc.path, '.*'),
|
|
79
|
+
function: loc.base_label,
|
|
80
|
+
line_number: loc.lineno
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
65
84
|
def output(**options)
|
|
66
|
-
|
|
67
|
-
path = prepare_log_path(options[:log_file])
|
|
68
|
-
require_relative 'multi_io'
|
|
69
|
-
io = MultiIO.new($stdout, File.open(path, 'a'))
|
|
70
|
-
@log = ::Logger.new(io)
|
|
71
|
-
elsif options[:log_file]
|
|
72
|
-
@log = ::Logger.new(prepare_log_path(options[:log_file]))
|
|
73
|
-
else
|
|
74
|
-
@log = ::Logger.new($stdout)
|
|
75
|
-
end
|
|
85
|
+
set_log(logfile: options[:log_file], log_stdout: options[:log_stdout])
|
|
76
86
|
end
|
|
77
87
|
|
|
78
88
|
def log
|
|
@@ -80,16 +90,25 @@ module Legion
|
|
|
80
90
|
end
|
|
81
91
|
|
|
82
92
|
def set_log(logfile: nil, log_stdout: nil, **)
|
|
93
|
+
previous_log = @log
|
|
94
|
+
|
|
83
95
|
if logfile && log_stdout != false
|
|
84
96
|
path = prepare_log_path(logfile)
|
|
85
97
|
require_relative 'multi_io'
|
|
86
|
-
|
|
98
|
+
file = File.new(path, 'a')
|
|
99
|
+
file.sync = true
|
|
100
|
+
io = MultiIO.new($stdout, file)
|
|
87
101
|
@log = ::Logger.new(io)
|
|
88
102
|
elsif logfile
|
|
89
|
-
|
|
103
|
+
file = File.new(prepare_log_path(logfile), 'a')
|
|
104
|
+
file.sync = true
|
|
105
|
+
@log = ::Logger.new(file)
|
|
90
106
|
else
|
|
91
107
|
@log = ::Logger.new($stdout)
|
|
92
108
|
end
|
|
109
|
+
|
|
110
|
+
close_replaced_log(previous_log)
|
|
111
|
+
@log
|
|
93
112
|
end
|
|
94
113
|
|
|
95
114
|
def prepare_log_path(path)
|
|
@@ -102,7 +121,7 @@ module Legion
|
|
|
102
121
|
log.level
|
|
103
122
|
end
|
|
104
123
|
|
|
105
|
-
def log_level(level = '
|
|
124
|
+
def log_level(level = 'debug')
|
|
106
125
|
log.level = case level
|
|
107
126
|
when 'trace', 'debug'
|
|
108
127
|
::Logger::DEBUG
|
|
@@ -120,29 +139,50 @@ module Legion
|
|
|
120
139
|
if level.is_a? Integer
|
|
121
140
|
level
|
|
122
141
|
else
|
|
123
|
-
|
|
142
|
+
1
|
|
124
143
|
end
|
|
125
144
|
end
|
|
126
|
-
@log = log
|
|
127
145
|
end
|
|
128
146
|
|
|
129
147
|
def async?
|
|
130
148
|
(@async == true && @async_writer&.alive?) || false
|
|
131
149
|
end
|
|
132
150
|
|
|
151
|
+
# rubocop:disable Naming/PredicateMethod
|
|
133
152
|
def start_async_writer(buffer_size: 10_000)
|
|
134
153
|
require_relative 'async_writer'
|
|
135
|
-
|
|
154
|
+
return false if @async_writer&.alive? && stop_async_writer == false
|
|
155
|
+
|
|
136
156
|
@async_writer = AsyncWriter.new(log, buffer_size: buffer_size)
|
|
137
157
|
@async_writer.start
|
|
138
158
|
@async = true
|
|
159
|
+
true
|
|
139
160
|
end
|
|
140
161
|
|
|
141
162
|
def stop_async_writer
|
|
142
163
|
writer = @async_writer
|
|
143
|
-
|
|
164
|
+
stopped = writer&.stop
|
|
165
|
+
return false if stopped == false
|
|
166
|
+
|
|
167
|
+
close_replaced_log(writer.logger) if writer.respond_to?(:logger)
|
|
168
|
+
@async_writer = nil if @async_writer.equal?(writer)
|
|
144
169
|
@async = false
|
|
145
|
-
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
# rubocop:enable Naming/PredicateMethod
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def close_replaced_log(logger)
|
|
177
|
+
return unless logger
|
|
178
|
+
return if logger.equal?(@log)
|
|
179
|
+
return if @async_writer&.alive? && @async_writer.respond_to?(:logger) && @async_writer.logger.equal?(logger)
|
|
180
|
+
|
|
181
|
+
log_device = logger.instance_variable_get(:@logdev)
|
|
182
|
+
dev = log_device&.dev
|
|
183
|
+
return if dev.nil? || [$stdout, $stderr].include?(dev)
|
|
184
|
+
|
|
185
|
+
dev.close if dev.respond_to?(:close)
|
|
146
186
|
end
|
|
147
187
|
end
|
|
148
188
|
end
|
|
@@ -11,6 +11,32 @@ module Legion
|
|
|
11
11
|
MAX_PAYLOAD_BYTES = 8192
|
|
12
12
|
MAX_TOTAL_BYTES = 65_536
|
|
13
13
|
BACKTRACE_FALLBACK_FRAMES = 20
|
|
14
|
+
MIN_TRUNCATED_FIELD_BYTES = 256
|
|
15
|
+
|
|
16
|
+
CORE_EXCEPTION_FIELDS = %i[
|
|
17
|
+
timestamp
|
|
18
|
+
level
|
|
19
|
+
exception_class
|
|
20
|
+
message
|
|
21
|
+
caller_file
|
|
22
|
+
caller_line
|
|
23
|
+
caller_function
|
|
24
|
+
lex
|
|
25
|
+
component_type
|
|
26
|
+
gem_name
|
|
27
|
+
lex_version
|
|
28
|
+
handled
|
|
29
|
+
pid
|
|
30
|
+
thread
|
|
31
|
+
task_id
|
|
32
|
+
conversation_id
|
|
33
|
+
user
|
|
34
|
+
error_fingerprint
|
|
35
|
+
node
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
GEM_SPEC_CACHE_MUTEX = Mutex.new
|
|
39
|
+
private_constant :GEM_SPEC_CACHE_MUTEX
|
|
14
40
|
|
|
15
41
|
class << self
|
|
16
42
|
def build(level:, message:, lex: nil, lex_segments: nil, context: nil, category: nil, caller_offset: 2)
|
|
@@ -187,12 +213,18 @@ module Legion
|
|
|
187
213
|
end
|
|
188
214
|
|
|
189
215
|
def resolve_gem_spec(name)
|
|
190
|
-
|
|
191
|
-
|
|
216
|
+
cache = (@gem_spec_cache ||= {})
|
|
217
|
+
return cache[name] if cache.key?(name)
|
|
218
|
+
|
|
219
|
+
spec = nil
|
|
220
|
+
["lex-#{name}", "legion-#{name}", name].each do |candidate|
|
|
221
|
+
spec = Gem::Specification.find_by_name(candidate)
|
|
222
|
+
break
|
|
192
223
|
rescue Gem::MissingSpecError
|
|
193
224
|
next
|
|
194
225
|
end
|
|
195
|
-
|
|
226
|
+
|
|
227
|
+
GEM_SPEC_CACHE_MUTEX.synchronize { cache[name] = spec }
|
|
196
228
|
end
|
|
197
229
|
|
|
198
230
|
def strip_ansi(str)
|
|
@@ -254,9 +286,13 @@ module Legion
|
|
|
254
286
|
end
|
|
255
287
|
|
|
256
288
|
def legion_versions
|
|
257
|
-
Gem::Specification
|
|
258
|
-
|
|
259
|
-
|
|
289
|
+
@legion_versions ||= Gem::Specification
|
|
290
|
+
.select { |s| s.name.start_with?('legion-', 'lex-') }
|
|
291
|
+
.to_h do |s|
|
|
292
|
+
[s.name,
|
|
293
|
+
s.version.to_s]
|
|
294
|
+
end
|
|
295
|
+
.freeze
|
|
260
296
|
end
|
|
261
297
|
|
|
262
298
|
def truncate_bytes(str, max)
|
|
@@ -297,6 +333,56 @@ module Legion
|
|
|
297
333
|
return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
|
|
298
334
|
|
|
299
335
|
event[:message] = truncate_bytes(event[:message].to_s, 1024)
|
|
336
|
+
trim_optional_fields!(event)
|
|
337
|
+
hard_cap_message!(event)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def trim_optional_fields!(event)
|
|
341
|
+
while safe_json_bytesize(event) > MAX_TOTAL_BYTES
|
|
342
|
+
key = largest_optional_field(event)
|
|
343
|
+
break unless key
|
|
344
|
+
|
|
345
|
+
reduced = reduce_field(event[key])
|
|
346
|
+
if reduced.nil?
|
|
347
|
+
event.delete(key)
|
|
348
|
+
else
|
|
349
|
+
event[key] = reduced
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def largest_optional_field(event)
|
|
355
|
+
event.each_key
|
|
356
|
+
.reject { |key| CORE_EXCEPTION_FIELDS.include?(key) }
|
|
357
|
+
.max_by { |key| safe_json_bytesize(event[key]) }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def reduce_field(value)
|
|
361
|
+
case value
|
|
362
|
+
when String
|
|
363
|
+
return nil if value.bytesize <= MIN_TRUNCATED_FIELD_BYTES
|
|
364
|
+
|
|
365
|
+
truncate_bytes(value, [value.bytesize / 2, MIN_TRUNCATED_FIELD_BYTES].max)
|
|
366
|
+
when Array
|
|
367
|
+
return nil if value.size <= 1
|
|
368
|
+
|
|
369
|
+
value.first([value.size / 2, 1].max)
|
|
370
|
+
when Hash
|
|
371
|
+
return nil if value.size <= 1
|
|
372
|
+
|
|
373
|
+
value.first([value.size / 2, 1].max).to_h
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def hard_cap_message!(event)
|
|
378
|
+
return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
|
|
379
|
+
|
|
380
|
+
event[:message] = truncate_bytes(event[:message].to_s, MIN_TRUNCATED_FIELD_BYTES)
|
|
381
|
+
return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
|
|
382
|
+
|
|
383
|
+
message_overhead = safe_json_bytesize(event.merge(message: ''))
|
|
384
|
+
available = MAX_TOTAL_BYTES - message_overhead
|
|
385
|
+
event[:message] = truncate_bytes(event[:message].to_s, [available, 0].max)
|
|
300
386
|
end
|
|
301
387
|
end
|
|
302
388
|
end
|