log_shout 0.1.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 +12 -0
- data/LICENSE +21 -0
- data/README.md +285 -0
- data/RELEASING.md +42 -0
- data/lib/log_shout/formatters/default.rb +15 -0
- data/lib/log_shout/helper.rb +21 -0
- data/lib/log_shout/io_ractor.rb +131 -0
- data/lib/log_shout/logging_ractor.rb +30 -0
- data/lib/log_shout/multi_io.rb +36 -0
- data/lib/log_shout/version.rb +5 -0
- data/lib/log_shout.rb +331 -0
- metadata +174 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cc4cc267c8eeb16ef1f1615f57a50189549a896c2d583eb74ab0540857812146
|
|
4
|
+
data.tar.gz: 487bb3371dbf862cb701e1e5278959d722765f424c3f70832cffdef30eb577dc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e85575c4a8cb47cabd352d1104326f07fb3c15f7ec486ae64de5b61cb75182235a3889a671b598a510aa5ed9f4c7a44df5cd062a743e86c0b5cefdbc46b71ae8
|
|
7
|
+
data.tar.gz: b45d29c88b91d0bacae4f55ce5864dd630847a544e680b6319bbf6b37c4a5c71c06a3ab412f3c4baefe61cf80c4e60486baad3455fd72319769158accb64abca
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
Initial release candidate:
|
|
6
|
+
|
|
7
|
+
- Fan out Ruby Logger messages to stdout, stderr, files, and memory through Ractors.
|
|
8
|
+
- Configure named loggers and shared destinations with YAML.
|
|
9
|
+
- Provide class and instance logger helpers and background job logging.
|
|
10
|
+
- Require Ruby 4.0 or newer for Ractor::Port and Ractor#value.
|
|
11
|
+
- Drain and close destinations during reconfiguration, reset, and shutdown.
|
|
12
|
+
- Surface destination failures to callers of flush, read, and close.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Christen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# LogShout
|
|
2
|
+
|
|
3
|
+
Fan-out logging for Ruby using Ractors. LogShout wraps Ruby's `::Logger` to write each log message to multiple destinations (stdout, stderr, files, in-memory) concurrently and safely.
|
|
4
|
+
|
|
5
|
+
## WARNING
|
|
6
|
+
|
|
7
|
+
Ractor support in Ruby is EXPERIMENTAL use at your own risk.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Requires CRuby 4.0 or newer (`Ractor::Port` and `Ractor#value`).
|
|
12
|
+
|
|
13
|
+
Add to your Gemfile and bundle:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "log_shout", "~> 0.1"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
1. Optionally create a `log_shout.yml` alongside your app (or set `LOG_SHOUT_CONFIG_PATH`). If you skip this, LogShout will run with sane defaults (documented below). If you specify `LOG_SHOUT_CONFIG_PATH` and the file does not exist you will get a runtime error.
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
---
|
|
29
|
+
defaultLevel: info
|
|
30
|
+
defaultHandlers: [stdout]
|
|
31
|
+
extraHandlers:
|
|
32
|
+
app_file_log:
|
|
33
|
+
type: file
|
|
34
|
+
path: ./app.log
|
|
35
|
+
loggers:
|
|
36
|
+
app:
|
|
37
|
+
level: debug
|
|
38
|
+
handlers: [stdout, app_file_log]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
2. Use LogShout in your code:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "log_shout"
|
|
45
|
+
|
|
46
|
+
logger = LogShout.get_logger(:app)
|
|
47
|
+
logger.info("Hello from LogShout")
|
|
48
|
+
# in your console
|
|
49
|
+
# [2025-10-04 10:50:55.514722 #583620/583642] I -- app: Hello from LogShout
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Tip: include the helper to get class/instance-level loggers.
|
|
53
|
+
This will provide access to a logger named after the class via both a class and instance method named logger.
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
module MyModule
|
|
57
|
+
class MyClass
|
|
58
|
+
include LogShout::Helper
|
|
59
|
+
|
|
60
|
+
def self.class_method
|
|
61
|
+
logger.debug("my message")
|
|
62
|
+
# [2025-10-04 10:50:55.514722 #583620/583642] D -- MyModule::MyClass: my message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def method
|
|
66
|
+
logger.debug("my message")
|
|
67
|
+
# [2025-10-04 10:50:55.514722 #583620/583642] D -- MyModule::MyClass: my message
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Tip: Run a job in its own Ractor with a Ractor safe logger
|
|
75
|
+
This behavior is similar to the class based helpers but the API is different.
|
|
76
|
+
|
|
77
|
+
If you have the following class
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
class MyJob
|
|
81
|
+
def self.run(logger, p1, p2)
|
|
82
|
+
logger.debug("some background task #{p1} #{p2}")
|
|
83
|
+
p1+p2
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then you can run the job as follows.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
r = LogShout::LoggingRactor.start(target: MyJob, name: :app, args: [1, 4])
|
|
92
|
+
# [2025-10-04 10:50:55.514722 #583620/583642] D -- MyJob: some background task 1 4
|
|
93
|
+
r.value # 5
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
### Schema
|
|
99
|
+
|
|
100
|
+
- defaultLevel: info|debug|warn|fatal
|
|
101
|
+
- defaultHandlers: [stdout] # array of handler names that every logger inherits by default.
|
|
102
|
+
- extraHandlers: map of name -> `{ type: stdout|stderr|file|memory, path?: string }` (file requires path).
|
|
103
|
+
- loggers: map of logger name -> `{ level: string, handlers: [names...] }`.
|
|
104
|
+
|
|
105
|
+
Notes:
|
|
106
|
+
|
|
107
|
+
- Built-in handlers `stdout` and `stderr` cannot be overridden. eg. you cannot create a **new** handler named `:stdout`
|
|
108
|
+
|
|
109
|
+
## Using without a config file (defaults)
|
|
110
|
+
|
|
111
|
+
The configuration file is optional. These are the defaults.
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
---
|
|
115
|
+
defaultLevel: INFO
|
|
116
|
+
defaultHandlers: [stdout]
|
|
117
|
+
extraHandlers: {}
|
|
118
|
+
loggers: {}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Runtime behavior of default config:
|
|
122
|
+
|
|
123
|
+
- If you set `LOG_SHOUT_CONFIG_PATH` and that file doesn’t exist, `configure` will raise.
|
|
124
|
+
- Any logger name you request (e.g., `get_logger(:anything)`) uses the default level and handlers when not explicitly configured.
|
|
125
|
+
- By default only `stdout` is attached; to add `stderr`, `file`, or `memory`, define them in `extraHandlers` and reference by name in a logger’s `handlers`.
|
|
126
|
+
|
|
127
|
+
Minimal example with defaults (no file present):
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
require "log_shout"
|
|
131
|
+
LogShout.get_logger(:any).info("goes to STDOUT at INFO level")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Delivery, failure, and lifecycle
|
|
135
|
+
|
|
136
|
+
`logger.info` and the other severity methods enqueue messages asynchronously.
|
|
137
|
+
Returning from a log call does not mean a destination has written the message.
|
|
138
|
+
Each destination processes its queue in order. Ordering across independent
|
|
139
|
+
producers or different destinations is not guaranteed.
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
LogShout.flush # wait for earlier queued writes at every destination
|
|
143
|
+
LogShout.close_handlers # drain queues, close destinations, wait for workers
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`flush` flushes Ruby IO buffers; it does not call `fsync` or guarantee survival
|
|
147
|
+
of a crash or power failure. Queues and memory destinations are unbounded:
|
|
148
|
+
slow destinations or large volumes can consume significant memory. There is
|
|
149
|
+
currently no backpressure, rotation, retry, or delivery guarantee after failure.
|
|
150
|
+
|
|
151
|
+
Destinations are opened during configuration. If opening or writing fails,
|
|
152
|
+
`Ractor::RemoteError` is raised by initialization or the next synchronous
|
|
153
|
+
`flush`, `read`, or `close`; its `cause` contains the underlying IO exception.
|
|
154
|
+
A failed worker wakes waiting callers. A live worker blocked in operating-system
|
|
155
|
+
IO can still block a flush or shutdown; no IO timeout is imposed. Ruby Logger
|
|
156
|
+
may report immediate write errors to stderr instead of propagating them, so use
|
|
157
|
+
an explicit `LogShout.flush` where delivery needs to be checked.
|
|
158
|
+
|
|
159
|
+
Calling `logger.close` flushes its shared destinations. Only
|
|
160
|
+
`LogShout.close_handlers` closes them, and repeated shutdown is safe. An `at_exit`
|
|
161
|
+
hook attempts shutdown and reports failures to stderr without masking an earlier
|
|
162
|
+
application exception. For reliable delivery, explicitly stop producers and call
|
|
163
|
+
`close_handlers` before exiting.
|
|
164
|
+
|
|
165
|
+
`LogShout.configure(path: ...)` validates and opens the replacement destinations,
|
|
166
|
+
drains and closes the previous handlers, and clears the logger cache. Invalid
|
|
167
|
+
configuration or an opening failure preserves the current configuration.
|
|
168
|
+
`LogShout.reset!` closes handlers and clears configuration; the next `get_logger`
|
|
169
|
+
loads configuration again. Stop and join background jobs before reconfiguring,
|
|
170
|
+
resetting, or closing. Discard old logger references and fetch fresh ones after
|
|
171
|
+
reconfiguration. Configuration changes are intended for the main Ractor while
|
|
172
|
+
producers are stopped; concurrent reconfiguration is unsupported.
|
|
173
|
+
|
|
174
|
+
String and symbol logger names are equivalent. Use `LoggingRactor.start` to
|
|
175
|
+
create loggers inside background Ractors rather than calling the module's global
|
|
176
|
+
configuration methods from another Ractor. Join the returned job with `r.value`
|
|
177
|
+
before flushing or closing destinations in the main Ractor.
|
|
178
|
+
|
|
179
|
+
## Architecture: Logger I/O
|
|
180
|
+
|
|
181
|
+
This shows how log messages flow from your code through Ruby’s `::Logger`, across `MultiIO`, into per-destination Ractors, and finally to underlying IOs.
|
|
182
|
+
|
|
183
|
+
Key parts:
|
|
184
|
+
|
|
185
|
+
- `LogShout.get_logger` in [lib/log_shout.rb](lib/log_shout.rb): builds `::Logger` with a custom logdev and default formatter.
|
|
186
|
+
- `LogShout::MultiIO` in [lib/log_shout/multi_io.rb](lib/log_shout/multi_io.rb): fan-out logdev that forwards `write/flush/close` to all handlers.
|
|
187
|
+
- `LogShout::IORactor` in [lib/log_shout/io_ractor.rb](lib/log_shout/io_ractor.rb): one ractor per handler; owns the actual IO (STDOUT/ERR/File/StringIO).
|
|
188
|
+
- `LogShout::LoggingRactor` in [lib/log_shout/logging_ractor.rb](lib/log_shout/logging_ractor.rb): helper to run a job in its own ractor with a ready-to-use logger.
|
|
189
|
+
- Default formatter in [lib/log_shout/formatters/default.rb](lib/log_shout/formatters/default.rb).
|
|
190
|
+
|
|
191
|
+
`LogShout::IORactor` objects are pre-created for all handlers defined. These objects are shared between all loggers that are created. So if you create 20 loggers with the same handler, only one `LogShout::IORactor` will exist, and thus only one Ractor will be created.
|
|
192
|
+
|
|
193
|
+
### Component view
|
|
194
|
+
|
|
195
|
+
```mermaid
|
|
196
|
+
flowchart LR
|
|
197
|
+
subgraph App
|
|
198
|
+
A["Your code"]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
subgraph LogShout core
|
|
202
|
+
LS["get_logger(name)<br/>/log_shout.rb"]
|
|
203
|
+
L["Logger<br/> stdlib"]
|
|
204
|
+
M["LogShout::MultiIO<br/>/log_shout/multi_io.rb"]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
subgraph "Handlers (per destination)"
|
|
208
|
+
direction TB
|
|
209
|
+
H1["LogShout::IORactor<br/>(stdout)<br/>/log_shout/io_ractor.rb"]
|
|
210
|
+
H2["LogShout::IORactor<br/>(stderr)<br/>/log_shout/io_ractor.rb"]
|
|
211
|
+
H3["LogShout::IORactor<br/>(file)<br/>/log_shout/io_ractor.rb"]
|
|
212
|
+
H4["LogShout::IORactor<br/>(memory)<br/>/log_shout/io_ractor.rb"]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
subgraph "Ractor workers (one per handler)"
|
|
216
|
+
direction TB
|
|
217
|
+
R1["Ractor loop<br/>.for_fd(1)"]
|
|
218
|
+
R2["Ractor loop<br/>.for_fd(2)"]
|
|
219
|
+
R3["Ractor loop<br/>.open(path,'a')"]
|
|
220
|
+
R4["Ractor loop"]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
A -->|"logger = LogShout.get_logger(:name)"| LS
|
|
224
|
+
LS -->|builds| L
|
|
225
|
+
L -->|logdev| M
|
|
226
|
+
|
|
227
|
+
M -->|write/flush/close| H1
|
|
228
|
+
M -->|write/flush/close| H2
|
|
229
|
+
M -->|write/flush/close| H3
|
|
230
|
+
M -->|write/flush/close| H4
|
|
231
|
+
|
|
232
|
+
H1 -->|"send [:op, ...]"| R1
|
|
233
|
+
H2 -->|"send [:op, ...]"| R2
|
|
234
|
+
H3 -->|"send [:op, ...]"| R3
|
|
235
|
+
H4 -->|"send [:op, ...]"| R4
|
|
236
|
+
|
|
237
|
+
R1 --> IO1[(STDOUT)]
|
|
238
|
+
R2 --> IO2[(STDERR)]
|
|
239
|
+
R3 --> IO3[(File)]
|
|
240
|
+
R4 --> IO4[(StringIO)]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Sequence view (write/flush/close)
|
|
244
|
+
|
|
245
|
+
```mermaid
|
|
246
|
+
sequenceDiagram
|
|
247
|
+
participant App
|
|
248
|
+
participant Logger as ::Logger
|
|
249
|
+
participant MultiIO as LogShout::MultiIO
|
|
250
|
+
participant IO as LogShout::IORactor
|
|
251
|
+
|
|
252
|
+
App->>Logger: info("msg")
|
|
253
|
+
Logger->>Logger: format -> "s"
|
|
254
|
+
Logger->>MultiIO: write("s")
|
|
255
|
+
loop for each handler
|
|
256
|
+
MultiIO->>IO: write("s")
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Dev setup(with nix+direnv)
|
|
261
|
+
|
|
262
|
+
The flake uses Ruby 4.0 from `nixos-unstable`, including the `Ractor::Port` API
|
|
263
|
+
previously used from Ruby nightly. No local Ruby fork is needed. Bundix uses
|
|
264
|
+
[the fork required by ruby-nix](https://github.com/inscapist/ruby-nix#global-bundix-installation-optional)
|
|
265
|
+
for platform-dependent gems, built with the same Ruby as the project.
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
direnv allow
|
|
269
|
+
bundix
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Without Nix, install dependencies with a released Bundler:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
gem install bundler -v 2.7.2
|
|
276
|
+
bundle _2.7.2_ install
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Run the tests and style checks with `bundle exec rake`; build with
|
|
280
|
+
`bundle exec rake build`. Run `ruby script/check_package.rb` to install the
|
|
281
|
+
built gem into a temporary gem home and check it through Bundler without the
|
|
282
|
+
development dependencies. That check downloads runtime dependencies.
|
|
283
|
+
|
|
284
|
+
See [CHANGELOG.md](CHANGELOG.md) and [RELEASING.md](RELEASING.md) for release status
|
|
285
|
+
and publishing instructions. Licensed under the [MIT License](LICENSE).
|
data/RELEASING.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Releasing LogShout
|
|
2
|
+
|
|
3
|
+
## One-time account setup
|
|
4
|
+
|
|
5
|
+
1. Confirm the gem name `log_shout` is available or owned by your RubyGems account.
|
|
6
|
+
2. Enable MFA on that account. The gem declares `rubygems_mfa_required`.
|
|
7
|
+
3. On RubyGems, create a pending trusted publisher for a first release (or a
|
|
8
|
+
trusted publisher on the existing gem):
|
|
9
|
+
- Gem: `log_shout`
|
|
10
|
+
- Repository owner: `mchristen`
|
|
11
|
+
- Repository: `log_shout`
|
|
12
|
+
- Workflow filename: `release.yml`
|
|
13
|
+
- Environment: `release`
|
|
14
|
+
4. In the GitHub repository, create a `release` environment with required reviewer
|
|
15
|
+
approval if available for the repository, and protect your release branches.
|
|
16
|
+
|
|
17
|
+
See the official [Trusted Publishing guide](https://guides.rubygems.org/trusted-publishing/).
|
|
18
|
+
No long-lived RubyGems API token is needed by the workflow.
|
|
19
|
+
|
|
20
|
+
## Each release
|
|
21
|
+
|
|
22
|
+
1. Set `LogShout::VERSION` in `lib/log_shout/version.rb`. Update the matching
|
|
23
|
+
changelog entry with the release date and user-visible changes. Verify the
|
|
24
|
+
maintainer email and public metadata before the first release.
|
|
25
|
+
2. If dependencies change, run `bundle _2.7.2_ lock`, regenerate `gemset.nix`
|
|
26
|
+
with `bundix`, and commit both lock representations.
|
|
27
|
+
3. Run `bundle exec rake`, `bundle exec rake build`, and
|
|
28
|
+
`ruby script/check_package.rb`. Inspect the packaged file list. The package
|
|
29
|
+
check installs runtime dependencies from RubyGems into a temporary directory.
|
|
30
|
+
4. Commit all release changes and ensure CI passes on Ruby 4.0.0 and the latest
|
|
31
|
+
4.0 patch release. Review the exact commit to publish.
|
|
32
|
+
5. Create and push the matching version tag, for example `v0.1.0`, pointing at
|
|
33
|
+
that tested commit.
|
|
34
|
+
6. In GitHub Actions, manually run **Release** from the default branch, entering
|
|
35
|
+
that existing tag. It verifies the tag matches the gemspec, reruns checks,
|
|
36
|
+
and publishes through RubyGems Trusted Publishing. Approve the `release`
|
|
37
|
+
environment deployment when configured.
|
|
38
|
+
7. Verify the published version and its metadata on RubyGems, then create release
|
|
39
|
+
notes from the changelog. Published version numbers cannot be reused.
|
|
40
|
+
|
|
41
|
+
The repository includes preparation and verification code only; account setup,
|
|
42
|
+
pushing release tags, and publishing are deliberate maintainer actions.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogShout
|
|
4
|
+
module Formatters
|
|
5
|
+
class Default < ::Logger::Formatter
|
|
6
|
+
FORMAT = "[%s #%d/%6s] %.1s -- %s: %s\n"
|
|
7
|
+
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S.%6N"
|
|
8
|
+
|
|
9
|
+
def call(severity, time, progname, msg)
|
|
10
|
+
thread_id = Thread.current.name || Thread.current.native_thread_id
|
|
11
|
+
sprintf(FORMAT, time.strftime(DATETIME_FORMAT), Process.pid, thread_id, severity, progname, msg2str(msg))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogShout
|
|
4
|
+
module Helper
|
|
5
|
+
module ClassMethods
|
|
6
|
+
def logger(extra_name: nil)
|
|
7
|
+
logger_name = "#{name}::#{extra_name}" if extra_name
|
|
8
|
+
logger_name ||= name
|
|
9
|
+
LogShout.get_logger(logger_name.to_sym)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def logger(extra_name: nil)
|
|
14
|
+
self.class.logger(extra_name: extra_name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.included(other)
|
|
18
|
+
other.extend(ClassMethods)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogShout
|
|
4
|
+
class IORactor
|
|
5
|
+
def initialize(handler: {})
|
|
6
|
+
@handler = handler
|
|
7
|
+
@closed = false
|
|
8
|
+
@r = Ractor.new(handler) do |definition|
|
|
9
|
+
IORactor.execute_ractor(definition)
|
|
10
|
+
end
|
|
11
|
+
request(:ready)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.execute_ractor(handler)
|
|
15
|
+
io = case handler[:type]
|
|
16
|
+
when nil, :stdout
|
|
17
|
+
IO.for_fd(1, autoclose: false)
|
|
18
|
+
when :stderr
|
|
19
|
+
IO.for_fd(2, autoclose: false)
|
|
20
|
+
when :file
|
|
21
|
+
File.open(handler.fetch(:destination), "a")
|
|
22
|
+
when :memory
|
|
23
|
+
StringIO.new
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError, "Invalid handler definition: #{handler.inspect}"
|
|
26
|
+
end
|
|
27
|
+
io.sync = true if io.respond_to?(:sync=)
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
loop do
|
|
31
|
+
op, arg, reply = Ractor.receive
|
|
32
|
+
result = case op
|
|
33
|
+
when :ready
|
|
34
|
+
nil
|
|
35
|
+
when :write
|
|
36
|
+
io.write(arg)
|
|
37
|
+
when :flush
|
|
38
|
+
io.flush
|
|
39
|
+
nil
|
|
40
|
+
when :read
|
|
41
|
+
io.is_a?(StringIO) ? io.string : nil
|
|
42
|
+
when :close
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
reply&.send([:ok, result])
|
|
46
|
+
end
|
|
47
|
+
ensure
|
|
48
|
+
io.close unless io.closed?
|
|
49
|
+
end
|
|
50
|
+
:closed
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def name
|
|
54
|
+
@handler[:name]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def type
|
|
58
|
+
@handler[:type]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def destination
|
|
62
|
+
@handler[:destination]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def closed?
|
|
66
|
+
@closed || @r.default_port.closed?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def write(str)
|
|
70
|
+
raise IOError, "closed log handler" if closed?
|
|
71
|
+
s = String(str)
|
|
72
|
+
@r.send([:write, s])
|
|
73
|
+
s.bytesize
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def flush(wait_for_flush = true)
|
|
77
|
+
if wait_for_flush
|
|
78
|
+
request(:flush)
|
|
79
|
+
else
|
|
80
|
+
@r.send([:flush])
|
|
81
|
+
end
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def read
|
|
86
|
+
request(:read)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def close
|
|
90
|
+
return if @closed
|
|
91
|
+
begin
|
|
92
|
+
begin
|
|
93
|
+
@r.send([:close])
|
|
94
|
+
rescue Ractor::ClosedError
|
|
95
|
+
# Join below still reports the worker's original failure.
|
|
96
|
+
end
|
|
97
|
+
@r.join
|
|
98
|
+
ensure
|
|
99
|
+
@closed = true
|
|
100
|
+
end
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# A fresh reply port belongs to the calling Ractor. Monitoring that same
|
|
107
|
+
# port also wakes the caller if the worker dies before replying.
|
|
108
|
+
def request(op)
|
|
109
|
+
raise IOError, "closed log handler" if @closed
|
|
110
|
+
reply = Ractor::Port.new
|
|
111
|
+
@r.monitor(reply)
|
|
112
|
+
begin
|
|
113
|
+
@r.send([op, nil, reply])
|
|
114
|
+
rescue Ractor::ClosedError
|
|
115
|
+
@r.join
|
|
116
|
+
raise IOError, "closed log handler"
|
|
117
|
+
end
|
|
118
|
+
response = reply.receive
|
|
119
|
+
unless response.is_a?(Array)
|
|
120
|
+
@r.join
|
|
121
|
+
raise IOError, "log handler terminated before replying"
|
|
122
|
+
end
|
|
123
|
+
response[1]
|
|
124
|
+
ensure
|
|
125
|
+
if reply
|
|
126
|
+
@r.unmonitor(reply)
|
|
127
|
+
reply.close
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogShout
|
|
4
|
+
class LoggingRactor
|
|
5
|
+
# caller passes a shareable target (Module/Class) and a method Symbol
|
|
6
|
+
def self.start(target:, method: :run, name: nil, args: [])
|
|
7
|
+
name ||= target.name
|
|
8
|
+
handlers = LogShout.get_handlers_for(name).freeze
|
|
9
|
+
level = ::Logger.const_get(LogShout.get_level_for_name(name).upcase)
|
|
10
|
+
logdev = LogShout::MultiIO.new(handlers)
|
|
11
|
+
r = Ractor.new(level, logdev, args, name: name) do |level, logdev, a|
|
|
12
|
+
logger = ::Logger.new(logdev,
|
|
13
|
+
level: level,
|
|
14
|
+
progname: Ractor.current.name,
|
|
15
|
+
formatter: LogShout::Formatters::Default.new)
|
|
16
|
+
# Call a shareable entrypoint; e.g., MyJob.run(logger, *a)
|
|
17
|
+
job = Ractor.receive # receive [ModuleOrClass, :method]
|
|
18
|
+
receiver = Ractor.receive
|
|
19
|
+
job.public_send(receiver, logger, *a)
|
|
20
|
+
ensure
|
|
21
|
+
logdev.flush(false)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Send the callable in a ractor-safe way (two shareable messages)
|
|
25
|
+
r.send(target)
|
|
26
|
+
r.send(method)
|
|
27
|
+
r
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogShout
|
|
4
|
+
class MultiIO
|
|
5
|
+
def initialize(targets)
|
|
6
|
+
@targets = targets
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def write(message)
|
|
10
|
+
@targets.each { |t| t.write(message) }
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def close
|
|
15
|
+
# Destinations belong to LogShout and may be used by other loggers.
|
|
16
|
+
flush
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def flush(wait_for_flush = true)
|
|
21
|
+
@targets.each do |t|
|
|
22
|
+
t.flush(wait_for_flush)
|
|
23
|
+
end
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# only useful for debugging memory handler
|
|
28
|
+
def read(handler)
|
|
29
|
+
@targets.each do |t|
|
|
30
|
+
next unless t.name == handler
|
|
31
|
+
return t.read
|
|
32
|
+
end
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/log_shout.rb
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "logger"
|
|
6
|
+
require "stringio"
|
|
7
|
+
|
|
8
|
+
require "log_shout/helper"
|
|
9
|
+
require "log_shout/multi_io"
|
|
10
|
+
require "log_shout/io_ractor"
|
|
11
|
+
require "log_shout/logging_ractor"
|
|
12
|
+
require "log_shout/version"
|
|
13
|
+
require "log_shout/formatters/default"
|
|
14
|
+
|
|
15
|
+
module LogShout
|
|
16
|
+
class LogShoutError < StandardError; end
|
|
17
|
+
|
|
18
|
+
# Replace the Sorbet enum with a plain Ruby value-object enum
|
|
19
|
+
class LogDestination
|
|
20
|
+
attr_reader :name
|
|
21
|
+
private_class_method :new
|
|
22
|
+
|
|
23
|
+
def initialize(name)
|
|
24
|
+
@name = name
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_sym
|
|
28
|
+
@name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_s
|
|
32
|
+
@name.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Keep the same API used elsewhere
|
|
36
|
+
def serialize
|
|
37
|
+
@name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ==(other)
|
|
41
|
+
other.is_a?(LogDestination) && other.name == name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias_method :eql?, :==
|
|
45
|
+
|
|
46
|
+
def hash
|
|
47
|
+
name.hash
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
STD_OUT = new(:stdout)
|
|
51
|
+
STD_ERR = new(:stderr)
|
|
52
|
+
FILE = new(:file)
|
|
53
|
+
MEMORY = new(:memory)
|
|
54
|
+
|
|
55
|
+
ALL = [STD_OUT, STD_ERR, FILE, MEMORY].freeze
|
|
56
|
+
|
|
57
|
+
def self.from(value)
|
|
58
|
+
return value if value.is_a?(LogDestination)
|
|
59
|
+
sym = value.to_s.downcase.to_sym
|
|
60
|
+
ALL.find { |v| v.name == sym } || (raise ArgumentError, "Unknown destination: #{value.inspect}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Keep keyword-only constructors for the public configuration structs.
|
|
65
|
+
|
|
66
|
+
LogHandler = Struct.new(:name, :type, :destination, keyword_init: true) do # standard:disable Style/RedundantStructKeywordInit
|
|
67
|
+
def ==(other)
|
|
68
|
+
other.is_a?(LogHandler) && name == other.name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def serialize
|
|
72
|
+
{
|
|
73
|
+
name: name,
|
|
74
|
+
type: type.respond_to?(:serialize) ? type.serialize : type.to_sym,
|
|
75
|
+
destination: destination
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
LogConfig = Struct.new(:level, :handlers, keyword_init: true) do # standard:disable Style/RedundantStructKeywordInit
|
|
81
|
+
def ==(other)
|
|
82
|
+
other.is_a?(LogConfig) &&
|
|
83
|
+
level == other.level &&
|
|
84
|
+
handlers == other.handlers
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def serialize
|
|
88
|
+
{
|
|
89
|
+
level: level,
|
|
90
|
+
handlers: handlers.map(&:serialize)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
LogConfigFile = Struct.new(:loggers, :extraHandlers, :defaultLevel, :defaultHandlers, keyword_init: true) do # standard:disable Style/RedundantStructKeywordInit
|
|
96
|
+
def ==(other)
|
|
97
|
+
other.is_a?(LogConfigFile) &&
|
|
98
|
+
loggers == other.loggers &&
|
|
99
|
+
extraHandlers == other.extraHandlers &&
|
|
100
|
+
defaultLevel == other.defaultLevel &&
|
|
101
|
+
defaultHandlers == other.defaultHandlers
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def serialize
|
|
105
|
+
{
|
|
106
|
+
loggers: loggers.transform_values(&:serialize),
|
|
107
|
+
extraHandlers: extraHandlers.transform_values(&:serialize),
|
|
108
|
+
defaultLevel: defaultLevel,
|
|
109
|
+
defaultHandlers: defaultHandlers.map(&:serialize)
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
BUILTIN_HANDLERS = {
|
|
115
|
+
stdout: LogHandler.new(name: :stdout, type: LogDestination::STD_OUT),
|
|
116
|
+
stderr: LogHandler.new(name: :stderr, type: LogDestination::STD_ERR)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
DEFAULT_CONFIG = LogConfigFile.new(
|
|
120
|
+
defaultLevel: "INFO",
|
|
121
|
+
defaultHandlers: [BUILTIN_HANDLERS[:stdout]],
|
|
122
|
+
loggers: {},
|
|
123
|
+
extraHandlers: {}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@loggers = {}
|
|
127
|
+
|
|
128
|
+
def self.configure(path: nil)
|
|
129
|
+
default_path = File.expand_path("log_shout.yml", FileUtils.pwd)
|
|
130
|
+
requested_path = ENV.fetch("LOG_SHOUT_CONFIG_PATH", path)
|
|
131
|
+
config_file_path = requested_path || default_path
|
|
132
|
+
config = if File.exist?(config_file_path)
|
|
133
|
+
begin
|
|
134
|
+
validate_config(YAML.load_file(config_file_path, symbolize_names: true))
|
|
135
|
+
rescue Psych::SyntaxError => e
|
|
136
|
+
raise LogShoutError, "Failed to parse YAML configuration file at #{config_file_path}: #{e.message}"
|
|
137
|
+
end
|
|
138
|
+
elsif requested_path
|
|
139
|
+
raise LogShoutError, "Configuration file not found at #{config_file_path}"
|
|
140
|
+
else
|
|
141
|
+
DEFAULT_CONFIG
|
|
142
|
+
end
|
|
143
|
+
validated_config = config.serialize.freeze
|
|
144
|
+
handlers = build_handlers(validated_config)
|
|
145
|
+
begin
|
|
146
|
+
close_handlers
|
|
147
|
+
rescue
|
|
148
|
+
handlers.each_value(&:close)
|
|
149
|
+
raise
|
|
150
|
+
end
|
|
151
|
+
@config_file_path = config_file_path
|
|
152
|
+
@validated_config = validated_config
|
|
153
|
+
@handlers = handlers.freeze
|
|
154
|
+
@loggers = {}
|
|
155
|
+
@config_file_path
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.build_handlers(config)
|
|
159
|
+
handlers = {}
|
|
160
|
+
definitions = config[:defaultHandlers] + config[:extraHandlers].values +
|
|
161
|
+
config[:loggers].values.flat_map { |logger| logger[:handlers] }
|
|
162
|
+
definitions.uniq { |handler| handler[:name] }.each do |handler|
|
|
163
|
+
handlers[handler[:name]] = create_handler(handler)
|
|
164
|
+
end
|
|
165
|
+
handlers
|
|
166
|
+
rescue
|
|
167
|
+
handlers.each_value(&:close)
|
|
168
|
+
raise
|
|
169
|
+
end
|
|
170
|
+
private_class_method :build_handlers
|
|
171
|
+
|
|
172
|
+
def self.create_handler(handler)
|
|
173
|
+
LogShout::IORactor.new(handler: handler) # must respond_to?(:write)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def self.validate_config(config)
|
|
177
|
+
raise LogShoutError, "Configuration must be a mapping" unless config.is_a?(Hash)
|
|
178
|
+
[:extraHandlers, :loggers].each do |key|
|
|
179
|
+
raise LogShoutError, "#{key} must be a mapping" unless config.fetch(key, {}).is_a?(Hash)
|
|
180
|
+
end
|
|
181
|
+
default_level = config.fetch(:defaultLevel, "INFO")
|
|
182
|
+
validate_level(default_level)
|
|
183
|
+
default_handler_names = config.fetch(:defaultHandlers, ["stdout"])
|
|
184
|
+
raise LogShoutError, "defaultHandlers must be an array" unless default_handler_names.is_a?(Array)
|
|
185
|
+
extra_handlers = config.fetch(:extraHandlers, {}).each_with_object({}) do |(name, config), obj|
|
|
186
|
+
if name === :stdout || name === :stderr
|
|
187
|
+
raise LogShoutError, "Cannot override built-in handler: #{name}"
|
|
188
|
+
end
|
|
189
|
+
raise LogShoutError, "Handler #{name} must be a mapping" unless config.is_a?(Hash)
|
|
190
|
+
handler = case config[:type]
|
|
191
|
+
when "memory"
|
|
192
|
+
LogHandler.new(name: name.to_sym, type: LogDestination::MEMORY)
|
|
193
|
+
when "file"
|
|
194
|
+
if !config[:path]
|
|
195
|
+
raise LogShoutError, "File handler #{name} requires a path"
|
|
196
|
+
end
|
|
197
|
+
LogHandler.new(name: name.to_sym, type: LogDestination::FILE, destination: config[:path])
|
|
198
|
+
when "stdout"
|
|
199
|
+
LogHandler.new(name: name.to_sym, type: LogDestination::STD_OUT)
|
|
200
|
+
when "stderr"
|
|
201
|
+
LogHandler.new(name: name.to_sym, type: LogDestination::STD_ERR)
|
|
202
|
+
else
|
|
203
|
+
raise LogShoutError, "Unknown handler type: #{config[:type]}"
|
|
204
|
+
end
|
|
205
|
+
obj[name.to_sym] = handler
|
|
206
|
+
end
|
|
207
|
+
default_handlers = default_handler_names.map do |name|
|
|
208
|
+
BUILTIN_HANDLERS[name.to_sym] || extra_handlers[name.to_sym] || (raise LogShoutError, "Unknown handler: #{name}")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
loggers = config.fetch(:loggers, {}).each_with_object({}) do |(name, config), obj|
|
|
212
|
+
raise LogShoutError, "Logger #{name} must be a mapping" unless config.is_a?(Hash)
|
|
213
|
+
level = config.fetch(:level, default_level)
|
|
214
|
+
validate_level(level)
|
|
215
|
+
handler_names = config.fetch(:handlers, default_handler_names)
|
|
216
|
+
raise LogShoutError, "Logger #{name} handlers must be an array" unless handler_names.is_a?(Array)
|
|
217
|
+
handlers = handler_names.map do |name|
|
|
218
|
+
BUILTIN_HANDLERS[name.to_sym] || extra_handlers[name.to_sym] || (raise LogShoutError, "Unknown handler: #{name}")
|
|
219
|
+
end
|
|
220
|
+
obj[name.to_sym] = LogConfig.new(level: level, handlers: handlers)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
LogConfigFile.new(
|
|
224
|
+
defaultLevel: config.fetch(:defaultLevel, "INFO"),
|
|
225
|
+
defaultHandlers: default_handlers,
|
|
226
|
+
extraHandlers: extra_handlers,
|
|
227
|
+
loggers: loggers
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def self.validate_level(level)
|
|
232
|
+
unless level.is_a?(String) && %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].include?(level.upcase)
|
|
233
|
+
raise LogShoutError, "Invalid log level: #{level.inspect}"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
private_class_method :validate_level
|
|
237
|
+
|
|
238
|
+
def self.get_handler(name)
|
|
239
|
+
@handlers[name]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def self.get_handlers_for(logger_name)
|
|
243
|
+
config = configuration[:loggers].fetch(
|
|
244
|
+
logger_name.to_sym,
|
|
245
|
+
{
|
|
246
|
+
level: configuration[:defaultLevel],
|
|
247
|
+
handlers: configuration[:defaultHandlers]
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
config[:handlers].map do |h|
|
|
251
|
+
get_handler(h[:name])
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def self.flush
|
|
256
|
+
return unless @handlers
|
|
257
|
+
@handlers.each_value do |handler|
|
|
258
|
+
handler.flush
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def self.close_handlers
|
|
263
|
+
error = nil
|
|
264
|
+
(@handlers || {}).each_value do |handler|
|
|
265
|
+
handler.close
|
|
266
|
+
rescue => e
|
|
267
|
+
error ||= e
|
|
268
|
+
end
|
|
269
|
+
raise error if error
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def self.get_level_for_name(name)
|
|
274
|
+
configuration[:loggers].fetch(
|
|
275
|
+
name.to_sym,
|
|
276
|
+
{
|
|
277
|
+
level: configuration[:defaultLevel],
|
|
278
|
+
handlers: configuration[:defaultHandlers]
|
|
279
|
+
}
|
|
280
|
+
)[:level]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def self.get_logger(name)
|
|
284
|
+
configure unless @handlers
|
|
285
|
+
name = name.to_sym
|
|
286
|
+
if (logger = @loggers[name])
|
|
287
|
+
return logger
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
config = configuration[:loggers].fetch(
|
|
291
|
+
name,
|
|
292
|
+
{
|
|
293
|
+
level: configuration[:defaultLevel],
|
|
294
|
+
handlers: configuration[:defaultHandlers]
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
handlers = config[:handlers].map do |h|
|
|
299
|
+
get_handler(h[:name])
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
@loggers[name] = ::Logger.new(LogShout::MultiIO.new(handlers),
|
|
303
|
+
level: ::Logger.const_get(config[:level].upcase),
|
|
304
|
+
progname: name.to_s,
|
|
305
|
+
formatter: LogShout::Formatters::Default.new)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.configuration
|
|
309
|
+
@validated_config || DEFAULT_CONFIG.serialize.freeze
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def self.config_file_path
|
|
313
|
+
@config_file_path
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def self.reset!
|
|
317
|
+
close_handlers
|
|
318
|
+
@handlers = nil
|
|
319
|
+
@config_file_path = nil
|
|
320
|
+
@validated_config = nil
|
|
321
|
+
@loggers = {}
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
LogShout.configure
|
|
326
|
+
|
|
327
|
+
at_exit do
|
|
328
|
+
LogShout.close_handlers
|
|
329
|
+
rescue => e
|
|
330
|
+
warn "LogShout shutdown failed: #{e.class}: #{e.message}"
|
|
331
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: log_shout
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matt Christen
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.7'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.7'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: standard
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.53'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.53'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: standard-performance
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.6'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.6'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ruby-lsp
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.23'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.23'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: minitest
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '5.26'
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: 5.26.1
|
|
78
|
+
type: :development
|
|
79
|
+
prerelease: false
|
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - "~>"
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '5.26'
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: 5.26.1
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: rake
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '13.1'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '13.1'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: bundler-audit
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - "~>"
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0.9'
|
|
109
|
+
type: :development
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0.9'
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: debug
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '1.11'
|
|
123
|
+
type: :development
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '1.11'
|
|
130
|
+
description: Easy to configure ruby loggers. Multi-destination and configuration levels.
|
|
131
|
+
email:
|
|
132
|
+
- oss@christen.tech
|
|
133
|
+
executables: []
|
|
134
|
+
extensions: []
|
|
135
|
+
extra_rdoc_files: []
|
|
136
|
+
files:
|
|
137
|
+
- CHANGELOG.md
|
|
138
|
+
- LICENSE
|
|
139
|
+
- README.md
|
|
140
|
+
- RELEASING.md
|
|
141
|
+
- lib/log_shout.rb
|
|
142
|
+
- lib/log_shout/formatters/default.rb
|
|
143
|
+
- lib/log_shout/helper.rb
|
|
144
|
+
- lib/log_shout/io_ractor.rb
|
|
145
|
+
- lib/log_shout/logging_ractor.rb
|
|
146
|
+
- lib/log_shout/multi_io.rb
|
|
147
|
+
- lib/log_shout/version.rb
|
|
148
|
+
homepage: https://github.com/mchristen/log_shout
|
|
149
|
+
licenses:
|
|
150
|
+
- MIT
|
|
151
|
+
metadata:
|
|
152
|
+
changelog_uri: https://github.com/mchristen/log_shout/blob/main/CHANGELOG.md
|
|
153
|
+
documentation_uri: https://github.com/mchristen/log_shout#readme
|
|
154
|
+
source_code_uri: https://github.com/mchristen/log_shout
|
|
155
|
+
bug_tracker_uri: https://github.com/mchristen/log_shout/issues
|
|
156
|
+
rubygems_mfa_required: 'true'
|
|
157
|
+
rdoc_options: []
|
|
158
|
+
require_paths:
|
|
159
|
+
- lib
|
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
161
|
+
requirements:
|
|
162
|
+
- - ">="
|
|
163
|
+
- !ruby/object:Gem::Version
|
|
164
|
+
version: 4.0.0
|
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
|
+
requirements:
|
|
167
|
+
- - ">="
|
|
168
|
+
- !ruby/object:Gem::Version
|
|
169
|
+
version: '0'
|
|
170
|
+
requirements: []
|
|
171
|
+
rubygems_version: 4.0.20
|
|
172
|
+
specification_version: 4
|
|
173
|
+
summary: Ruby loggers with flexibility.
|
|
174
|
+
test_files: []
|