Skip to content

Commit 3e5f32c

Browse files
authored
Merge pull request #64 from quinnj/jq/debugutilities
Alternative approach to debug logging utilities.
2 parents 396afbb + b20b42e commit 3e5f32c

File tree

5 files changed

+222
-1
lines changed

5 files changed

+222
-1
lines changed

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ All of them, except `FormatLogger`, just wrap existing loggers.
9393
- The `FileLogger` is a simple logger sink that writes to file.
9494
- The `DatetimeRotatingFileLogger` is a logger sink that writes to file, rotating logs based upon a user-provided `DateFormat`.
9595
- The `FormatLogger` is a logger sink that simply formats the message and writes to the logger stream.
96+
- The `LevelOverrideLogger` for overriding the log level of other loggers
9697

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

@@ -374,6 +375,61 @@ Main | [Info] This is an informational message.
374375
Main | [Warn] This is a warning, should take a look.
375376
```
376377
378+
## `LevelOverrideLogger` (*Filter*)
379+
Allows overriding the minimum log level set by the logger it wraps.
380+
Useful when debug logging
381+
and used in conjuction with `Logging.with_logger` or `LoggingExtras.withlevel` to
382+
temporarily modify the current logger with a custom level.
383+
More generally useful if you want to use the current/global logger as a _sink_ but don't know if it is going to have a problematically high min log level set (as julia's default logger sets min level to `Info`).
384+
385+
```julia
386+
julia> using LoggingExtras
387+
388+
julia> logger = LevelOverrideLogger(Debug, global_logger())
389+
390+
julia> with_logger(logger) do
391+
@debug "This message will log since we're overriding the global Info default log level"
392+
end
393+
┌ Debug: This message will log since we're overriding the global Info default log level
394+
└ @ Main REPL[33]:2
395+
```
396+
This is roughly complementary to the `MinLevelFilterLogger`.
397+
The `MinLevelFilterLogger` lets you effectively *raise* the level of any logger it wraps to meet the level you specify.
398+
The `LevelOverrideLogger` lets you *lower* (or *raise*) the level of the wrapped logger as it bypasses checks on it entirely.
399+
400+
# Utilities
401+
402+
## Verbosity macros
403+
Sometimes when logging, it is desirable to be able to specify a verbosity level along with
404+
the log level, and to be able to filter on verbosity levels. For example, you may want multiple levels
405+
of verbosity for `Debug` log statements. LoggingExtras.jl exports verbosity macros that act like their
406+
non-verbose counterparts, but allow specifying a verbosity level as well:
407+
* `@debugv N msg`
408+
* `@infov N msg`
409+
* `@warnv N msg`
410+
* `@errorv N msg`
411+
412+
For verbosity filtering, the `LoggingExtras.withlevel(f, Info; verbosity=0)` utlility is provided
413+
for temporarily (i.e. while `f()` is executed) allowing log messages with `level` and `verbosity`.
414+
This is very handy for allowing finer grained control in debug logging for long-running or complex user API function
415+
calls. For example:
416+
417+
```julia
418+
using LoggingExtras
419+
420+
function complex_user_call(; verbose=0)
421+
LoggingExtras.withlevel(Debug; verbosity=verbose)
422+
# execute complex function body
423+
@debugv 1 "a level 1 verbosity debug message"
424+
@debugv 2 "a more verbose level 2 debug message"
425+
end
426+
end
427+
```
428+
429+
This allows easy control by the user to specify verbosity (by passing `verbose=2` or any > 0 value),
430+
and convenience for the function developer by being able to sprinkle `@debugv N msg` calls as desired,
431+
even in highly nested functions.
432+
377433
# More Examples
378434
379435
## Filter out any overly long messages

src/LoggingExtras.jl

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

1111
export TeeLogger, TransformerLogger, FileLogger,
1212
ActiveFilteredLogger, EarlyFilteredLogger, MinLevelLogger,
13-
DatetimeRotatingFileLogger, FormatLogger
13+
DatetimeRotatingFileLogger, FormatLogger, LevelOverrideLogger,
14+
@debugv, @infov, @warnv, @errorv, @logmsgv
1415

1516
######
1617
# Re export Logging.jl from stdlib
@@ -50,6 +51,8 @@ include("minlevelfiltered.jl")
5051
include("filelogger.jl")
5152
include("formatlogger.jl")
5253
include("datetime_rotation.jl")
54+
include("overridelogger.jl")
55+
include("verbosity.jl")
5356
include("deprecated.jl")
5457

5558
end # module

src/overridelogger.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
LevelOverrideLogger(level, logger)
3+
4+
A logger that allows overriding the log level of a child level.
5+
Useful in debugging scenarios, where it is desirable to ignore
6+
the log level any other logger may have set.
7+
"""
8+
struct LevelOverrideLogger{T <: AbstractLogger} <: AbstractLogger
9+
level::LogLevel
10+
logger::T
11+
end
12+
13+
14+
handle_message(logger::LevelOverrideLogger, args...; kwargs...) =
15+
handle_message(logger.logger, args...; kwargs...)
16+
17+
function shouldlog(logger::LevelOverrideLogger, level, args...)
18+
# Ignore the logger.logger's own level and instead check the override level
19+
level >= logger.level && shouldlog(logger.logger, level, args...)
20+
end
21+
22+
min_enabled_level(logger::LevelOverrideLogger) = logger.level
23+
catch_exceptions(logger::LevelOverrideLogger) = catch_exceptions(logger.logger)

src/verbosity.jl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
function restore_callsite_source_position!(expr, src)
2+
@assert expr.head == :escape
3+
@assert expr.args[1].head == :macrocall
4+
@assert expr.args[1].args[2] isa LineNumberNode
5+
# used to fix the logging source file + line
6+
# since we're lowering our verbose logging macros to the
7+
# Logging.jl macros; otherwise, they would always report this (verbosity.jl)
8+
# file as the logging callsite
9+
expr.args[1].args[2] = src
10+
return expr
11+
end
12+
13+
vlogmacrodocs = """
14+
@debugv N msg args...
15+
@infov N msg args...
16+
@warnv N msg args...
17+
@errorv N msg args...
18+
19+
"Verbose" logging macros. Drop in replacements of standard logging macros, but an
20+
additional verbosity level `N` is passed to indicate differing verbosity levels
21+
for a given log level. The verbosity argument is subtracted from the base log level when passed down
22+
to the core logging logic, so `@debugv 1 msg` will essentially call `@logmsg Debug-1 msg`.
23+
24+
An `LoggingExtras.LevelOverrideLogger`can then be used to filter on the `level` argument.
25+
For convenience, the
26+
[`LoggintExtras.with(f; level, verbosity)`](@ref) function is provided to temporarily
27+
wrap the current logger with a log level and verbosity subtracted to filter while `f` is executed.
28+
"""
29+
30+
"$vlogmacrodocs"
31+
macro debugv(verbosity::Int, msg, exs...)
32+
return restore_callsite_source_position!(
33+
esc(:($Base.@logmsg (Logging.Debug - $verbosity) $msg $(exs...))),
34+
__source__,
35+
)
36+
end
37+
38+
"$vlogmacrodocs"
39+
macro infov(verbosity::Int, msg, exs...)
40+
return restore_callsite_source_position!(
41+
esc(:($Base.@logmsg (Logging.Info - $verbosity) $msg $(exs...))),
42+
__source__,
43+
)
44+
end
45+
46+
"$vlogmacrodocs"
47+
macro warnv(verbosity::Int, msg, exs...)
48+
return restore_callsite_source_position!(
49+
esc(:($Base.@logmsg (Logging.Warn - $verbosity) $msg $(exs...))),
50+
__source__,
51+
)
52+
end
53+
54+
"$vlogmacrodocs"
55+
macro errorv(verbosity::Int, msg, exs...)
56+
return restore_callsite_source_position!(
57+
esc(:($Base.@logmsg (Logging.Error - $verbosity) $msg $(exs...))),
58+
__source__,
59+
)
60+
end
61+
62+
"$vlogmacrodocs"
63+
macro logmsgv(verbosity::Int, level, msg, exs...)
64+
return restore_callsite_source_position!(
65+
esc(:($Base.@logmsg ($level - $verbosity) $msg $(exs...))),
66+
__source__,
67+
)
68+
end
69+
70+
"""
71+
LoggingExtras.withlevel(f, level; verbosity::Integer=0)
72+
73+
Convenience function like `Logging.with_logger` to temporarily wrap
74+
the current logger with a level filter while `f` is executed.
75+
That is, the current logger will still be used for actual logging, but
76+
log messages will first be checked that they meet the `level`
77+
log level before being passed on to be logged.
78+
"""
79+
function withlevel(f, level::Union{Int, LogLevel}=Info; verbosity::Integer=0)
80+
lvl = Base.CoreLogging._min_enabled_level[]
81+
try
82+
# by default, this global filter is Debug, but for debug logging
83+
# we want to enable sub-Debug levels
84+
Base.CoreLogging._min_enabled_level[] = BelowMinLevel
85+
with_logger(LevelOverrideLogger(level - verbosity, current_logger())) do
86+
f()
87+
end
88+
finally
89+
Base.CoreLogging._min_enabled_level[] = lvl
90+
end
91+
end

test/runtests.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,54 @@ end
239239
end
240240
end
241241

242+
@testset "LevelOverrideLogger" begin
243+
logger = TestLogger(min_level=Info)
244+
with_logger(LevelOverrideLogger(Debug, logger)) do
245+
@debug "debug message"
246+
@info "info message"
247+
248+
end
249+
@test length(logger.logs) == 2
250+
@test map(x -> x.level, logger.logs) == [Debug, Info]
251+
end
252+
253+
@testset "Verbosity utilities" begin
254+
logger = TestLogger(min_level=Info)
255+
with_logger(logger) do
256+
@infov 1 "info 1 message"
257+
end
258+
@test isempty(logger.logs)
259+
260+
logger = TestLogger(min_level=Info)
261+
with_logger(logger) do
262+
LoggingExtras.withlevel(Debug) do
263+
@debug "debug message"
264+
end
265+
end
266+
@test logger.logs[1].level == Debug
267+
268+
logger = TestLogger(min_level=Info)
269+
with_logger(logger) do
270+
LoggingExtras.withlevel(Debug; verbosity=1) do
271+
@debugv 0 "debug 0 message"
272+
@debugv 1 "debug 1 message"
273+
@debugv 2 "debug 2 message"
274+
end
275+
end
276+
@test length(logger.logs) == 2
277+
@test map(x -> x.level, logger.logs) == [Debug, Debug-1]
278+
279+
logger = TestLogger(min_level=Info)
280+
with_logger(logger) do
281+
with_logger(MinLevelLogger(current_logger(), Info)) do
282+
LoggingExtras.withlevel(Debug; verbosity=1) do
283+
@debug "This should show up, even though it is behind 2 info level filters"
284+
end
285+
end
286+
end
287+
@test_broken length(logger.logs) == 1
288+
end
289+
242290
@testset "Deprecations" begin
243291
testlogger = TestLogger(min_level=BelowMinLevel)
244292

0 commit comments

Comments
 (0)