legion-logging 1.5.6 → 1.5.19
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 +84 -0
- data/CLAUDE.md +18 -10
- data/README.md +44 -10
- data/lib/legion/logging/async_writer.rb +125 -36
- data/lib/legion/logging/builder.rb +65 -37
- data/lib/legion/logging/event_builder.rb +9 -6
- data/lib/legion/logging/helper.rb +11 -19
- data/lib/legion/logging/method_tracer.rb +13 -2
- data/lib/legion/logging/methods.rb +152 -53
- data/lib/legion/logging/output_logger.rb +60 -0
- data/lib/legion/logging/redactor.rb +3 -2
- data/lib/legion/logging/settings.rb +21 -5
- data/lib/legion/logging/shipper/file_transport.rb +2 -1
- data/lib/legion/logging/shipper/http_transport.rb +4 -2
- data/lib/legion/logging/shipper.rb +118 -69
- data/lib/legion/logging/siem_exporter.rb +1 -1
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +31 -30
- 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: '037790df316dd01a4f090a1d02a4b07fab65feac2d0c70a501d949d57fd45276'
|
|
4
|
+
data.tar.gz: af6fda1d1af5fe004fe8e4b76c225aa997f4075aed19b8d557f4090cec9e7770
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 659f1952bc1c331c74528153276b9a593eb1d775c67fbd57797434e78f1f016612ca2a2e0322ab6ad041f7101efede0a94e803c388dedf96608727431b5c303c
|
|
7
|
+
data.tar.gz: 9f4150366ac4f64da3b1c14a429058820de2cb639dd810974f6448a5ca78fa7e32d5c13f23100eb0d21802143091df232b0ffd89263313f00b7df1355341b547
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# Legion::Logging Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.19] - 2026-10-29
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Document the complete async contract: bounded backpressure, producer snapshots, worker-side preparation, lifecycle deadlines, reentrant logging, and explicit synchronous boundaries (#22).
|
|
7
|
+
- Correct writer callback signatures and describe delivery acknowledgement limits, retained Shipper retries, and raw Logger compatibility (#22).
|
|
8
|
+
|
|
9
|
+
## [1.5.18] - 2026-10-29
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Preserve producer-side raw Logger admission and message/progname normalization: accepted `with_level` records are not filtered again, and a block's nil result reaches the formatter unchanged (#22).
|
|
13
|
+
- Pass captured producer time to raw Logger formatters instead of the later worker dispatch time (#22).
|
|
14
|
+
|
|
15
|
+
## [1.5.17] - 2026-10-29
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Make async regression cleanup nil-safe so setup failures are not replaced by secondary errors while releasing worker gates (#22; PR #23 review feedback).
|
|
19
|
+
|
|
20
|
+
## [1.5.16] - 2026-10-29
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Preserve producer timestamps in text/JSON output and producer thread IDs in JSON, including queued standard Logger calls (#22).
|
|
24
|
+
- Snapshot mutable raw Logger messages and context before enqueueing so later caller mutation cannot change accepted output.
|
|
25
|
+
|
|
26
|
+
## [1.5.15] - 2026-10-29
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Move normal log and both exception APIs' formatting, redaction, event enrichment, fingerprinting, and envelope preparation onto the async worker (#22).
|
|
30
|
+
- Capture producer call sites, timestamps, thread/session context, and shallow input snapshots before enqueueing; preserve component-level overrides during worker output.
|
|
31
|
+
- Honor configurable forwarding levels at `logging.transport.log_levels`, retaining warn/error/fatal by default and avoiding duplicate synchronous event construction.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- Avoid implicitly loading Settings from logging and shipping; early redaction defaults no longer hide custom patterns loaded later.
|
|
35
|
+
|
|
36
|
+
## [1.5.14] - 2026-10-29
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Serialize logger-owned lifecycle transitions so concurrent startup cannot orphan a writer and discard its accepted backlog (#22).
|
|
40
|
+
- Reject contending lifecycle calls without blocking callbacks behind their own shutdown join.
|
|
41
|
+
- Retry retained shipping batches on a subsequent shutdown call after a previous delivery failure.
|
|
42
|
+
|
|
43
|
+
## [1.5.13] - 2026-10-29
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- Keep startup and shutdown draining on the writer, including accepted work without a running consumer; apply shutdown deadlines without synchronous fallback (#22).
|
|
47
|
+
- Retain timed-out workers and queues, reject writes after shutdown, and avoid joining the writer from its own callback.
|
|
48
|
+
- Stop the existing writer before reconfiguring outputs; a failed shutdown leaves the configuration intact instead of opening a synchronous bypass.
|
|
49
|
+
|
|
50
|
+
## [1.5.12] - 2026-10-29
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- Route opt-in MethodTracer output through configured async logging instead of direct producer-thread stdout writes (#22).
|
|
54
|
+
- Keep failed shipping diagnostics out of the forwarding pipeline to avoid logging-to-shipper feedback loops, and add explicit HTTP write timeouts.
|
|
55
|
+
- Clarify that direct transport/export APIs, MultiIO, and hook dispatch are synchronous primitives; their async callers own scheduling.
|
|
56
|
+
|
|
57
|
+
## [1.5.11] - 2026-10-29
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- Move threshold-triggered and explicit Shipper delivery onto one background worker; `flush(timeout:)` waits for delivery without performing sink IO on its caller (#22).
|
|
61
|
+
- Bound accepted and in-flight shipping events with `logging.shipper.buffer_size` (default 10,000), preserve failed batches for retry, and wake the worker for flush/shutdown instead of sleeping through requests.
|
|
62
|
+
|
|
63
|
+
## [1.5.10] - 2026-10-29
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
- Prevent writer callbacks and hooks from waiting on their own full queue: nested messages are written locally on the writer without recursively forwarding them (#22).
|
|
67
|
+
- Preserve nested exception events as local JSON lines and bypass recursive formatting when necessary, without growing an overflow queue or dropping nested messages.
|
|
68
|
+
|
|
69
|
+
## [1.5.9] - 2026-10-29
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- Keep the standard Ruby Logger interface returned by `log`, including `add`, `log`, and raw append, on the configured async writer; preserve producer context for Sinatra/APM consumers (#22).
|
|
73
|
+
- Route instance tracing through async logging and raise `ClosedQueueError` for rejected async writes instead of silently performing producer-thread IO.
|
|
74
|
+
|
|
75
|
+
## [1.5.8] - 2026-10-29
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
- Queue structured exception publication for both `log_exception` and Helper `handle_exception`, keeping exception-writer callbacks off producer threads when async logging is enabled (#22).
|
|
79
|
+
- Preserve captured exception metadata and callback envelopes while retaining synchronous publication for explicit synchronous logging.
|
|
80
|
+
|
|
81
|
+
## [1.5.7] - 2026-10-29
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
- Document bounded, lossless-in-process queue backpressure as the intentional async saturation policy: producers wait rather than silently dropping logs or growing the queue without a bound (#22).
|
|
85
|
+
- Correct async fatal routing and the buffer-size configuration example.
|
|
86
|
+
|
|
3
87
|
## [1.5.6] - 2026-06-01
|
|
4
88
|
|
|
5
89
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -2,45 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
Structured logging framework for LegionIO. Provides colorized console output (Rainbow), structured JSON logging, and a consistent interface across all Legion gems and extensions.
|
|
4
4
|
|
|
5
|
-
**GitHub**: https://github.com/LegionIO/legion-logging
|
|
6
|
-
**Version**: 1.5.3
|
|
7
|
-
|
|
8
5
|
## Architecture
|
|
9
6
|
|
|
10
7
|
```
|
|
11
8
|
Legion::Logging (singleton module)
|
|
12
9
|
├── Methods # debug, info, warn, error, fatal, unknown; log_exception
|
|
13
10
|
├── Builder # Output destination, log level, formatter, async: keyword
|
|
14
|
-
├── AsyncWriter #
|
|
11
|
+
├── AsyncWriter # Bounded, backpressured SizedQueue-backed writer thread
|
|
12
|
+
├── OutputLogger # Queues standard Logger add/log/<< calls
|
|
15
13
|
├── Hooks # Callback registry for fatal/error/warn events
|
|
16
14
|
├── EventBuilder # Structured event payload + fingerprint for dedup
|
|
17
15
|
├── Helper # Injectable log mixin for LEX extensions
|
|
18
16
|
├── TaggedLogger # Prepends structured tags to each message
|
|
19
17
|
├── CategoryRegistry # Named log categories with expected_fields
|
|
20
18
|
├── SIEMExporter # PHI-redacting SIEM export (Splunk HEC, ELK)
|
|
21
|
-
├── Shipper #
|
|
19
|
+
├── Shipper # Bounded batch-delivery worker (file/HTTP transports)
|
|
22
20
|
├── Redactor # PII/PHI + secret pattern redaction (opt-in)
|
|
23
|
-
└── MultiIO #
|
|
21
|
+
└── MultiIO # Synchronous, sequential writes to multiple destinations
|
|
24
22
|
|
|
25
23
|
# Module-level writer lambdas (pluggable forwarding slots)
|
|
26
|
-
Legion::Logging.log_writer # ->(event, routing_key:) {}
|
|
24
|
+
Legion::Logging.log_writer # ->(event, routing_key:, headers:, properties:) {}
|
|
27
25
|
Legion::Logging.exception_writer # ->(event, routing_key:, headers:, properties:) {}
|
|
28
26
|
```
|
|
29
27
|
|
|
30
28
|
## Key Patterns
|
|
31
29
|
|
|
32
30
|
- **Singleton module** — `class << self`; called directly: `Legion::Logging.info("msg")`
|
|
33
|
-
- **Async by default** — `setup`
|
|
31
|
+
- **Async by default** — `setup` queues all levels, including fatal, plus structured exception publication and raw Logger calls. Buffer size via `Settings[:logging][:async][:buffer_size]` (default 10,000 entries, not bytes). Saturation blocks producers rather than dropping logs or growing the queue without bound.
|
|
34
32
|
- **Structured JSON** — `format: :json` outputs machine-parseable JSON lines (disables color)
|
|
33
|
+
- **Producer/worker boundary** — Producers evaluate message blocks and capture caller/context plus shallow snapshots. Formatting, event enrichment, fingerprinting, and publication run on the worker. Nested payload objects are not deep-copied. Built-in output retains producer time/thread; raw Logger output retains admission, normalized payload, and formatter time.
|
|
34
|
+
- **Lifecycle** — `stop_async_writer(timeout: 2)` closes admission and drains on the worker; timeout or lifecycle contention returns false. No caller-thread drain or synchronous rejection fallback. Stopped async loggers raise `ClosedQueueError` until restart or explicit `setup(async: false)`. Setup file operations remain synchronous.
|
|
35
|
+
- **Delivery semantics** — Successful shutdown means processing finished, not durable delivery or external acknowledgement. AsyncWriter diagnoses failures locally without retry; Shipper retains failed batches for retry.
|
|
35
36
|
- **Helper mixin** — `Legion::Logging::Helper` injects into LEX extensions; derives tags from `segments`, `lex_filename`, or class name
|
|
36
37
|
- **Writer lambdas** — `log_writer` and `exception_writer` are module-level lambda slots for forwarding to external systems (AMQP, etc.). Default no-ops.
|
|
37
38
|
- **Redactor** — Opt-in (`logging.redaction.enabled: true`). Patterns: SSN, phone, MRN, DOB, Vault tokens, JWTs, bearer tokens, lease IDs. Guards against Settings recursive init.
|
|
38
39
|
- **Hook callbacks** — `on_fatal`, `on_error`, `on_warn` register procs; gated by `enable_hooks!`/`disable_hooks!`; fire on async writer thread
|
|
40
|
+
- **Reentrant logging** — Callback/hook logs stay local on the worker without recursively forwarding or waiting on its own queue. Nested exception events and recursive formatter diagnostics use local JSON lines.
|
|
41
|
+
- **Forwarding levels** — `logging.transport.log_levels` accepts strings/symbols; absent or empty lists default to warn/error/fatal. Settings guards use defaults while Settings is unloaded rather than initializing it.
|
|
42
|
+
- **Synchronous APIs** — `MultiIO`, direct transport delivery, SIEM export, and direct hook dispatch remain synchronous. Shipper filtering/redaction run on its caller; bounded batch delivery runs on its worker.
|
|
39
43
|
- **EventBuilder** — Structured event hashes from log context (caller, exception, lex identity). `fingerprint` produces MD5 for dedup.
|
|
40
44
|
|
|
41
45
|
## Role in LegionIO
|
|
42
46
|
|
|
43
47
|
Foundational gem — dependency of `legion-cache`, `legion-data`, and `LegionIO`. First module initialized during `Legion::Service` startup.
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
## Backtrace Limit
|
|
50
|
+
|
|
51
|
+
- `settings[:logging][:backtrace_limit]` — `nil` = full trace, `0` = suppress entirely, `N` = cap at N frames
|
|
52
|
+
- Per-call override: `log_exception(err, backtrace_limit: 5)`
|
|
53
|
+
- Read at runtime via `Legion::Settings.dig(:logging, :backtrace_limit)` after checking Settings is loaded; otherwise use the default without triggering initialization.
|
|
54
|
+
|
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.
|
|
5
|
+
**Version**: 1.5.19
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -33,7 +33,7 @@ Legion::Logging.fatal('critical failure')
|
|
|
33
33
|
|
|
34
34
|
### Async Logging
|
|
35
35
|
|
|
36
|
-
By default, `setup` enables async logging
|
|
36
|
+
By default, `setup` enables async logging: log calls enqueue output for a background writer, including fatal messages. Async does not mean wait-free. The queue holds at most 10,000 entries by default, and producers wait for capacity when it fills.
|
|
37
37
|
|
|
38
38
|
```ruby
|
|
39
39
|
# Async is on by default
|
|
@@ -43,10 +43,30 @@ Legion::Logging.setup(level: 'info')
|
|
|
43
43
|
Legion::Logging.setup(level: 'info', async: false)
|
|
44
44
|
|
|
45
45
|
# Configure buffer size via Legion::Settings
|
|
46
|
-
# Legion::Settings[:logging
|
|
46
|
+
# Legion::Settings[:logging][:async] = { buffer_size: 20_000 }
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Bounded backpressure is intentional: saturation blocks callers rather than dropping logs or allowing unbounded memory growth. A stalled destination can therefore stall producers once the queue fills. Buffer capacity limits queued entries, not their total byte size, and does not guarantee delivery across process termination.
|
|
50
|
+
|
|
51
|
+
Use `Legion::Logging.stop_async_writer(timeout: 2)` during shutdown. It closes admission and asks the worker to drain accepted entries, returning `true` only after that worker exits. A deadline expiry returns `false` without killing the worker or draining on the caller; release the blocked destination and retry shutdown. Calling shutdown from a writer callback requests closure but returns `false` rather than joining itself.
|
|
52
|
+
|
|
53
|
+
A successful stop means queued entries have been processed, not that a sink durably stored them or an external service acknowledged them. AsyncWriter reports sink/callback failures locally and does not automatically retry them. The Shipper's retained-batch retry behavior is separate.
|
|
54
|
+
|
|
55
|
+
Stopped async loggers reject new writes until explicitly restarted or configured with `setup(async: false)`. Reconfiguration stops the previous worker before replacing its output. `setup(shutdown_timeout: 2, ...)` returns `false` if that drain times out and does not change the existing output; it does not silently enter synchronous mode.
|
|
56
|
+
|
|
57
|
+
Lifecycle transitions are serialized per logger. A competing setup/start/stop returns `false` instead of creating overlapping workers or blocking a callback behind its own shutdown. Retry the control operation after the active transition completes.
|
|
58
|
+
|
|
59
|
+
Configuration is a synchronous control path: opening or closing owned log files can still block in `setup`. Shutdown deadlines bound worker drain/join, not every filesystem operation.
|
|
60
|
+
|
|
61
|
+
Producers evaluate message blocks and capture caller/context plus shallow snapshots of strings, arrays, hashes, and exceptions. The worker performs formatting, redaction, structured-event enrichment, fingerprinting, and envelope construction. Nested payload objects are not deep-copied. Explicit caller tracing and application-provided message/settings methods can still perform producer-side work.
|
|
62
|
+
|
|
63
|
+
Text and JSON timestamps represent enqueue time, not time spent waiting for a destination. JSON thread IDs identify the producer. Standard Logger calls use the same shallow snapshot policy for messages and context; custom raw Logger formatters receive the producer timestamp. The worker preserves producer-side `with_level` admission and already-resolved message/progname values, including a block's nil result.
|
|
64
|
+
|
|
65
|
+
Forwarding defaults to warn/error/fatal. Set `logging.transport.log_levels` to an array of strings or symbols to change which locally emitted levels reach `log_writer` and Hooks; absent or empty lists retain the defaults. Library-owned Settings lookups use defaults while Settings is unloaded rather than triggering initialization.
|
|
66
|
+
|
|
67
|
+
The standard Ruby Logger interface returned by `Legion::Logging.log` also queues output in async mode, including `add`, `log`, and `<<`. Instance tracing uses the same async path. If an async writer is unavailable or rejects an entry during shutdown, logging raises `ClosedQueueError` rather than falling back to synchronous IO. Explicit synchronous mode retains direct output.
|
|
68
|
+
|
|
69
|
+
Logs generated by a writer callback or hook are written locally on that worker rather than queued behind themselves or forwarded recursively. Nested exception events and recursive formatter diagnostics use local JSON lines. This keeps bounded backpressure for producers without allowing the sole consumer to deadlock on its own queue.
|
|
50
70
|
|
|
51
71
|
### Structured JSON Output
|
|
52
72
|
|
|
@@ -60,12 +80,26 @@ This is useful for log aggregation pipelines (Elasticsearch, Splunk, etc.).
|
|
|
60
80
|
|
|
61
81
|
### Multi-Output IO
|
|
62
82
|
|
|
63
|
-
`Legion::Logging::MultiIO` writes to
|
|
83
|
+
`Legion::Logging::MultiIO` writes to destinations sequentially—for example, stdout followed by a file. It is a synchronous IO primitive used by the async writer, not a parallel executor. A slow destination delays that worker and can eventually cause queue backpressure.
|
|
84
|
+
|
|
85
|
+
Opt-in `MethodTracer` output uses the configured logging queue rather than writing directly to the traced application's stdout. Hook dispatch is synchronous on its invoking thread; normal async logging invokes hooks on the writer, not on producers.
|
|
86
|
+
|
|
87
|
+
### Buffered Shipping
|
|
88
|
+
|
|
89
|
+
Explicitly require `legion/logging/shipper` and enable `logging.shipper.enabled` to use the file or HTTP batch shipper. `Shipper.ship(event)` starts its worker automatically and never performs threshold-triggered delivery on the producer. Filtering and redaction still run on the calling thread; batch delivery runs on the worker. Set `logging.shipper.transport` to `file` or `http`.
|
|
90
|
+
|
|
91
|
+
Defaults: `batch_size: 100`, `flush_interval: 5` seconds, and `buffer_size: 10_000`. The capacity includes in-flight events; producers wait when it is full. Failed batches remain queued in order for retry.
|
|
92
|
+
|
|
93
|
+
`Shipper.flush(timeout: 5)` requests worker delivery and waits up to the supplied deadline; it returns `false` on timeout or delivery failure. `Shipper.stop(timeout: 5)` wakes and joins the worker without moving delivery onto the caller. A timeout retains the worker and accepted events; check the return value before assuming delivery completed. If shutdown exits after a delivery failure, retrying `stop` starts a drainer for the retained batch.
|
|
94
|
+
|
|
95
|
+
`FileTransport.ship_batch` and `HttpTransport.ship_batch` are intentionally synchronous delivery primitives; use `Shipper.ship` for producer-side asynchronous shipping. Sink failures are reported locally to stderr instead of being forwarded back into the failing shipper.
|
|
64
96
|
|
|
65
97
|
### SIEM Export
|
|
66
98
|
|
|
67
99
|
`Legion::Logging::SIEMExporter` provides PHI-redacting export helpers for security event pipelines:
|
|
68
100
|
|
|
101
|
+
`export_to_splunk` is an explicitly synchronous request/response API. Use the HTTP Shipper for asynchronous delivery. Both HTTP implementations configure 5-second connection and 10-second read/write timeouts; these are phase timeouts, not a total request deadline.
|
|
102
|
+
|
|
69
103
|
```ruby
|
|
70
104
|
# Redact PHI patterns (SSN, phone, MRN, DOB) from a string
|
|
71
105
|
clean = Legion::Logging::SIEMExporter.redact_phi(raw_message)
|
|
@@ -96,7 +130,7 @@ end
|
|
|
96
130
|
|
|
97
131
|
### Exception Logging
|
|
98
132
|
|
|
99
|
-
`log_exception` provides a single call for complete exception logging with component context. It writes a human-readable exception line
|
|
133
|
+
`log_exception` provides a single call for complete exception logging with component context. It writes a human-readable exception line and publishes a structured exception event when an `exception_writer` is configured. With async logging enabled, both output and exception publication run on the writer thread; Helper `handle_exception` uses the same publication path. Bounded queue backpressure still applies.
|
|
100
134
|
|
|
101
135
|
```ruby
|
|
102
136
|
begin
|
|
@@ -112,7 +146,7 @@ rescue StandardError => e
|
|
|
112
146
|
end
|
|
113
147
|
```
|
|
114
148
|
|
|
115
|
-
The
|
|
149
|
+
The human-readable log line includes the full Ruby backtrace unless `backtrace_limit` is configured. Legion does not replace the tail with `... N more`.
|
|
116
150
|
|
|
117
151
|
Structured exception events include:
|
|
118
152
|
|
|
@@ -126,15 +160,15 @@ Structured exception events include:
|
|
|
126
160
|
|
|
127
161
|
### Writer Lambdas
|
|
128
162
|
|
|
129
|
-
`log_writer` and `exception_writer` are pluggable lambda slots
|
|
163
|
+
`log_writer` and `exception_writer` are pluggable lambda slots for forwarding events to external systems. They coexist with Hooks; normal async logging invokes both on the writer thread:
|
|
130
164
|
|
|
131
165
|
```ruby
|
|
132
166
|
Legion::Logging.exception_writer = lambda do |payload, routing_key:, headers:, properties:|
|
|
133
167
|
publish_to_amqp(payload, routing_key:, headers:, properties:)
|
|
134
168
|
end
|
|
135
169
|
|
|
136
|
-
Legion::Logging.log_writer = lambda do |context, routing_key:|
|
|
137
|
-
publish_log(context, routing_key:)
|
|
170
|
+
Legion::Logging.log_writer = lambda do |context, routing_key:, headers:, properties:|
|
|
171
|
+
publish_log(context, routing_key:, headers:, properties:)
|
|
138
172
|
end
|
|
139
173
|
```
|
|
140
174
|
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
3
4
|
require_relative 'methods'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Logging
|
|
7
8
|
class AsyncWriter
|
|
8
9
|
LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx, :caller_trace,
|
|
9
|
-
:conv_id, :request_id, :exchange_id, :chain_id)
|
|
10
|
-
|
|
10
|
+
:conv_id, :request_id, :exchange_id, :chain_id, :force)
|
|
11
|
+
EventEntry = ::Data.define(:callback, :event, :options)
|
|
12
|
+
CallEntry = ::Data.define(:receiver, :method_name, :arguments, :keywords, :context)
|
|
13
|
+
OutputEntry = ::Data.define(:severity, :message, :progname, :segments, :method_ctx, :caller_trace,
|
|
14
|
+
:conv_id, :request_id, :exchange_id, :chain_id, :origin)
|
|
11
15
|
THREAD_KEYS = %i[
|
|
12
16
|
legion_log_segments legion_log_method legion_log_caller
|
|
13
17
|
legion_log_conv_id legion_log_request_id legion_log_exchange_id legion_log_chain_id
|
|
14
18
|
].freeze
|
|
19
|
+
CALL_THREAD_KEYS = (THREAD_KEYS + %i[legion_context legion_session_context legion_log_origin]).freeze
|
|
15
20
|
|
|
16
21
|
attr_reader :logger
|
|
17
22
|
|
|
23
|
+
def self.snapshot_value(value:)
|
|
24
|
+
return value if value.nil? || value.frozen?
|
|
25
|
+
return value unless value.is_a?(String) || value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(Exception)
|
|
26
|
+
|
|
27
|
+
value.dup.freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
18
30
|
def initialize(logger, buffer_size: 10_000)
|
|
19
31
|
@logger = logger
|
|
20
32
|
@buffer_size = buffer_size
|
|
@@ -24,36 +36,55 @@ module Legion
|
|
|
24
36
|
@accepting = true
|
|
25
37
|
end
|
|
26
38
|
|
|
39
|
+
# rubocop:disable Naming/PredicateMethod
|
|
27
40
|
def start
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
@state_mutex.synchronize do
|
|
42
|
+
return @accepting if @thread&.alive?
|
|
43
|
+
|
|
44
|
+
if @queue.closed?
|
|
45
|
+
queue = SizedQueue.new(@buffer_size)
|
|
46
|
+
queue << @queue.pop(true) until @queue.empty?
|
|
47
|
+
@queue = queue
|
|
48
|
+
end
|
|
49
|
+
@accepting = true
|
|
50
|
+
@thread = Thread.new { consume(queue: @state_mutex.synchronize { @queue }) }
|
|
51
|
+
@thread.name = 'legion-log-writer'
|
|
52
|
+
@thread.abort_on_exception = false
|
|
53
|
+
end
|
|
54
|
+
true
|
|
36
55
|
end
|
|
37
56
|
|
|
38
|
-
# rubocop:disable Naming/PredicateMethod
|
|
39
57
|
def stop(timeout: 2)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
59
|
+
thread = @state_mutex.synchronize do
|
|
60
|
+
@accepting = false
|
|
61
|
+
@queue.close
|
|
62
|
+
if !@thread&.alive? && !@queue.empty?
|
|
63
|
+
@thread = Thread.new { consume(queue: @state_mutex.synchronize { @queue }) }
|
|
64
|
+
@thread.name = 'legion-log-writer'
|
|
65
|
+
@thread.abort_on_exception = false
|
|
66
|
+
end
|
|
67
|
+
@thread
|
|
46
68
|
end
|
|
69
|
+
return true unless thread
|
|
70
|
+
return false if Thread.current.equal?(thread)
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return false if
|
|
72
|
+
remaining = [deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC), 0].max if deadline
|
|
73
|
+
thread.join(remaining)
|
|
74
|
+
return false if thread.alive?
|
|
51
75
|
|
|
52
|
-
@thread = nil
|
|
76
|
+
@state_mutex.synchronize { @thread = nil if @thread.equal?(thread) }
|
|
53
77
|
true
|
|
54
78
|
end
|
|
55
79
|
|
|
56
80
|
def push(entry)
|
|
81
|
+
if current_thread?
|
|
82
|
+
return write_reentrant_entry(entry) unless @preparing
|
|
83
|
+
|
|
84
|
+
write_entry(entry)
|
|
85
|
+
return true
|
|
86
|
+
end
|
|
87
|
+
|
|
57
88
|
return false unless accepting?
|
|
58
89
|
|
|
59
90
|
@queue.push(entry)
|
|
@@ -67,28 +98,90 @@ module Legion
|
|
|
67
98
|
@thread&.alive? || false
|
|
68
99
|
end
|
|
69
100
|
|
|
101
|
+
def current_thread?
|
|
102
|
+
Thread.current.equal?(@thread)
|
|
103
|
+
end
|
|
104
|
+
|
|
70
105
|
private
|
|
71
106
|
|
|
72
|
-
def consume
|
|
107
|
+
def consume(queue:)
|
|
73
108
|
loop do
|
|
74
|
-
entry =
|
|
75
|
-
break if entry.nil?
|
|
109
|
+
entry = queue.pop
|
|
110
|
+
break if entry.nil?
|
|
76
111
|
|
|
77
112
|
write_entry(entry)
|
|
78
113
|
end
|
|
79
114
|
end
|
|
80
115
|
|
|
81
|
-
def write_entry(entry)
|
|
116
|
+
def write_entry(entry, forward: true)
|
|
117
|
+
previous_preparing = @preparing
|
|
118
|
+
@preparing = entry.is_a?(CallEntry)
|
|
119
|
+
if @preparing
|
|
120
|
+
write_call_entry(entry)
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if entry.is_a?(EventEntry)
|
|
125
|
+
callback_origin = Thread.current[:legion_log_origin]
|
|
126
|
+
Thread.current[:legion_log_origin] = nil
|
|
127
|
+
entry.callback.call(entry.event, **entry.options)
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
82
131
|
with_entry_context(entry) do
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
if entry.is_a?(OutputEntry)
|
|
133
|
+
write_output_entry(entry)
|
|
134
|
+
else
|
|
135
|
+
if entry.force
|
|
136
|
+
formatter = @logger.formatter || ::Logger::Formatter.new
|
|
137
|
+
severity = entry.level == :unknown ? 'ANY' : entry.level.to_s.upcase
|
|
138
|
+
@logger << formatter.call(severity, Time.now, nil, entry.message)
|
|
139
|
+
else
|
|
140
|
+
@logger.send(entry.level, entry.message)
|
|
141
|
+
end
|
|
142
|
+
fire_writer(entry) if forward && entry.writer_context
|
|
143
|
+
end
|
|
85
144
|
end
|
|
86
145
|
rescue StandardError => e
|
|
87
146
|
warn("legion-log-writer error: #{e.message} (#{e.backtrace&.first})")
|
|
147
|
+
ensure
|
|
148
|
+
@preparing = previous_preparing
|
|
149
|
+
Thread.current[:legion_log_origin] = callback_origin if entry.is_a?(EventEntry)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def write_output_entry(entry)
|
|
153
|
+
return @logger << entry.message if entry.severity.nil?
|
|
154
|
+
|
|
155
|
+
severity = @logger.send(:format_severity, entry.severity)
|
|
156
|
+
@logger << @logger.send(:format_message, severity, entry.origin[:timestamp].getlocal, entry.progname, entry.message)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def write_reentrant_entry(entry)
|
|
160
|
+
previous = @reentrant_write
|
|
161
|
+
@reentrant_write = true
|
|
162
|
+
if previous || entry.is_a?(EventEntry)
|
|
163
|
+
event = entry.is_a?(EventEntry) ? entry.event : { message: entry.message.to_s }
|
|
164
|
+
@logger << "#{::JSON.generate(event)}\n"
|
|
165
|
+
else
|
|
166
|
+
write_entry(entry, forward: false)
|
|
167
|
+
end
|
|
168
|
+
true
|
|
169
|
+
ensure
|
|
170
|
+
@reentrant_write = previous
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def write_call_entry(entry)
|
|
174
|
+
previous = entry.context.map { |key, _| [key, Thread.current[key]] }
|
|
175
|
+
entry.context.each { |key, value| Thread.current[key] = value }
|
|
176
|
+
entry.receiver.public_send(entry.method_name, *entry.arguments, **entry.keywords)
|
|
177
|
+
ensure
|
|
178
|
+
previous&.each { |key, value| Thread.current[key] = value }
|
|
88
179
|
end
|
|
89
180
|
|
|
90
181
|
def with_entry_context(entry)
|
|
91
182
|
prev = THREAD_KEYS.map { |k| [k, Thread.current[k]] }
|
|
183
|
+
previous_origin = Thread.current[:legion_log_origin]
|
|
184
|
+
Thread.current[:legion_log_origin] = entry.origin if entry.is_a?(OutputEntry)
|
|
92
185
|
Thread.current[:legion_log_segments] = entry.segments
|
|
93
186
|
Thread.current[:legion_log_method] = entry.method_ctx
|
|
94
187
|
Thread.current[:legion_log_caller] = entry.caller_trace
|
|
@@ -99,15 +192,7 @@ module Legion
|
|
|
99
192
|
yield
|
|
100
193
|
ensure
|
|
101
194
|
prev&.each { |k, v| Thread.current[k] = v }
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def drain
|
|
105
|
-
until @queue.empty?
|
|
106
|
-
entry = @queue.pop(true)
|
|
107
|
-
write_entry(entry) unless entry == SHUTDOWN
|
|
108
|
-
end
|
|
109
|
-
rescue ThreadError
|
|
110
|
-
nil
|
|
195
|
+
Thread.current[:legion_log_origin] = previous_origin
|
|
111
196
|
end
|
|
112
197
|
|
|
113
198
|
def accepting?
|
|
@@ -115,6 +200,7 @@ module Legion
|
|
|
115
200
|
end
|
|
116
201
|
|
|
117
202
|
def fire_writer(entry)
|
|
203
|
+
previous_origin = Thread.current[:legion_log_origin]
|
|
118
204
|
ctx = entry.writer_context
|
|
119
205
|
event = ctx[:event]
|
|
120
206
|
level = ctx[:level]
|
|
@@ -123,10 +209,13 @@ module Legion
|
|
|
123
209
|
routing_key = "legion.logging.log.#{level}.#{lex_name}.#{component}"
|
|
124
210
|
headers = Legion::Logging.send(:build_log_headers, event, component, level)
|
|
125
211
|
properties = Legion::Logging.send(:build_log_properties, level)
|
|
212
|
+
Thread.current[:legion_log_origin] = nil
|
|
126
213
|
Legion::Logging.log_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
|
|
127
214
|
Legion::Logging::Hooks.fire(level, entry.message, event) if defined?(Legion::Logging::Hooks)
|
|
128
215
|
rescue StandardError => e
|
|
129
216
|
warn("legion-log-writer writer error: #{e.message}")
|
|
217
|
+
ensure
|
|
218
|
+
Thread.current[:legion_log_origin] = previous_origin
|
|
130
219
|
end
|
|
131
220
|
end
|
|
132
221
|
end
|