From 7baa57718e44228925f61b7f27cfd1d05c94b414 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Mon, 1 Jan 2024 20:57:03 -0500 Subject: [PATCH] Add `@create_log_macro` for making custom styled logging macros (#52196) --- NEWS.md | 5 ++++ base/logging.jl | 18 ++++++++------ stdlib/Logging/docs/src/index.md | 14 ++++++++++- stdlib/Logging/src/ConsoleLogger.jl | 1 + stdlib/Logging/src/Logging.jl | 37 +++++++++++++++++++++++++++++ stdlib/Logging/test/runtests.jl | 20 +++++++++++----- 6 files changed, 81 insertions(+), 14 deletions(-) diff --git a/NEWS.md b/NEWS.md index 488be0f3209b4..7b4cda50b8985 100644 --- a/NEWS.md +++ b/NEWS.md @@ -118,6 +118,11 @@ Standard library changes * Structured matrices now retain either the axes of the parent (for `Symmetric`/`Hermitian`/`AbstractTriangular`/`UpperHessenberg`), or that of the principal diagonal (for banded matrices) ([#52480]). * `bunchkaufman` and `bunchkaufman!` now work for any `AbstractFloat`, `Rational` and their complex variants. `bunchkaufman` now supports `Integer` types, by making an internal conversion to `Rational{BigInt}`. Added new function `inertia` that computes the inertia of the diagonal factor given by the `BunchKaufman` factorization object of a real symmetric or Hermitian matrix. For complex symmetric matrices, `inertia` only computes the number of zero eigenvalues of the diagonal factor ([#51487]). +#### Logging +* New `@create_log_macro` macro for creating new log macros like `@info`, `@warn` etc. For instance + `@create_log_macro MyLog 1500 :magenta` will create `@mylog` to be used like `@mylog "hello"` which + will show as `┌ MyLog: hello` etc. ([#52196]) + #### Printf #### Profile diff --git a/base/logging.jl b/base/logging.jl index f6a34aee2f516..1853caa16148c 100644 --- a/base/logging.jl +++ b/base/logging.jl @@ -162,14 +162,18 @@ const AboveMaxLevel = LogLevel( 1000001) # Global log limiting mechanism for super fast but inflexible global log limiting. const _min_enabled_level = Ref{LogLevel}(Debug) +# stored as LogLevel => (name, color) +const custom_log_levels = Dict{LogLevel,Tuple{Symbol,Union{Symbol,Int}}}() + function show(io::IO, level::LogLevel) - if level == BelowMinLevel print(io, "BelowMinLevel") - elseif level == Debug print(io, "Debug") - elseif level == Info print(io, "Info") - elseif level == Warn print(io, "Warn") - elseif level == Error print(io, "Error") - elseif level == AboveMaxLevel print(io, "AboveMaxLevel") - else print(io, "LogLevel($(level.level))") + if haskey(custom_log_levels, level) print(io, custom_log_levels[level][1]) + elseif level == BelowMinLevel print(io, "BelowMinLevel") + elseif level == Debug print(io, "Debug") + elseif level == Info print(io, "Info") + elseif level == Warn print(io, "Warn") + elseif level == Error print(io, "Error") + elseif level == AboveMaxLevel print(io, "AboveMaxLevel") + else print(io, "LogLevel($(level.level))") end end diff --git a/stdlib/Logging/docs/src/index.md b/stdlib/Logging/docs/src/index.md index 55d24c7ae0a26..882c66af2baef 100644 --- a/stdlib/Logging/docs/src/index.md +++ b/stdlib/Logging/docs/src/index.md @@ -58,7 +58,7 @@ automatically extracted. Let's examine the user-defined data first: * The *log level* is a broad category for the message that is used for early filtering. There are several standard levels of type [`LogLevel`](@ref); user-defined levels are also possible. - Each is distinct in purpose: + Each built-in log level is distinct in purpose: - [`Logging.Debug`](@ref) (log level -1000) is information intended for the developer of the program. These events are disabled by default. - [`Logging.Info`](@ref) (log level 0) is for general information to the user. @@ -70,6 +70,17 @@ automatically extracted. Let's examine the user-defined data first: Often this log-level is unneeded as throwing an exception can convey all the required information. + You can create logging macros for custom log levels. For instance: + ```julia-repl + julia> using Logging + + julia> @create_log_macro MyLog 200 :magenta + @mylog (macro with 1 method) + + julia> @mylog "hello" + [ MyLog: hello + ``` + * The *message* is an object describing the event. By convention `AbstractString`s passed as messages are assumed to be in markdown format. Other types will be displayed using `print(io, obj)` or `string(obj)` for @@ -298,6 +309,7 @@ Logging.Debug Logging.Info Logging.Warn Logging.Error +Logging.@create_log_macro ``` ### [Processing events with AbstractLogger](@id AbstractLogger-interface) diff --git a/stdlib/Logging/src/ConsoleLogger.jl b/stdlib/Logging/src/ConsoleLogger.jl index 747f8a2b22966..1d45296c907d1 100644 --- a/stdlib/Logging/src/ConsoleLogger.jl +++ b/stdlib/Logging/src/ConsoleLogger.jl @@ -58,6 +58,7 @@ end showvalue(io, ex::Exception) = showerror(io, ex) function default_logcolor(level::LogLevel) + level in keys(custom_log_levels) ? custom_log_levels[level][2] : level < Info ? Base.debug_color() : level < Warn ? Base.info_color() : level < Error ? Base.warn_color() : diff --git a/stdlib/Logging/src/Logging.jl b/stdlib/Logging/src/Logging.jl index 0743c650326cc..83e93ee396361 100644 --- a/stdlib/Logging/src/Logging.jl +++ b/stdlib/Logging/src/Logging.jl @@ -21,6 +21,7 @@ for sym in [ Symbol("@warn"), Symbol("@error"), Symbol("@logmsg"), + :custom_log_levels, :with_logger, :current_logger, :global_logger, @@ -29,6 +30,41 @@ for sym in [ @eval const $sym = Base.CoreLogging.$sym end +""" + @create_log_macro(name::Symbol, level::Int, color::Union{Int,Symbol}) + +Creates a custom log macro like `@info`, `@warn` etc. with a given `name`, `level` and +`color`. The macro created is named with the lowercase form of `name` but the given form +is used for the printing. + +The available color keys can be seen by typing `Base.text_colors` in the help mode of the REPL + +```julia-repl +julia> @create_log_macro(:MyLog, 200, :magenta) +@mylog (macro with 1 method) + +julia> @mylog "hello" +[ MyLog: hello +``` +""" +macro create_log_macro(name, level, color) + macro_name = Symbol(lowercase(string(name))) + macro_string = QuoteNode(name) + loglevel = LogLevel(level) + if loglevel in (BelowMinLevel, Debug, Info, Warn, Error, AboveMaxLevel) + throw(ArgumentError("Cannot use the same log level as a built in log macro")) + end + if haskey(custom_log_levels, loglevel) + throw(ArgumentError("Custom log macro already exists for given log level")) + end + quote + $(custom_log_levels)[$(esc(loglevel))] = ($(macro_string), $(esc(color))) + macro $(esc(macro_name))(exs...) + $(Base.CoreLogging.logmsg_code)(($(Base.CoreLogging.@_sourceinfo))..., $(esc(loglevel)), exs...) + end + end +end + # LogLevel aliases (re-)documented here (JuliaLang/julia#40978) """ Debug @@ -67,6 +103,7 @@ export @warn, @error, @logmsg, + @create_log_macro, with_logger, current_logger, global_logger, diff --git a/stdlib/Logging/test/runtests.jl b/stdlib/Logging/test/runtests.jl index 3a793c4e0bc33..e7b3fbca2098d 100644 --- a/stdlib/Logging/test/runtests.jl +++ b/stdlib/Logging/test/runtests.jl @@ -7,8 +7,8 @@ import Logging: min_enabled_level, shouldlog, handle_message @noinline func1() = backtrace() # see "custom log macro" testset -CustomLog = LogLevel(-500) -macro customlog(exs...) Base.CoreLogging.logmsg_code((Base.CoreLogging.@_sourceinfo)..., esc(CustomLog), exs...) end +@create_log_macro CustomLog1 -500 :magenta +@create_log_macro CustomLog2 1500 1 @testset "Logging" begin @@ -280,16 +280,24 @@ end end @testset "custom log macro" begin - @test_logs (CustomLog, "a") min_level=CustomLog @customlog "a" + llevel = LogLevel(-500) + + @test_logs (llevel, "foo") min_level=llevel @customlog1 "foo" buf = IOBuffer() io = IOContext(buf, :displaysize=>(30,80), :color=>false) - logger = ConsoleLogger(io, CustomLog) + logger = ConsoleLogger(io, llevel) + + with_logger(logger) do + @customlog1 "foo" + end + @test occursin("CustomLog1: foo", String(take!(buf))) + with_logger(logger) do - @customlog "a" + @customlog2 "hello" end - @test occursin("LogLevel(-500): a", String(take!(buf))) + @test occursin("CustomLog2: hello", String(take!(buf))) end end