Skip to content

Commit 8d601e6

Browse files
authored
Merge pull request #35 from fredrikekre/fe/formatlogger
FormatLogger
2 parents 2261ce2 + f25251e commit 8d601e6

File tree

4 files changed

+166
-64
lines changed

4 files changed

+166
-64
lines changed

README.md

Lines changed: 84 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ LoggingExtras is a composable logging system.
1818

1919
Loggers can be broken down into 4 types:
2020
- *Sinks*: Sinks are the final end point of a log messages journey. They write it to file, or display it on the console, or set off a red flashing light in the laboratory. A Sink should never decide what to accept, only what to do with it.
21-
- *Filters*: Filters wrap around other loggers and decide wether or not to pass on a message. Thery can further be broken down by when that decision occurs (See `ActiveFilteredLogger` vs `EarlyFilteredLogger`).
21+
- *Filters*: Filters wrap around other loggers and decide whether or not to pass on a message. They can further be broken down by when that decision occurs (See `ActiveFilteredLogger` vs `EarlyFilteredLogger`).
2222
- *Transformers*: Transformers modify the content of log messages, before passing them on. This includes the metadata like severity level. Unlike Filters they can't block a log message, but they could drop its level down to say `Debug` so that normally noone would see it.
23-
- *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.
23+
- *Demux*: There is only one possible Demux Logger. and it is central to log routing. It acts as a hub that receives 1 log message, and then sends copies of it to all its child loggers. Like in 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,22 +81,24 @@ 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.
88-
- The `TransformerLogger` applies a function to modify log messages before passing them on.
8989
- The 3 filter loggers are used to control if a message is written or not
90-
- The `MinLevelLogger` only allowes messages to pass that are above a given level of severity
90+
- The `MinLevelLogger` only allows messages to pass that are above a given level of severity
9191
- The `EarlyFilteredLogger` lets you write filter rules based on the `level`, `module`, `group` and `id` of the log message
9292
- The `ActiveFilteredLogger` lets you filter based on the full content
93+
- The `TransformerLogger` applies a function to modify log messages before passing them on.
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

98100

99-
## `TeeLogger`
101+
## `TeeLogger` (*Demux*)
100102

101103
The `TeeLogger` sends the log messages to multiple places.
102104
It takes a list of loggers.
@@ -107,53 +109,7 @@ It is up to those loggers to determine if they will accept it.
107109
Which they do using their methods for `shouldlog` and `min_enabled_level`.
108110
Or you can do, by wrapping them in a filtered logger as discussed below.
109111

110-
## `FileLogger`
111-
The `FileLogger` does logging to file.
112-
It is just a convience wrapper around the base julia `SimpleLogger`,
113-
to make it easier to pass in a filename, rather than a stream.
114-
It is really simple.
115-
- It takes a filename,
116-
- a kwarg to check if should `always_flush` (default: `true`).
117-
- a kwarg to `append` rather than overwrite (default `false`. i.e. overwrite by default)
118-
The resulting file format is similar to that which is shown in the REPL.
119-
(Not identical, but similar)
120-
121-
### Demo: `TeeLogger` and `FileLogger`
122-
We are going to log info and above to one file,
123-
and warnings and above to another.
124-
125-
```julia
126-
julia> using Logging; using LoggingExtras;
127-
128-
julia> demux_logger = TeeLogger(
129-
MinLevelLogger(FileLogger("info.log"), Logging.Info),
130-
MinLevelLogger(FileLogger("warn.log"), Logging.Warn),
131-
);
132-
133-
134-
julia> with_logger(demux_logger) do
135-
@warn("It is bad")
136-
@info("normal stuff")
137-
@error("THE WORSE THING")
138-
@debug("it is chill")
139-
end
140-
141-
shell> cat warn.log
142-
┌ Warning: It is bad
143-
└ @ Main REPL[34]:2
144-
┌ Error: THE WORSE THING
145-
└ @ Main REPL[34]:4
146-
147-
shell> cat info.log
148-
┌ Warning: It is bad
149-
└ @ Main REPL[34]:2
150-
┌ Info: normal stuff
151-
└ @ Main REPL[34]:3
152-
┌ Error: THE WORSE THING
153-
└ @ Main REPL[34]:4
154-
```
155-
156-
## `ActiveFilteredLogger`
112+
## `ActiveFilteredLogger` (*Filter*)
157113

158114
The `ActiveFilteredLogger` exists to give more control over which messages should be logged.
159115
It warps any logger, and before sending messages to the logger to log,
@@ -183,7 +139,7 @@ end
183139
[ Info: Yo Dawg! it is all good
184140
```
185141
186-
## `EarlyFilteredLogger`
142+
## `EarlyFilteredLogger` (*Filter*)
187143
188144
The `EarlyFilteredLogger` is similar to the `ActiveFilteredLogger`,
189145
but it runs earlier in the logging pipeline.
@@ -233,7 +189,7 @@ end
233189
└ ii = 10
234190
```
235191
236-
## `MinLevelLogger`
192+
## `MinLevelLogger` (*Filter*)
237193
This is basically a special case of the early filtered logger,
238194
that just checks if the level of the message is above the level specified when it was created.
239195
@@ -253,12 +209,12 @@ julia> with_logger(error_only_logger) do
253209
└ @ Main REPL[18]:4
254210
```
255211
256-
## `TransformerLogger`
212+
## `TransformerLogger` (*Transformer*)
257213
The transformer logger allows for the modification of log messages.
258214
This modification includes such things as its log level, and content,
259215
and all the other arguments passed to `handle_message`.
260216
261-
When constructing a `TransformerLogger` you pass in a tranformation function,
217+
When constructing a `TransformerLogger` you pass in a transformation function,
262218
and a logger to be wrapped.
263219
The transformation function takes a named tuple containing all the log message fields,
264220
and should return a new modified named tuple.
@@ -291,7 +247,53 @@ It can also be used to do things such as change the log level of messages from a
291247
Or to set common properties for all log messages within the `with_logger` block,
292248
for example to set them all to the same `group`.
293249
294-
## `DatetimeRotatingFileLogger`
250+
## `FileLogger` (*Sink*)
251+
The `FileLogger` does logging to file.
252+
It is just a convenience wrapper around the base julia `SimpleLogger`,
253+
to make it easier to pass in a filename, rather than a stream.
254+
It is really simple.
255+
- It takes a filename,
256+
- a kwarg to check if should `always_flush` (default: `true`).
257+
- a kwarg to `append` rather than overwrite (default `false`. i.e. overwrite by default)
258+
The resulting file format is similar to that which is shown in the REPL.
259+
(Not identical, but similar)
260+
261+
### Demo: `TeeLogger` and `FileLogger`
262+
We are going to log info and above to one file,
263+
and warnings and above to another.
264+
265+
```julia
266+
julia> using Logging; using LoggingExtras;
267+
268+
julia> demux_logger = TeeLogger(
269+
MinLevelLogger(FileLogger("info.log"), Logging.Info),
270+
MinLevelLogger(FileLogger("warn.log"), Logging.Warn),
271+
);
272+
273+
274+
julia> with_logger(demux_logger) do
275+
@warn("It is bad")
276+
@info("normal stuff")
277+
@error("THE WORSE THING")
278+
@debug("it is chill")
279+
end
280+
281+
shell> cat warn.log
282+
┌ Warning: It is bad
283+
└ @ Main REPL[34]:2
284+
┌ Error: THE WORSE THING
285+
└ @ Main REPL[34]:4
286+
287+
shell> cat info.log
288+
┌ Warning: It is bad
289+
└ @ Main REPL[34]:2
290+
┌ Info: normal stuff
291+
└ @ Main REPL[34]:3
292+
┌ Error: THE WORSE THING
293+
└ @ Main REPL[34]:4
294+
```
295+
296+
## `DatetimeRotatingFileLogger` (*Sink*)
295297
Use this sink to rotate your logs based upon a given `DateFormat`, automatically closing one file and opening another
296298
when the `DateFormat` would change the filename. Note that if you wish to have static portions of your filename, you must
297299
escape them so they are not interpreted by the `DateFormat` code. Example:
@@ -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` (*Sink*)
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
@@ -368,7 +389,7 @@ global_logger(transformer_logger)
368389
## Add timestamp to all logging
369390
370391
```julia
371-
using Logging, LoggingExtras, Dates
392+
using Logging, LoggingExtras, Dates
372393

373394
const date_format = "yyyy-mm-dd HH:MM:SS"
374395

@@ -379,7 +400,7 @@ end
379400
ConsoleLogger(stdout, Logging.Debug) |> timestamp_logger |> global_logger
380401
```
381402
382-
This will produce output similar to:
403+
This will produce output similar to:
383404
```julia
384405
[ Info: 2019-09-20 17:43:54 /es/update 200
385406
┌ Debug: 2019-09-20 18:03:25 Recompiling stale cache file /.julia/compiled/v1.2/TranslationsController.ji for TranslationsController [top-level]

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)