legion-logging 1.4.3 → 1.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/CLAUDE.md +39 -21
- data/README.md +1 -1
- data/lib/legion/logging/async_writer.rb +35 -10
- data/lib/legion/logging/builder.rb +39 -9
- data/lib/legion/logging/event_builder.rb +73 -0
- data/lib/legion/logging/helper.rb +312 -23
- data/lib/legion/logging/method_tracer.rb +74 -0
- data/lib/legion/logging/methods.rb +114 -66
- data/lib/legion/logging/multi_io.rb +0 -1
- data/lib/legion/logging/redactor.rb +33 -2
- data/lib/legion/logging/settings.rb +2 -2
- data/lib/legion/logging/shipper/file_transport.rb +9 -1
- data/lib/legion/logging/shipper/http_transport.rb +4 -0
- data/lib/legion/logging/shipper.rb +16 -12
- data/lib/legion/logging/tagged_logger.rb +39 -8
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +28 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46129a3dbab733493155c3883562e0b8f867959f9749b0ead2c55096eaaa91f0
|
|
4
|
+
data.tar.gz: e5de12a36bdaa85ceca97cf8936312257ad607393b56fa3663ac0f827d850315
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbedead38cab56af14b780484b110f6f9a8bb6fbc28934478f203d02dea738208fe9fbc63e3c7bdb8d8cc90a33b3328ba39e5059cff85a0ab8be7e83a7068c7f
|
|
7
|
+
data.tar.gz: 337d49c5e424fc9cf487cec98295e4fd80a71da2882a5892c2b478d896cf05e52f6f10632719737df4072abbc555193c1475ae7b01d0e9f51d88ac5df3cc62fc
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Legion::Logging Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.1] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Logging::MethodTracer` module: opt-in TracePoint-based method call tracing with indent-aware call/return output and parameter formatting
|
|
7
|
+
- `Helper.included` / `Helper.extended` hooks auto-attach `MethodTracer` when `MethodTracer::ENABLED` is true
|
|
8
|
+
- `log_exception` now formats backtrace (up to 10 frames + overflow count) inline in the stdout/file log line
|
|
9
|
+
- `Legion::Logging.current_settings` and `.configuration_generation` so helper mixins can refresh memoized tagged loggers after runtime reconfiguration
|
|
10
|
+
- 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`
|
|
11
|
+
- `Methods#emit_tagged` / `TaggedLogger#dispatch` path so component-level loggers can emit with their own level while preserving tagged context
|
|
12
|
+
- Fallback exception event construction in `Helper#handle_exception` when structured exception support is unavailable
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `setup` and `Builder#log_level` now default to `debug`
|
|
16
|
+
- Default helper/tagged logger behavior enables trace and extended metadata
|
|
17
|
+
- `Helper#log` rebuilds memoized `TaggedLogger` instances when logging configuration changes
|
|
18
|
+
- Runtime logger settings take precedence over loaded global settings for helper-mixed components
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `setup(async: true)` now tolerates boolean `logging.async` settings without probing for `buffer_size`
|
|
22
|
+
- Exception stdout/file output now falls back safely when singleton logger helpers are unavailable
|
|
23
|
+
- Structured exception publishing is skipped when the exception writer/EventBuilder path is unavailable
|
|
24
|
+
- `TaggedLogger#unknown` falls back to `debug` output when `Legion::Logging.unknown` is unavailable
|
|
25
|
+
|
|
3
26
|
## [1.4.3] - 2026-04-01
|
|
4
27
|
|
|
5
28
|
### Added
|
|
@@ -176,4 +199,4 @@
|
|
|
176
199
|
- `format_for_elk` produces ELK-compatible event hashes
|
|
177
200
|
|
|
178
201
|
## v1.2.0
|
|
179
|
-
Moving from BitBucket to GitHub. All git history is reset from this point on
|
|
202
|
+
Moving from BitBucket to GitHub. All git history is reset from this point on
|
data/CLAUDE.md
CHANGED
|
@@ -8,40 +8,50 @@
|
|
|
8
8
|
Ruby logging class for the LegionIO framework. Provides colorized console output via Rainbow, structured JSON logging (`format: :json`), and a consistent logging interface across all Legion gems and extensions.
|
|
9
9
|
|
|
10
10
|
**GitHub**: https://github.com/LegionIO/legion-logging
|
|
11
|
-
**Version**: 1.
|
|
11
|
+
**Version**: 1.5.0
|
|
12
12
|
**License**: Apache-2.0
|
|
13
13
|
|
|
14
14
|
## Architecture
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
Legion::Logging (singleton module)
|
|
18
|
-
├── Methods
|
|
19
|
-
├── Builder
|
|
20
|
-
├── AsyncWriter
|
|
21
|
-
├── Hooks
|
|
22
|
-
├── EventBuilder
|
|
23
|
-
├── Helper
|
|
24
|
-
├── Logger
|
|
25
|
-
├── MultiIO
|
|
26
|
-
├──
|
|
27
|
-
├──
|
|
28
|
-
├──
|
|
29
|
-
|
|
18
|
+
├── Methods # Log level methods: debug, info, warn, error, fatal, unknown; log_exception helper
|
|
19
|
+
├── Builder # Output destination (stdout/file), log level, formatter, async: keyword
|
|
20
|
+
├── AsyncWriter # Non-blocking SizedQueue-backed writer thread; fatal calls bypass queue
|
|
21
|
+
├── Hooks # Callback registry for fatal/error/warn events (on_fatal, on_error, on_warn)
|
|
22
|
+
├── EventBuilder # Structured event payload builder (caller, exception, lex, gem metadata); fingerprint for dedup
|
|
23
|
+
├── Helper # Injectable log mixin for LEX extensions (derives logger tags from segments/class)
|
|
24
|
+
├── Logger # Core logger configuration and setup
|
|
25
|
+
├── MultiIO # Write to multiple destinations simultaneously
|
|
26
|
+
├── TaggedLogger # Logger wrapper that prepends structured tags to each message
|
|
27
|
+
├── CategoryRegistry # Registry of named log categories with description and expected_fields
|
|
28
|
+
├── MethodTracer # Tracing module for instrumenting method calls (call/return, formatted args)
|
|
29
|
+
├── SIEMExporter # PHI-redacting SIEM export (Splunk HEC, ELK/OpenSearch)
|
|
30
|
+
├── Shipper # Buffered log event forwarding; sub-transports: FileTransport, HttpTransport
|
|
31
|
+
├── Redactor # PII/PHI + secret pattern redaction; opt-in via logging.redaction.enabled
|
|
32
|
+
└── Version # VERSION constant
|
|
33
|
+
|
|
34
|
+
# Module-level writers (pluggable lambda slots replacing old Hooks for AMQP forwarding)
|
|
35
|
+
Legion::Logging.log_writer # -> lambda(->(event, routing_key:) {})
|
|
36
|
+
Legion::Logging.exception_writer # -> lambda(->(event, routing_key:, headers:, properties:) {})
|
|
30
37
|
```
|
|
31
38
|
|
|
32
39
|
### Key Design Patterns
|
|
33
40
|
|
|
34
|
-
- **Singleton Module**: `Legion::Logging` uses `class << self`
|
|
35
|
-
- **Rainbow Colorization**: Console output uses Rainbow gem for colored terminal output
|
|
36
|
-
- **Setup Method**: `Legion::Logging.setup(
|
|
37
|
-
- **Async by Default**: `setup` enables async logging — calls return immediately. Fatal calls always bypass the queue. `stop_async_writer` flushes and stops on shutdown. Buffer size configurable via `Legion::Settings
|
|
38
|
-
- **Structured JSON**: `format: :json` in settings outputs machine-parseable JSON log lines
|
|
41
|
+
- **Singleton Module**: `Legion::Logging` uses `class << self` — called directly: `Legion::Logging.info("msg")`
|
|
42
|
+
- **Rainbow Colorization**: Console output uses Rainbow gem for colored terminal output. Color auto-disabled in JSON format and when writing to a log file.
|
|
43
|
+
- **Setup Method**: `Legion::Logging.setup(level:, format:, async:, **options)` configures output, level, format, and async mode. Increments `configuration_generation` on each call.
|
|
44
|
+
- **Async by Default**: `setup` enables async logging — calls return immediately. Fatal calls always bypass the queue. `stop_async_writer` flushes and stops on shutdown. Buffer size configurable via `Legion::Settings[:logging][:async][:buffer_size]` (default 10,000). Back-pressure: callers block when buffer is full.
|
|
45
|
+
- **Structured JSON**: `format: :json` in settings outputs machine-parseable JSON log lines (disables color)
|
|
39
46
|
- **Shared Interface**: Same method signature (`info`, `warn`, `error`, etc.) across all Legion components
|
|
40
47
|
- **MultiIO**: Splits writes to stdout and a log file simultaneously (used by Builder when `log_file` is set)
|
|
41
48
|
- **SIEMExporter**: PHI redaction (SSN, phone, MRN, DOB patterns), `export_to_splunk` (HEC), `format_for_elk`
|
|
42
|
-
- **Hook Callbacks**: `on_fatal`, `on_error`, `on_warn` register procs called after each log at those levels. Hooks are gated by `enable_hooks!`/`disable_hooks!`. Hook failures are silently rescued
|
|
43
|
-
- **EventBuilder**: Builds structured event hashes from log context (caller location, exception info, lex identity, gem metadata). All from in-memory data, zero IO.
|
|
49
|
+
- **Hook Callbacks**: `on_fatal`, `on_error`, `on_warn` register procs called after each log at those levels. Hooks are gated by `enable_hooks!`/`disable_hooks!`. Hook failures are silently rescued. Hooks fire on the async writer thread; event context captured on caller thread.
|
|
50
|
+
- **EventBuilder**: Builds structured event hashes from log context (caller location, exception info, lex identity, gem metadata). All from in-memory data, zero IO. `fingerprint` produces MD5 for dedup in log aggregation.
|
|
44
51
|
- **Helper mixin**: `Legion::Logging::Helper` is injectable into LEX extensions. Derives logger tags from `segments`, `lex_filename`, or class name. Passes through `settings[:logger]` config when available.
|
|
52
|
+
- **Writer Lambdas**: `log_writer` and `exception_writer` are module-level lambda slots for forwarding to external systems (AMQP, etc.). Default implementations are no-ops. Set via `Legion::Logging.log_writer = lambda`.
|
|
53
|
+
- **CategoryRegistry**: Named log categories with description and expected_fields. Register via `Legion::Logging.register_category`. Used for structured log validation.
|
|
54
|
+
- **Redactor**: Opt-in PII/PHI redaction (`logging.redaction.enabled: true`). Guards against Settings recursive init via `@loader` ivar check. Patterns: SSN, phone, MRN, DOB, Vault tokens, JWTs, bearer tokens, lease IDs.
|
|
45
55
|
|
|
46
56
|
## Dependencies
|
|
47
57
|
|
|
@@ -62,7 +72,15 @@ Legion::Logging (singleton module)
|
|
|
62
72
|
| `lib/legion/logging/multi_io.rb` | Multi-output IO (write to multiple destinations simultaneously) |
|
|
63
73
|
| `lib/legion/logging/siem_exporter.rb` | PHI-redacting SIEM export helpers (Splunk HEC, ELK format) |
|
|
64
74
|
| `lib/legion/logging/hooks.rb` | Callback registry (fatal/error/warn hook arrays, enable/disable/clear) |
|
|
65
|
-
| `lib/legion/logging/event_builder.rb` | Structured event payload builder |
|
|
75
|
+
| `lib/legion/logging/event_builder.rb` | Structured event payload builder; `fingerprint` for MD5 dedup |
|
|
76
|
+
| `lib/legion/logging/tagged_logger.rb` | Logger wrapper that prepends structured tags to each message |
|
|
77
|
+
| `lib/legion/logging/category_registry.rb` | Named log category registration and lookup |
|
|
78
|
+
| `lib/legion/logging/method_tracer.rb` | Method call tracing instrumentation (call/return, formatted args) |
|
|
79
|
+
| `lib/legion/logging/shipper.rb` | Buffered log event forwarding to external systems |
|
|
80
|
+
| `lib/legion/logging/shipper/file_transport.rb` | File-based log shipper transport |
|
|
81
|
+
| `lib/legion/logging/shipper/http_transport.rb` | HTTP-based log shipper transport |
|
|
82
|
+
| `lib/legion/logging/siem_exporter.rb` | PHI-redacting SIEM export (Splunk HEC, ELK format) |
|
|
83
|
+
| `lib/legion/logging/redactor.rb` | PII/PHI + secret pattern redaction (opt-in) |
|
|
66
84
|
| `lib/legion/logging/version.rb` | VERSION constant |
|
|
67
85
|
|
|
68
86
|
## Role in LegionIO
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Logging module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides colorized console output via Rainbow, structured JSON logging, multi-output IO, and a consistent logging interface across all Legion gems and extensions.
|
|
4
4
|
|
|
5
|
-
**Version**: 1.
|
|
5
|
+
**Version**: 1.5.0
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -5,40 +5,58 @@ require_relative 'methods'
|
|
|
5
5
|
module Legion
|
|
6
6
|
module Logging
|
|
7
7
|
class AsyncWriter
|
|
8
|
-
LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx)
|
|
8
|
+
LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx, :caller_trace)
|
|
9
9
|
SHUTDOWN = :shutdown
|
|
10
10
|
|
|
11
|
+
attr_reader :logger
|
|
12
|
+
|
|
11
13
|
def initialize(logger, buffer_size: 10_000)
|
|
12
14
|
@logger = logger
|
|
15
|
+
@buffer_size = buffer_size
|
|
13
16
|
@queue = SizedQueue.new(buffer_size)
|
|
14
17
|
@thread = nil
|
|
18
|
+
@state_mutex = Mutex.new
|
|
19
|
+
@accepting = true
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def start
|
|
18
23
|
return if @thread&.alive?
|
|
19
24
|
|
|
25
|
+
@state_mutex.synchronize { @accepting = true }
|
|
20
26
|
drain
|
|
27
|
+
@queue = SizedQueue.new(@buffer_size)
|
|
21
28
|
@thread = Thread.new { consume }
|
|
22
29
|
@thread.name = 'legion-log-writer'
|
|
23
30
|
@thread.abort_on_exception = false
|
|
24
31
|
end
|
|
25
32
|
|
|
33
|
+
# rubocop:disable Naming/PredicateMethod
|
|
26
34
|
def stop(timeout: 2)
|
|
27
|
-
|
|
35
|
+
@state_mutex.synchronize { @accepting = false }
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
unless @thread&.alive?
|
|
38
|
+
drain
|
|
39
|
+
@thread = nil
|
|
40
|
+
return true
|
|
33
41
|
end
|
|
34
|
-
|
|
35
|
-
@
|
|
36
|
-
|
|
42
|
+
|
|
43
|
+
@queue.close
|
|
44
|
+
timeout ? @thread.join(timeout) : @thread.join
|
|
45
|
+
return false if @thread&.alive?
|
|
46
|
+
|
|
47
|
+
@thread = nil
|
|
48
|
+
true
|
|
37
49
|
end
|
|
38
50
|
|
|
39
51
|
def push(entry)
|
|
52
|
+
return false unless accepting?
|
|
53
|
+
|
|
40
54
|
@queue.push(entry)
|
|
55
|
+
true
|
|
56
|
+
rescue ClosedQueueError
|
|
57
|
+
false
|
|
41
58
|
end
|
|
59
|
+
# rubocop:enable Naming/PredicateMethod
|
|
42
60
|
|
|
43
61
|
def alive?
|
|
44
62
|
@thread&.alive? || false
|
|
@@ -49,7 +67,7 @@ module Legion
|
|
|
49
67
|
def consume
|
|
50
68
|
loop do
|
|
51
69
|
entry = @queue.pop
|
|
52
|
-
break if entry == SHUTDOWN
|
|
70
|
+
break if entry.nil? || entry == SHUTDOWN
|
|
53
71
|
|
|
54
72
|
write_entry(entry)
|
|
55
73
|
end
|
|
@@ -58,8 +76,10 @@ module Legion
|
|
|
58
76
|
def write_entry(entry)
|
|
59
77
|
prev_segments = Thread.current[:legion_log_segments]
|
|
60
78
|
prev_method_ctx = Thread.current[:legion_log_method]
|
|
79
|
+
prev_caller = Thread.current[:legion_log_caller]
|
|
61
80
|
Thread.current[:legion_log_segments] = entry.segments
|
|
62
81
|
Thread.current[:legion_log_method] = entry.method_ctx
|
|
82
|
+
Thread.current[:legion_log_caller] = entry.caller_trace
|
|
63
83
|
@logger.send(entry.level, entry.message)
|
|
64
84
|
fire_writer(entry) if entry.writer_context
|
|
65
85
|
rescue StandardError => e
|
|
@@ -67,6 +87,7 @@ module Legion
|
|
|
67
87
|
ensure
|
|
68
88
|
Thread.current[:legion_log_segments] = prev_segments
|
|
69
89
|
Thread.current[:legion_log_method] = prev_method_ctx
|
|
90
|
+
Thread.current[:legion_log_caller] = prev_caller
|
|
70
91
|
end
|
|
71
92
|
|
|
72
93
|
def drain
|
|
@@ -78,6 +99,10 @@ module Legion
|
|
|
78
99
|
nil
|
|
79
100
|
end
|
|
80
101
|
|
|
102
|
+
def accepting?
|
|
103
|
+
@state_mutex.synchronize { @accepting }
|
|
104
|
+
end
|
|
105
|
+
|
|
81
106
|
def fire_writer(entry)
|
|
82
107
|
ctx = entry.writer_context
|
|
83
108
|
event = ctx[:event]
|
|
@@ -41,7 +41,7 @@ module Legion
|
|
|
41
41
|
def text_format(include_pid: false, **options)
|
|
42
42
|
log.formatter = proc do |severity, datetime, _progname, msg|
|
|
43
43
|
lex_name = resolve_lex_tag(options)
|
|
44
|
-
runner_trace = build_runner_trace if lex_name
|
|
44
|
+
runner_trace = Thread.current[:legion_log_caller] || build_runner_trace if lex_name
|
|
45
45
|
|
|
46
46
|
string = "[#{datetime}]"
|
|
47
47
|
string.concat("[#{::Process.pid}]") if include_pid
|
|
@@ -69,8 +69,7 @@ module Legion
|
|
|
69
69
|
tag
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def build_runner_trace
|
|
73
|
-
loc = caller_locations(6, 1)&.first
|
|
72
|
+
def build_runner_trace(loc = caller_locations(6, 1)&.first)
|
|
74
73
|
return unless loc
|
|
75
74
|
|
|
76
75
|
path = loc.to_s.split('/').last(2)
|
|
@@ -91,16 +90,25 @@ module Legion
|
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
def set_log(logfile: nil, log_stdout: nil, **)
|
|
93
|
+
previous_log = @log
|
|
94
|
+
|
|
94
95
|
if logfile && log_stdout != false
|
|
95
96
|
path = prepare_log_path(logfile)
|
|
96
97
|
require_relative 'multi_io'
|
|
97
|
-
|
|
98
|
+
file = File.new(path, 'a')
|
|
99
|
+
file.sync = true
|
|
100
|
+
io = MultiIO.new($stdout, file)
|
|
98
101
|
@log = ::Logger.new(io)
|
|
99
102
|
elsif logfile
|
|
100
|
-
|
|
103
|
+
file = File.new(prepare_log_path(logfile), 'a')
|
|
104
|
+
file.sync = true
|
|
105
|
+
@log = ::Logger.new(file)
|
|
101
106
|
else
|
|
102
107
|
@log = ::Logger.new($stdout)
|
|
103
108
|
end
|
|
109
|
+
|
|
110
|
+
close_replaced_log(previous_log)
|
|
111
|
+
@log
|
|
104
112
|
end
|
|
105
113
|
|
|
106
114
|
def prepare_log_path(path)
|
|
@@ -113,7 +121,7 @@ module Legion
|
|
|
113
121
|
log.level
|
|
114
122
|
end
|
|
115
123
|
|
|
116
|
-
def log_level(level = '
|
|
124
|
+
def log_level(level = 'debug')
|
|
117
125
|
log.level = case level
|
|
118
126
|
when 'trace', 'debug'
|
|
119
127
|
::Logger::DEBUG
|
|
@@ -140,19 +148,41 @@ module Legion
|
|
|
140
148
|
(@async == true && @async_writer&.alive?) || false
|
|
141
149
|
end
|
|
142
150
|
|
|
151
|
+
# rubocop:disable Naming/PredicateMethod
|
|
143
152
|
def start_async_writer(buffer_size: 10_000)
|
|
144
153
|
require_relative 'async_writer'
|
|
145
|
-
|
|
154
|
+
return false if @async_writer&.alive? && stop_async_writer == false
|
|
155
|
+
|
|
146
156
|
@async_writer = AsyncWriter.new(log, buffer_size: buffer_size)
|
|
147
157
|
@async_writer.start
|
|
148
158
|
@async = true
|
|
159
|
+
true
|
|
149
160
|
end
|
|
150
161
|
|
|
151
162
|
def stop_async_writer
|
|
152
163
|
writer = @async_writer
|
|
153
|
-
|
|
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)
|
|
154
169
|
@async = false
|
|
155
|
-
|
|
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)
|
|
156
186
|
end
|
|
157
187
|
end
|
|
158
188
|
end
|
|
@@ -11,6 +11,29 @@ 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
|
|
14
37
|
|
|
15
38
|
GEM_SPEC_CACHE_MUTEX = Mutex.new
|
|
16
39
|
private_constant :GEM_SPEC_CACHE_MUTEX
|
|
@@ -310,6 +333,56 @@ module Legion
|
|
|
310
333
|
return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
|
|
311
334
|
|
|
312
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)
|
|
313
386
|
end
|
|
314
387
|
end
|
|
315
388
|
end
|