diff --git a/src/MicroLogging.jl b/src/MicroLogging.jl index 64bd199..ed3e58e 100644 --- a/src/MicroLogging.jl +++ b/src/MicroLogging.jl @@ -13,7 +13,7 @@ export ## Log creation @debug, @info, @warn, @error, @logmsg, ## Logger installation and control - with_logger, current_logger, global_logger, disable_logging + with_logger, current_logger, global_logger, disable_logging, filterlogs # ----- MicroLogging & stdlib Logging API ----- export @@ -21,6 +21,10 @@ export # config system) configure_logging, ConsoleLogger, + AbstractLogFilter, + LogLevelFilter, + MaxlogFilter, + CatchLogErrors, InteractiveLogger import Base.CoreLogging: @@ -37,6 +41,7 @@ include("StickyMessages.jl") include("ConsoleLogger.jl") include("InteractiveLogger.jl") # deprecated +include("filters.jl") include("config.jl") end diff --git a/src/filters.jl b/src/filters.jl new file mode 100644 index 0000000..adfb975 --- /dev/null +++ b/src/filters.jl @@ -0,0 +1,171 @@ +""" + AbstractLogFilter + +An `AbstractLogFilter` defines filtering and mapping rules for log events. +Log filters can be composed together using `∘` and applied to the log stream +generated by a function call using `filterlogs`. + + +Implementing a log filter means overriding some subset of the following +methods. + +* `min_enabled_level` +* `shouldlog` +* `catch_exceptions` +* `handle_message` + +TODO: Methods for attach and detach, to handle stateful filters? +TODO: Configurable log record matching seems to be a thing, not just for testing but for applying filter actions. +""" +abstract type AbstractLogFilter end + +# Simple filters don't affect early filtering by default +min_enabled_level(f::AbstractLogFilter) = nothing +shouldlog(f::AbstractLogFilter, args...; kwargs...) = true +catch_exceptions(f::AbstractLogFilter) = nothing + +# Simple filters pass message directly through to sink by default, but they can +# modify the message by overriding this function. +handle_message(f::AbstractLogFilter, sink, args...; kwargs...) = + handle_message(sink, args...; kwargs...) + + +""" + FilteringLogger(parent, filter) + +A logger which applies `filter` to all incoming messages and then passes them +on to the `parent` logger for further processing. `filter` may both map and +filter messages; see `AbstractLogFilter`. +""" +struct FilteringLogger{P<:AbstractLogger,F<:AbstractLogFilter} <: AbstractLogger + parent::P + filter::F +end + +shouldlog(f::FilteringLogger, args...) = shouldlog(f.filter, args...) && shouldlog(f.parent, args...) +catch_exceptions(f::FilteringLogger) = something(catch_exceptions(f.filter), catch_exceptions(f.parent)) + +min_enabled_level(f::FilteringLogger) = something(min_enabled_level(f.filter), min_enabled_level(f.parent)) + +function handle_message(f::FilteringLogger, args...; kwargs...) + if !shouldlog(f.filter, args...; kwargs...) + return + end + handle_message(f.filter, f.parent, args...; kwargs...) +end + + +struct ComposedLogFilter{F,G} <: AbstractLogFilter + filter1::F + filter2::G +end + +Base.:∘(f1::AbstractLogFilter, f2::AbstractLogFilter) = ComposedLogFilter(f1, f2) + +FilteringLogger(parent::AbstractLogger, f::ComposedLogFilter) = + FilteringLogger(FilteringLogger(parent, f.filter2), f.filter1) + + +#------------------------------------------------------------------------------- +# Concrete log filter implementations + +""" +Disables messages below a given level +""" +struct LogLevelFilter <: AbstractLogFilter + default_min_level::LogLevel + module_limits::Dict{Module,LogLevel} +end + +function LogLevelFilter(min_level=Info, limits::Pair...) + LogLevelFilter(min_level, Dict{Module,LogLevel}(limits...)) +end + +function min_enabled_level(f::LogLevelFilter) + min_level = f.default_min_level + for (_,level) ∈ f.module_limits + if level < min_level + min_level = level + end + end + min_level +end + +shouldlog(f::LogLevelFilter, level, _module, group, id) = + !(level < get(f.module_limits, _module, f.default_min_level)) + +shouldlog(f::LogLevelFilter, args...; kwargs...) = true + + +#------------------------------------------------------------------------------- +""" + MaxlogFilter() + +Filter messages from log statements with a `maxlog=N` key value pair, which +occur more than `N` times. +""" +struct MaxlogFilter <: AbstractLogFilter + message_limits::Dict{Any,Int} +end + +shouldlog(f::MaxlogFilter, level, _module, group, id) = get(f.message_limits, id, 1) > 0 + +function shouldlog(f::MaxlogFilter, args...; maxlog=nothing, kwargs...) + if maxlog === nothing || !(maxlog isa Integer) + return true + end + remaining = get!(f.message_limits, id, maxlog) + f.message_limits[id] = remaining - 1 + remaining > 0 +end + +""" +Catch log exceptions, or allow them to propagate. +""" +struct CatchLogErrors <: AbstractLogFilter + catch_exceptions::Bool +end + +catch_exceptions(f::CatchLogErrors) = f.catch_exceptions + +# TODO: Sticky filter +# TODO: Downgrade filter +# TODO: Filter to conditionally add keywords + +#------------------------------------------------------------------------------- +function filterlogs(func, f::AbstractLogFilter) + parent = current_logger() + logger = FilteringLogger(parent, f) + with_logger(func, logger) + # TODO: For sticky filter, make `with_logger` `attach` and `detach` the + # loggers? + # + # replace_logger(logger) +end + +# Prototypes + +#= +struct MaxlogFilter{ParentLogger<:AbstractLogger} <: AbstractLogger + parent::ParentLogger + message_limits::Dict{Any,Int} +end + +MaxlogFilter(parent<:AbstractLogger) = MaxlogFilter(parent, Dict{Any,Int}()) + +shouldlog(f::MaxlogFilter, level, _module, group, id) = + get(f.message_limits, id, 1) > 0 && shouldlog(f.parent) + +min_enabled_level(f::MaxlogFilter) = min_enabled_level(f.parent) + +function handle_message(f::MaxlogFilter, args...; maxlog=nothing, kwargs...) + if maxlog !== nothing && maxlog isa Integer + remaining = get!(logger.message_limits, id, maxlog) + logger.message_limits[id] = remaining - 1 + remaining > 0 || return + end + + handle_message(f.parent, args...; kwargs...) +end +=# +