Skip to content

allow customizing logfmt standard keys #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
70 changes: 52 additions & 18 deletions src/LoggingFormats.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading