Skip to content

Commit 92cbdfd

Browse files
committed
Add a FormatLogger logger sink.
1 parent 2261ce2 commit 92cbdfd

File tree

4 files changed

+107
-5
lines changed

4 files changed

+107
-5
lines changed

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Loggers can be broken down into 4 types:
2323
- *Demux*: There is only one possible Demux Logger. and it is central to log routing. It acts as a hub that recieves 1 log message, and then sends copies of it to all its child loggers. Like iin the diagram above, it can be composed with Filters to control what goes where.
2424

2525
This is a basically full taxonomy of all compositional loggers.
26-
Other than `Sinks`, this package implements the full set. So you shouldn't need to build your own routing components, just configure the ones included in this package.
26+
This package implements the full set. So you shouldn't need to build your own routing components, just configure the ones included in this package.
2727

2828
It is worth understanding the idea of logging purity.
2929
The loggers defined in this package are all pure.
@@ -81,9 +81,10 @@ logger = global_logger()
8181

8282

8383
# Loggers introduced by this package:
84-
This package introduces 7 new loggers.
85-
The `TeeLogger`, the `TransformerLogger`, 3 types of filtered logger, and the `FileLogger`.
86-
All of them just wrap existing loggers.
84+
This package introduces 8 new loggers.
85+
The `TeeLogger`, the `TransformerLogger`, 3 types of filtered logger, the `FileLogger`,
86+
the `DatetimeRotatingFileLogger` and the `FormatLogger`.
87+
All of them, except `FormatLogger`, just wrap existing loggers.
8788
- The `TeeLogger` sends the logs to multiple different loggers.
8889
- The `TransformerLogger` applies a function to modify log messages before passing them on.
8990
- The 3 filter loggers are used to control if a message is written or not
@@ -92,6 +93,7 @@ All of them just wrap existing loggers.
9293
- The `ActiveFilteredLogger` lets you filter based on the full content
9394
- The `FileLogger` is a simple logger sink that writes to file.
9495
- The `DatetimeRotatingFileLogger` is a logger sink that writes to file, rotating logs based upon a user-provided `DateFormat`.
96+
- The `FormatLogger` is a logger sink that simply formats the message and writes to the logger stream.
9597

9698
By combining `TeeLogger` with filter loggers you can arbitrarily route log messages, wherever you want.
9799

@@ -315,6 +317,25 @@ julia> filter(f -> endswith(f, ".log"), readdir(pwd()))
315317
316318
The user implicitly controls when the files will be rolled over based on the `DateFormat` given.
317319
320+
## `FormatLogger`
321+
The `FormatLogger` is a sink that formats the message and prints to a wrapped IO.
322+
Formatting is done by providing a function `f(io::IO, log_args::NamedTuple)`.
323+
324+
```julia
325+
julia> using Logging, LoggingExtras
326+
327+
julia> logger = FormatLogger() do io, args
328+
println(io, args._module, " | ", "[", args.level, "] ", args.message)
329+
end;
330+
331+
julia> with_logger(logger) do
332+
@info "This is an informational message."
333+
@warn "This is a warning, should take a look."
334+
end
335+
Main | [Info] This is an informational message.
336+
Main | [Warn] This is a warning, should take a look.
337+
```
338+
318339
# More Examples
319340
320341
## Filter out any overly long messages

src/LoggingExtras.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Base.CoreLogging:
1010

1111
export TeeLogger, TransformerLogger, FileLogger,
1212
ActiveFilteredLogger, EarlyFilteredLogger, MinLevelLogger,
13-
DatetimeRotatingFileLogger
13+
DatetimeRotatingFileLogger, FormatLogger
1414

1515

1616
######
@@ -39,6 +39,7 @@ include("earlyfiltered.jl")
3939
include("minlevelfiltered.jl")
4040
include("filelogger.jl")
4141
include("datetime_rotation.jl")
42+
include("formatlogger.jl")
4243
include("deprecated.jl")
4344

4445
end # module

src/formatlogger.jl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
struct FormatLogger <: AbstractLogger
3+
f::Function
4+
io::IO
5+
always_flush::Bool
6+
end
7+
8+
"""
9+
FormatLogger(f::Function, io::IO=stderr; always_flush=true)
10+
11+
Logger sink that formats the message and finally writes to `io`.
12+
The formatting function should be of the form `f(io::IOContext, log_args::NamedTuple)`
13+
where `log_args` has the following fields:
14+
`(level, message, _module, group, id, file, line, kwargs)`.
15+
See `?LoggingExtra.handle_message_args` for more information on what field is.
16+
17+
# Examples
18+
```julia-repl
19+
julia> using Logging, LoggingExtras
20+
21+
julia> logger = FormatLogger() do io, args
22+
println(io, args._module, " | ", "[", args.level, "] ", args.message)
23+
end;
24+
25+
julia> with_logger(logger) do
26+
@info "This is an informational message."
27+
@warn "This is a warning, should take a look."
28+
end
29+
Main | [Info] This is an informational message.
30+
Main | [Warn] This is a warning, should take a look.
31+
```
32+
"""
33+
function FormatLogger(f::Function, io::IO=stderr; always_flush=true)
34+
return FormatLogger(f, io, always_flush)
35+
end
36+
37+
function handle_message(logger::FormatLogger, args...; kwargs...)
38+
log_args = handle_message_args(args...; kwargs...)
39+
# We help the user by passing an IOBuffer to the formatting function
40+
# to make sure that everything writes to the logger io in one go.
41+
iob = IOBuffer()
42+
ioc = IOContext(iob, logger.io)
43+
logger.f(ioc, log_args)
44+
write(logger.io, take!(iob))
45+
logger.always_flush && flush(logger.io)
46+
return nothing
47+
end
48+
shouldlog(logger::FormatLogger, arg...) = true
49+
min_enabled_level(logger::FormatLogger) = BelowMinLevel
50+
catch_exceptions(logger::FormatLogger) = true # Or false? SimpleLogger doesn't, ConsoleLogger does.

test/runtests.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,36 @@ end
159159
end
160160
end
161161

162+
@testset "FormatLogger" begin
163+
io = IOBuffer()
164+
logger = FormatLogger(io) do io, args
165+
# Put in some bogus sleep calls just to test that
166+
# log records writes in one go
167+
print(io, args.level)
168+
sleep(rand())
169+
print(io, ": ")
170+
sleep(rand())
171+
println(io, args.message)
172+
end
173+
with_logger(logger) do
174+
@sync begin
175+
@async @debug "debug message"
176+
@async @info "info message"
177+
@async @warn "warning message"
178+
@async @error "error message"
179+
end
180+
end
181+
str = String(take!(io))
182+
@test occursin(r"^Debug: debug message$"m, str)
183+
@test occursin(r"^Info: info message$"m, str)
184+
@test occursin(r"^Warn: warning message$"m, str)
185+
@test occursin(r"^Error: error message$"m, str)
186+
@test logger.always_flush
187+
# Test constructor with default io and kwarg
188+
logger = FormatLogger(x -> x; always_flush=false)
189+
@test logger.io === stderr
190+
@test !logger.always_flush
191+
end
162192

163193
@testset "Deprecations" begin
164194
testlogger = TestLogger(min_level=BelowMinLevel)

0 commit comments

Comments
 (0)