Skip to content

Commit 0ee09fc

Browse files
authored
Merge pull request #9 from oxinabox/ox/philo
A more pure design
2 parents 5114301 + 1c26386 commit 0ee09fc

13 files changed

+543
-202
lines changed

Project.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
authors = ["Lyndon White <[email protected]>"]
21
name = "LoggingExtras"
32
uuid = "bda5643c-bbc4-11e8-2bd7-9ffae705afc0"
4-
version = "0.1.0"
5-
6-
[deps]
3+
authors = ["Lyndon White <[email protected]>"]
4+
version = "0.2.0"
75

86
[compat]
9-
julia = "0.7"
7+
julia = "0.7, 1"
108

119
[extras]
1210
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

README.md

Lines changed: 201 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,49 @@
66

77
![Diagram showing how loggers connect](diag.svg)
88

9+
# Discussion: Compositional Loggers
10+
11+
LoggingExtras is designs around allowing you to build arbitrarily complicated
12+
systems for "log plumbing". That is to say basically routing logged information to different places.
13+
It is built around the idea of simple parts which are composed together,
14+
to allow for powerful and flexible definition of your logging system.
15+
Without having to define any custom loggers by subtyping `AbstractLogger`.
16+
When we talk about composability we mean to say that the composition of any set of Loggers is itself a Logger.
17+
LoggingExtras is a composable logging system.
18+
19+
Loggers can be broken down into 4 types:
20+
- *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`).
22+
- *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.
24+
25+
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.
27+
28+
It is worth understanding the idea of logging purity.
29+
The loggers defined in this package are all pure.
30+
The Filters, only filter, the Sinks only sink, the transformers only Transform.
31+
32+
We can contrast this to the the `ConsoleLogger` (the standard logger in the REPL).
33+
The `ConsoleLogger` is an impure sink.
34+
As well as displaying logs to the user (as a Sink);
35+
it uses the log content, in the form of the `max_log` kwarg to decide if a log should be displayed (Active Filtering);
36+
and it has a min_enabled_level setting, that controls if it will accept a message at all
37+
(Early Filtering, in particular see `MinLevelLogger`).
38+
If it was to be defined in a compositional way,
39+
we would write something along the lines of:
40+
```
41+
ConsoleLogger(stream, min_level) =
42+
MinLevelLogger(
43+
ActiveFilteredLogger(max_log_filter,
44+
PureConsoleLogger(stream)
45+
),
46+
min_level
47+
)
48+
```
49+
950

10-
## Usage
51+
# Usage
1152
Load the package with `using LoggingExtras`.
1253
You likely also want to load the `Logging` standard lib.
1354
Loggers can be constructed and used like normal.
@@ -19,7 +60,7 @@ For full details, see the [Julia documentation on Logging](https://docs.julialan
1960
To use a `logger` in a given scope do
2061
```
2162
with_logger(logger) do
22-
#things
63+
#things
2364
end
2465
```
2566

@@ -34,56 +75,62 @@ logger = global_logger()
3475
```
3576

3677
# Loggers introduced by this package:
37-
38-
39-
This package introduces 3 new loggers.
40-
The `DemuxLogger`, the `FilteredLogger` and the `FileLogger`.
78+
This package introduces 6 new loggers.
79+
The `DemuxLogger`, the `TransformerLogger`, 3 types of filtered logger, and the `FileLogger`.
4180
All of them just wrap existing loggers.
42-
The `DemuxLogger` sends the logs to multiple different loggers.
43-
The `FilteredLogger` lets you add rules to cause a logger to ignore some inputs.
81+
- The `DemuxLogger` sends the logs to multiple different loggers.
82+
- The `TransformerLogger` applies a function to modify log messages before passing them on.
83+
- The 3 filter loggers are used to control if a message is written or not
84+
- The `MinLevelLogger` only allowes messages to pass that are above a given level of severity
85+
- The `EarlyFilteredLogger` lets you write filter rules based on the `level`, `module`, `group` and `id` of the log message
86+
- The `ActiveFilteredLogger` lets you filter based on the full content
87+
- The `FileLogger` is a simple logger sink that writes to file.
4488

89+
By combining `DemuxLogger` with filter loggers you can arbitrarily route log messages, wherever you want.
4590

46-
By combining `DemuxLogger` with `FilteredLogger`s you can arbitrarily route log messages, wherever you want.
4791

48-
The `FileLogger` is just a convience wrapper around the base julia `SimpleLogger`,
49-
to make it easier to pass in a filename, rather than a stream.
50-
51-
52-
## `DemuxLogger` and `FileLogger`
92+
## `DemuxLogger`
5393

5494
The `DemuxLogger` sends the log messages to multiple places.
5595
It takes a list of loggers.
5696
It also has the keyword argument `include_current_global`,
5797
to determine if you also want to log to the global logger.
5898

59-
It is up to those loggers to determine if they will accept it.\
99+
It is up to those loggers to determine if they will accept it.
60100
Which they do using their methods for `shouldlog` and `min_enabled_level`.
61-
Or you can do, by wrapping them in a `FilteredLogger` as discussed below.
101+
Or you can do, by wrapping them in a filtered logger as discussed below.
62102

103+
## `FileLogger`
63104
The `FileLogger` does logging to file.
105+
It is just a convience wrapper around the base julia `SimpleLogger`,
106+
to make it easier to pass in a filename, rather than a stream.
64107
It is really simple.
65-
It takes a filename; and the minimum level it should log.
108+
- It takes a filename,
109+
- a kwarg to check if should `always_flush` (default: `true`).
110+
- a kwarg to `append` rather than overwrite (default `false`. i.e. overwrite by default)
111+
The resulting file format is similar to that which is shown in the REPL.
112+
(Not identical, but similar)
66113

67-
### Demo
114+
### Demo: `DemuxLogger` and `FileLogger`
68115
We are going to log info and above to one file,
69116
and warnings and above to another.
70117

71118
```
72119
julia> using Logging; using LoggingExtras;
73120
74121
julia> demux_logger = DemuxLogger(
75-
FileLogger("info.log", min_level=Logging.Info),
76-
FileLogger("warn.log", min_level=Logging.Warn),
77-
include_current_global=false
78-
);
122+
MinLevelLogger(FileLogger("info.log"), Logging.Info),
123+
MinLevelLogger(FileLogger("warn.log"), Logging.Warn),
124+
include_current_global=false
125+
);
79126
80127
81128
julia> with_logger(demux_logger) do
82-
@warn("It is bad")
83-
@info("normal stuff")
84-
@error("THE WORSE THING")
85-
@debug("it is chill")
86-
end
129+
@warn("It is bad")
130+
@info("normal stuff")
131+
@error("THE WORSE THING")
132+
@debug("it is chill")
133+
end
87134
88135
shell> cat warn.log
89136
┌ Warning: It is bad
@@ -100,51 +147,140 @@ shell> cat info.log
100147
└ @ Main REPL[34]:4
101148
```
102149

103-
## `FilteredLogger`
150+
## `ActiveFilteredLogger`
104151

105-
The `FilteredLogger` exists to give more control over which messages should be logged.
152+
The `ActiveFilteredLogger` exists to give more control over which messages should be logged.
106153
It warps any logger, and before sending messages to the logger to log,
107154
checks them against a filter function.
108155
The filter function takes the full set of parameters of the message.
109-
(See it's docstring with `?FilteredLogger` for more details.)
156+
(See it's docstring with `?ActiveFilteredLogger` for more details.)
110157

111158
### Demo
112159
We want to filter to only log strings staring with `"Yo Dawg!"`.
113160

114161
```
115-
julia> function yodawg_filter(level, message, _module, group, id, file, line; kwargs...)
116-
startswith(msg, "Yo Dawg!")
162+
julia> function yodawg_filter(log_args)
163+
startswith(log_args.message, "Yo Dawg!")
117164
end
118-
yodawg_filter (generic function with 1 method)
165+
yodawg_filter (generic function with 1 method)
119166
120-
julia> filtered_logger = FilteredLogger(yodawg_filter, global_logger());
167+
julia> filtered_logger = ActiveFilteredLogger(yodawg_filter, global_logger());
121168
122169
julia> with_logger(filtered_logger) do
123-
@info "Boring message"
124-
@warn "Yo Dawg! it is bad"
125-
@info "Another boring message"
126-
@info "Yo Dawg! it is all good"
127-
end
170+
@info "Boring message"
171+
@warn "Yo Dawg! it is bad"
172+
@info "Another boring message"
173+
@info "Yo Dawg! it is all good"
174+
end
128175
┌ Warning: Yo Dawg! it is bad
129176
└ @ Main REPL[28]:3
130177
[ Info: Yo Dawg! it is all good
131178
```
132179

180+
## `EarlyFilteredLogger`
181+
182+
The `EarlyFilteredLogger` is similar to the `ActiveFilteredLogger`,
183+
but it runs earlier in the logging pipeline.
184+
In particular it runs before the message is computed.
185+
It can be useful to filter things early if creating the log message is expensive.
186+
E.g. if it includes summary statistics of the error.
187+
The filter function for early filter logging only has access to the
188+
`level`, `_module`, `id` and `group` fields of the log message.
189+
The most notable use of it is to filter based on modules,
190+
see the HTTP example below.
191+
192+
Another example is using them to stop messages every being repeated within a given time period.
193+
194+
```
195+
using Dates, Logging, LoggingExtras
196+
197+
julia> function make_throttled_logger(period)
198+
history = Dict{Symbol, DateTime}()
199+
# We are going to use a closure
200+
EarlyFilteredLogger(global_logger()) do log
201+
if !haskey(history, log.id) || (period < now() - history[log.id])
202+
# then we will log it, and update record of when we did
203+
history[log.id] = now()
204+
return true
205+
else
206+
return false
207+
end
208+
end
209+
end
210+
make_throttled_logger (generic function with 1 method)
211+
212+
julia> throttled_logger = make_throttled_logger(Second(3));
133213
214+
julia> with_logger(throttled_logger) do
215+
for ii in 1:10
216+
sleep(1)
217+
@info "It happened" ii
218+
end
219+
end
220+
┌ Info: It happened
221+
└ ii = 1
222+
┌ Info: It happened
223+
└ ii = 4
224+
┌ Info: It happened
225+
└ ii = 7
226+
┌ Info: It happened
227+
└ ii = 10
228+
```
134229

135-
# Examples
230+
## `MinLevelLogger`
231+
This is basically a special case of the early filtered logger,
232+
that just checks if the level of the message is above the level specified when it was created.
233+
234+
## `TransformerLogger`
235+
The transformer logger allows for the modification of log messages.
236+
This modification includes such things as its log level, and content,
237+
and all the other arguments passed to `handle_message`.
238+
239+
When constructing a `TransformerLogger` you pass in a tranformation function,
240+
and a logger to be wrapped.
241+
The transformation function takes a named tuple containing all the log message fields,
242+
and should return a new modified named tuple.
243+
244+
A simple example of its use is truncating messages.
245+
246+
```
247+
julia> using Logging, LoggingExtras
248+
249+
julia> truncating_logger = TransformerLogger(global_logger()) do log
250+
if length(log.message) > 128
251+
short_message = log.message[1:min(end, 125)] * "..."
252+
return merge(log, (;message=short_message))
253+
else
254+
return log
255+
end
256+
end;
257+
258+
julia> with_logger(truncating_logger) do
259+
@info "the truncating logger only truncates long messages"
260+
@info "Like this one that is this is a long and rambling message, it just keeps going and going and going, and it seems like it will never end."
261+
@info "Not like this one, that is is short"
262+
end
263+
[ Info: the truncating logger only truncates long messages
264+
[ Info: Like this one that is this is a long and rambling message, it just keeps going and going and going, and it seems like it wil...
265+
[ Info: Not like this one, that is is short
266+
```
267+
268+
It can also be used to do things such as change the log level of messages from a particular module (see the example below).
269+
270+
271+
# More Examples
136272

137273
## Filter out any overly long messages
138274

139275
```
140276
using LoggingExtras
141277
using Logging
142278
143-
function sensible_message_filter(level, message, _module, group, id, file, line; kwargs...)
144-
length(message) < 1028
279+
function sensible_message_filter(log)
280+
length(log.message) < 1028
145281
end
146282
147-
global_logger(FilteredLogger(sensible_message_filter, global_logger()))
283+
global_logger(ActiveFilteredLogger(sensible_message_filter, global_logger()))
148284
```
149285

150286

@@ -155,10 +291,29 @@ using LoggingExtras
155291
using Logging
156292
using HTTP
157293
158-
function not_HTTP_message_filter(level, message, _module, group, id, file, line; kwargs...)
159-
_module != HTTP
294+
function not_HTTP_message_filter(log)
295+
log._module != HTTP
160296
end
161297
162-
global_logger(FilteredLogger(not_HTTP_message_filter, global_logger()))
298+
global_logger(EarlyFilteredLogger(not_HTTP_message_filter, global_logger()))
163299
```
164300

301+
## Raising HTTP debug level errors to be Info level
302+
303+
```
304+
using LoggingExtras
305+
using Logging
306+
using HTTP
307+
308+
transformer_logger(global_logger()) do log
309+
if log._module == HTTP && log.level=Logging.Debug
310+
# Merge can be used to construct a new NamedTuple
311+
# which effectively is the overwriting of fields of a NamedTuple
312+
return merge(log, (; level=Logging.Info))
313+
else
314+
return log
315+
end
316+
end
317+
318+
global_logger(transformer_logger)
319+
```

src/LoggingExtras.jl

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,37 @@ using Base.CoreLogging:
66

77
import Base.CoreLogging:
88
AbstractLogger, SimpleLogger,
9-
handle_message, shouldlog, min_enabled_level, catch_exceptions
9+
handle_message, shouldlog, min_enabled_level, catch_exceptions
1010

1111
export demux_global_logger,
12-
DemuxLogger, FilteredLogger, FileLogger
12+
DemuxLogger, TransformerLogger, FileLogger,
13+
ActiveFilteredLogger, EarlyFilteredLogger, MinLevelLogger
1314

14-
include("demuxlogger.jl")
15-
include("filteredlogger.jl")
15+
16+
######
17+
# Utilities for dealing with compositional loggers.
18+
# Since the logging system itself will not engage its checks
19+
# Once the first logger has started, any compositional logger needs to check
20+
# before passing anything on.
21+
22+
# For checking child logger, need to check both `min_enabled_level` and `shouldlog`
23+
function comp_shouldlog(logger, args...)
24+
level = first(args)
25+
min_enabled_level(logger) <= level && shouldlog(logger, args...)
26+
end
27+
28+
# For checking if child logger will take the message you are sending
29+
function comp_handle_message_check(logger, args...; kwargs...)
30+
level, message, _module, group, id, file, line = args
31+
return comp_shouldlog(logger, level, _module, group, id)
32+
end
33+
###############################
34+
35+
include("demux.jl")
36+
include("transformer.jl")
37+
include("activefiltered.jl")
38+
include("earlyfiltered.jl")
39+
include("minlevelfiltered.jl")
1640
include("filelogger.jl")
1741

1842
end # module

0 commit comments

Comments
 (0)