From 861a07f10ad5523f6814bbb19429be9546ff2431 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Thu, 30 May 2024 17:38:50 +0200 Subject: [PATCH] allow customizing logfmt standard keys (#16) * allow customizing logfmt standard keys * don't define `STANDARD_KEYS` twice * bump project --- Project.toml | 2 +- README.md | 15 ++++++++++ src/LoggingFormats.jl | 70 ++++++++++++++++++++++++++++++++----------- test/runtests.jl | 16 ++++++++++ 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/Project.toml b/Project.toml index b7048da..5cc3d12 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "LoggingFormats" uuid = "98105f81-4425-4516-93fd-1664fb551ab6" -version = "1.4.0" +version = "1.5.0" [deps] JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" diff --git a/README.md b/README.md index 2602736..3b21c6f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,21 @@ level=error msg="something is wrong" module=Main file="REPL[2]" line=3 group="RE Similarly to the JSON logger, `LogFmt` handles exceptions specially, by printing errors and stacktraces using `Base.showerror`. +One can also restrict the "standard" keys used in the log message, for example: + +```julia +julia> using LoggingFormats, LoggingExtras + +julia> with_logger(FormatLogger(LoggingFormats.LogFmt((:level, :message, :file)), stderr)) do + @info "hello, world" extra="bye" + @error "something is wrong" + end +level=info msg="hello, world" file="REPL[5]" extra="bye" +level=error msg="something is wrong" file="REPL[5]" +``` + +Note here that the `module`, `group`, `id` do not appear, since they weren't specified, but the "custom" key `extra` still appears. The full set of standard keys is `(:level, :msg, :module, :file, :line, :group, :id)`, which are all used by default, in that order. + ## `Truncated`: Truncate long variables and messages `LoggingFormats.Truncated(max_var_len=5_000)` is a function which formats data in similar manner as `ConsoleLogger`, diff --git a/src/LoggingFormats.jl b/src/LoggingFormats.jl index 32ab0f3..fe84eba 100644 --- a/src/LoggingFormats.jl +++ b/src/LoggingFormats.jl @@ -147,26 +147,60 @@ end ############ # See https://brandur.org/logfmt +""" + LogFmt(standard_keys=$STANDARD_KEYS) + LogFmt(standard_keys...) + +Creates a `logfmt` format logger. The log message includes each of the `standard_keys`, as well as any "custom" keys. For example, + +```julia +julia> using LoggingFormats, LoggingExtras + +julia> with_logger(FormatLogger(LoggingFormats.LogFmt((:level, :message, :file)), stderr)) do + @info "hello, world" extra="bye" + @error "something is wrong" + end +level=info msg="hello, world" file="REPL[5]" extra="bye" +level=error msg="something is wrong" file="REPL[5]" +``` + +Note that the order of arguments to `LogFmt` is respected in the log printing. +""" struct LogFmt <: Function + standard_keys::NTuple{<:Any,Symbol} + function LogFmt(keys::NTuple{N,Symbol}) where {N} + extra = setdiff(keys, STANDARD_KEYS) + + if !isempty(extra) + if length(extra) == 1 + extra = first(extra) + plural = "" + else + extra = Tuple(extra) + plural = "s" + end + throw(ArgumentError("Unsupported standard logging key$plural `$(repr(extra))` found. The only supported keys are: `$STANDARD_KEYS`.")) + end + return new(keys) + end end -function (::LogFmt)(io, args) - print(io, "level=", lvlstr(args.level), - " msg=\"", - ) - escape_string(io, args.message isa AbstractString ? args.message : string(args.message), '"') - print(io, "\"", - " module=", something(args._module, "nothing"), - " file=\"", - ) - escape_string(io, args.file isa AbstractString ? args.file : string(something(args.file, "nothing")), '"') - print(io, "\"", - " line=", something(args.line, "nothing"), - " group=\"", - ) - escape_string(io, args.group isa AbstractString ? args.group : string(something(args.group, "nothing")), '"') - print(io, "\"", - " id=", something(args.id, "nothing"), - ) +LogFmt() = LogFmt(STANDARD_KEYS) +LogFmt(keys::Symbol...) = LogFmt(keys) + +function fmtval(k, v) + k == :level && return lvlstr(v) + return v isa AbstractString ? v : string(something(v, "nothing")) +end + +function (l::LogFmt)(io, args) + for (i, k) in enumerate(l.standard_keys) + i == 1 || print(io, ' ') + print(io, k, '=') + k in (:level, :module) || print(io, '"') + k_lookup = k === :module ? :_module : k === :msg ? :message : k + escape_string(io, fmtval(k, getproperty(args, k_lookup)), '"') + k in (:level, :module) || print(io, '"') + end for (k, v) in args.kwargs print(io, " ", k, "=\"") v = maybe_stringify_exceptions(v) diff --git a/test/runtests.jl b/test/runtests.jl index 57b850d..53edc24 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -207,6 +207,22 @@ end end @testset "logfmt" begin + # Unsupported keys: + @test_throws ArgumentError("Unsupported standard logging key `:hi` found. The only supported keys are: `(:level, :msg, :module, :file, :line, :group, :id)`.") LogFmt((:hi,)) + @test_throws ArgumentError("Unsupported standard logging keys `(:hi, :bye)` found. The only supported keys are: `(:level, :msg, :module, :file, :line, :group, :id)`.") LogFmt((:hi, :bye)) + @test_throws MethodError LogFmt("no") + + # Fewer keys, out of order + io = IOBuffer() + with_logger(FormatLogger(LogFmt(:msg, :level, :file), io)) do + @debug "debug msg" extra="hi" + @info "info msg" _file="file with space.jl" + end + strs = collect(eachline(seekstart(io))) + @test match(r"msg=\"debug msg\" level=debug file=\"(.*)\" extra=\"hi\"", strs[1]) !== nothing + @test strs[2] == "msg=\"info msg\" level=info file=\"file with space.jl\"" + + # Standard: io = IOBuffer() with_logger(FormatLogger(LogFmt(), io)) do @debug "debug msg"