From 57dd263cd231160fb0e2c2f2e5607e319b4b8929 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Thu, 18 Nov 2021 18:57:16 +0100 Subject: [PATCH] add `recursive` option to JSON log format (#8) --- Project.toml | 2 +- README.md | 14 ++++++++++++++ src/LoggingFormats.jl | 38 ++++++++++++++++++++++++++++---------- test/runtests.jl | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/Project.toml b/Project.toml index 010fee2..2e740be 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "LoggingFormats" uuid = "98105f81-4425-4516-93fd-1664fb551ab6" -version = "1.0.0" +version = "1.1.0" [deps] JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" diff --git a/README.md b/README.md index 654f6d4..30bf77e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,20 @@ julia> with_logger(FormatLogger(LoggingFormats.JSON(), stderr)) do {"level":"error","msg":"something is wrong","module":"Main","file":"REPL[10]","line":3,"group":"REPL[10]","id":"Main_2289c7f9","kwargs":{}} ``` +One can also pass `recursive=true` to recursively serialize the `kwargs` as JSON: + +```julia +julia> using LoggingFormats, LoggingExtras + +julia> with_logger(FormatLogger(LoggingFormats.JSON(; recursive=true), stderr)) do + @info "hello, world" key=Dict("hello" => true) + end +{"level":"info","msg":"hello, world","module":"Main","file":"REPL[18]","line":2,"group":"REPL[18]","id":"Main_ffce16b5","kwargs":{"key":{"hello":true}}} +``` + +If it encounters something which does not have a defined `StructTypes.StructType` to use +for serializing to JSON, it will fallback to converting the objects to strings, like the default `recursive=false` option does. + ## `LogFmt`: Format log events as logfmt `LoggingFormats.LogFmt()` is a function which formats the log message in the diff --git a/src/LoggingFormats.jl b/src/LoggingFormats.jl index 88e5b96..0b80092 100644 --- a/src/LoggingFormats.jl +++ b/src/LoggingFormats.jl @@ -56,7 +56,7 @@ lvlstr(lvl::Logging.LogLevel) = lvl >= Logging.Error ? "error" : lvl >= Logging.Info ? "info" : "debug" -struct JSONLogMessage +struct JSONLogMessage{T} level::String msg::String _module::Union{String,Nothing} @@ -64,10 +64,14 @@ struct JSONLogMessage line::Union{Int,Nothing} group::Union{String,Nothing} id::Union{String,Nothing} - kwargs::Dict{String,String} + kwargs::Dict{String,T} end -function JSONLogMessage(args) - JSONLogMessage( + +transform(::Type{String}, v) = string(v) +transform(::Type{Any}, v) = v + +function JSONLogMessage{T}(args) where {T} + JSONLogMessage{T}( lvlstr(args.level), args.message isa AbstractString ? args.message : string(args.message), args._module === nothing ? nothing : string(args._module), @@ -75,18 +79,32 @@ function JSONLogMessage(args) args.line, args.group === nothing ? nothing : string(args.group), args.id === nothing ? nothing : string(args.id), - Dict{String,String}(string(k) => string(v) for (k, v) in args.kwargs) + Dict{String,T}(string(k) => transform(T, v) for (k, v) in args.kwargs) ) end -StructTypes.StructType(::Type{JSONLogMessage}) = StructTypes.OrderedStruct() -StructTypes.names(::Type{JSONLogMessage}) = ((:_module, :module), ) +StructTypes.StructType(::Type{<:JSONLogMessage}) = StructTypes.OrderedStruct() +StructTypes.names(::Type{<:JSONLogMessage}) = ((:_module, :module), ) struct JSON <: Function + recursive::Bool end -function (::JSON)(io, args) - logmsg = JSONLogMessage(args) - JSON3.write(io, logmsg) +JSON(; recursive=false) = JSON(recursive) + +function (j::JSON)(io, args) + if j.recursive + logmsg = JSONLogMessage{Any}(args) + try + JSON3.write(io, logmsg) + catch e + fallback_msg = JSONLogMessage{String}(args) + fallback_msg.kwargs["LoggingFormats.FormatError"] = sprint(Base.showerror, e) + JSON3.write(io, fallback_msg) + end + else + logmsg = JSONLogMessage{String}(args) + JSON3.write(io, logmsg) + end println(io) return nothing end diff --git a/test/runtests.jl b/test/runtests.jl index 719fb46..8782f03 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -46,6 +46,10 @@ import JSON3 @test occursin("│ short_var = a", str) end +struct NoStructTypeDefined + f::Int +end + @testset "JSON" begin @test LoggingFormats.lvlstr(Logging.Error + 1) == "error" @test LoggingFormats.lvlstr(Logging.Error) == "error" @@ -93,6 +97,38 @@ end @test json.line isa Int @test json.kwargs.x == "[1, 2, 3]" @test json.kwargs.y == "(1, 2)" + + # `recursive=true` + io = IOBuffer() + with_logger(FormatLogger(JSON(; recursive=true), io)) do + y = (1, 2) + @info "info msg" x = [1, 2, 3] y = Dict("hi" => Dict("hi2" => [1,2])) + end + json = JSON3.read(seekstart(io)) + @test json.level == "info" + @test json.msg == "info msg" + @test json.module == "Main" + @test json.line isa Int + @test json.kwargs.x == [1, 2, 3] + @test json.kwargs.y == Dict(:hi => Dict(:hi2 => [1,2])) + + # Fallback to strings + io = IOBuffer() + with_logger(FormatLogger(JSON(; recursive=true), io)) do + y = (1, 2) + @info "info msg" x = [1, 2, 3] y = Dict("hi" => NoStructTypeDefined(1)) + end + json = JSON3.read(seekstart(io)) + @test json.level == "info" + @test json.msg == "info msg" + @test json.module == "Main" + @test json.line isa Int + @test json.kwargs.x == "[1, 2, 3]" + y = json.kwargs.y + must_have = ("Dict", "\"hi\"", "=>", "NoStructTypeDefined(1)") + @test all(h -> occursin(h, y), must_have) # avoid issues with printing changing with versions + @test json.kwargs[Symbol("LoggingFormats.FormatError")] == "ArgumentError: NoStructTypeDefined doesn't have a defined `StructTypes.StructType`" + end @testset "logfmt" begin