Skip to content

Commit 6b4c0d7

Browse files
authored
Merge pull request #37 from Arkoniak/21-json-logger
add: initial implementation json logger (#21)
2 parents b4208fe + f04b63b commit 6b4c0d7

File tree

9 files changed

+415
-113
lines changed

9 files changed

+415
-113
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
- uses: actions/checkout@v2
5151
- uses: julia-actions/setup-julia@v1
5252
with:
53-
version: '1.3'
53+
version: '1.8'
5454
- run: |
5555
julia --project=docs -e '
5656
using Pkg

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "MiniLoggers"
22
uuid = "93f3dd0f-005d-4452-894a-a31841fa4078"
33
authors = ["Andrey Oskin and contributors"]
4-
version = "0.4.3"
4+
version = "0.5.0"
55

66
[deps]
77
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"

src/MiniLoggers.jl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ using Markdown
55
import Logging: AbstractLogger, shouldlog, min_enabled_level, catch_exceptions, handle_message, LogLevel
66
using Logging: Warn, Info, Debug, Error, BelowMinLevel, AboveMaxLevel, global_logger, with_logger, default_logcolor
77

8-
export MiniLogger, global_logger, with_logger
8+
export MiniLogger, JsonLogger, global_logger, with_logger
99

10+
# Utils
11+
include("common.jl")
1012
include("tokenizer.jl")
13+
include("modes.jl")
14+
15+
# Loggers
1116
include("minilogger.jl")
17+
include("jsonlogger.jl")
1218

1319
end # module

src/common.jl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
abstract type AbstractMiniLogger <: AbstractLogger end
2+
3+
shouldlog(logger::AbstractMiniLogger, level, _module, group, id) = lock(logger.lock) do
4+
get(logger.message_limits, id, 1) > 0
5+
end
6+
7+
min_enabled_level(logger::AbstractMiniLogger) = logger.minlevel
8+
9+
catch_exceptions(logger::AbstractMiniLogger) = true
10+
11+
function Base.close(logger::AbstractMiniLogger)
12+
if logger.io != stdout && logger.io != stderr && isopen(logger.io)
13+
close(logger.io)
14+
end
15+
16+
if logger.ioerr != stdout && logger.ioerr != stderr && isopen(logger.ioerr)
17+
close(logger.ioerr)
18+
end
19+
end
20+
21+
getio(io, append) = io
22+
getio(io::AbstractString, append) = open(io, append ? "a" : "w")
23+
24+
getflushthreshold(x::Integer) = x
25+
getflushthreshold(x::TimePeriod) = Dates.value(Millisecond(x))
26+
27+
tsnow(dtf) = Dates.format(Dates.now(), dtf)

src/jsonlogger.jl

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
struct JsonLogger{AM <: AbstractMode, IOT1 <: IO, IOT2 <: IO, DFT <: DateFormat} <: AbstractMiniLogger
2+
io::IOT1
3+
ioerr::IOT2
4+
errlevel::LogLevel
5+
minlevel::LogLevel
6+
message_limits::Dict{Any,Int}
7+
flush::Bool
8+
format::Vector{Token}
9+
dtformat::DFT
10+
mode::AM
11+
squash_delimiter::String
12+
flush_threshold::Int
13+
lastflush::Base.RefValue{Int64}
14+
lock::ReentrantLock # thread-safety of message_limits Dict
15+
end
16+
17+
"""
18+
JsonLogger(; <keyword arguments>)
19+
20+
JsonLogger constructor creates custom logger with json output, which can be used with usual `@info`, `@debug` commands.
21+
Supported keyword arguments include:
22+
23+
* `io` (default `stdout`): IO stream which is used to output log messages below `errlevel` level. Can be either `IO` or `String`, in the latter case it is treated as a name of the output file.
24+
* `ioerr` (default `stderr`): IO stream which is used to output log messages above `errlevel` level. Can be either `IO` or `String`, in the latter case it is treated as a name of the output file.
25+
* `errlevel` (default `Error`): determines which output IO to use for log messages. If you want for all messages to go to `io`, set this parameter to `MiniLoggers.AboveMaxLevel`. If you want for all messages to go to `ioerr`, set this parameter to `MiniLoggers.BelowMinLevel`.
26+
* `minlevel` (default: `Info`): messages below this level are ignored. For example with default setting `@debug "foo"` is ignored.
27+
* `append` (default: `false`): defines whether to append to output stream or to truncate file initially. Used only if `io` or `ioerr` is a file path.
28+
* `flush` (default: `true`): whether to `flush` IO stream for each log message. Flush behaviour also affected by `flush_threshold` argument.
29+
* `squash_delimiter`: (default: "\\t"): defines which delimiter to use when squashing multilines messages.
30+
* `flush_threshold::Union{Integer, TimePeriod}` (default: 0): if this argument is nonzero and `flush` is `true`, then `io` is flushed only once per `flush_threshold` milliseconds. I.e. if time between two consecutive log messages is less then `flush_threshold`, then second message is not flushed and will have to wait for the next log event.
31+
* `dtformat` (default: "yyyy-mm-dd HH:MM:SS"): if `datetime` parameter is used in `format` argument, this dateformat is applied for output timestamps.
32+
* `format`: defines which keywords should be used in the output. If defined, should be a string which defines the structure of the output json. It should use keywords, and allowed keywords are:
33+
* `timestamp`: timestamp of the log message
34+
* `level`: name of log level (Debug, Info, etc)
35+
* `filepath`: filepath of the file, which produced log message
36+
* `basename`: basename of the filepath of the file, which produced log message
37+
* `line`: line number of the log command in the file, which produced log message
38+
* `group`: log group
39+
* `module`: name of the module, which contains log command
40+
* `id`: log message id
41+
* `message`: message itself
42+
43+
Format string should consists of comma separated tokens. In it's simplest form, tokens can be just keywords, then names of the keywords are used as a fieldnames. So, for example `"timestamp,level,message"` result in `{"timestamp":"2023-01-01 12:34:56","level":"Debug","message":"some logging message"}`. If fields must be renamed, then one should use `<field name>:<keyword>` form. For example, `"severity:level"` result in `{"severity":"Debug"}`, here field name is `severity` and the value is taken from the logging `level`. One can also create nested json, in order to do it one should use `<field name>:{<format string>}` form, where previous rules for format string also applies. For example, `source:{line, file:basename}` result in `{"source":{"line":123,"file":"calculations.jl"}}`
44+
45+
By default, `format` is `timestamp,level,basename,line,message`.
46+
"""
47+
function JsonLogger(; io = stdout, ioerr = stderr, errlevel = Error, minlevel = Info, append = false, message_limits = Dict{Any, Int}(), flush = true, format = "timestamp,level,basename,line,message", dtformat = dateformat"yyyy-mm-dd HH:MM:SS", flush_threshold = 0, squash_delimiter = "\t")
48+
tio = getio(io, append)
49+
tioerr = io == ioerr ? tio : getio(ioerr, append)
50+
lastflush = Dates.value(Dates.now())
51+
JsonLogger(tio,
52+
tioerr,
53+
errlevel,
54+
minlevel,
55+
message_limits,
56+
flush,
57+
tokenize(JsonLoggerTokenizer(), format),
58+
dtformat,
59+
JsonSquash(),
60+
squash_delimiter,
61+
getflushthreshold(flush_threshold),
62+
Ref(lastflush),
63+
ReentrantLock())
64+
end
65+
66+
function handle_message(logger::JsonLogger, level, message, _module, group, id,
67+
filepath, line; maxlog=nothing, kwargs...)
68+
if maxlog !== nothing && maxlog isa Integer
69+
remaining = lock(logger.lock) do
70+
logger.message_limits[id] = max(get(logger.message_limits, id, maxlog), 0) - 1
71+
end
72+
remaining 0 || return
73+
end
74+
75+
io = if level < logger.errlevel
76+
isopen(logger.io) ? logger.io : stdout
77+
else
78+
isopen(logger.ioerr) ? logger.ioerr : stderr
79+
end
80+
81+
buf = IOBuffer()
82+
iob = IOContext(buf, io)
83+
84+
isfirst = true
85+
for token in logger.format
86+
val = token.val
87+
if val == "timestamp"
88+
print(iob, "\"", tsnow(logger.dtformat), "\"")
89+
elseif val == "level"
90+
print(iob, "\"", string(level), "\"")
91+
elseif val == "filepath"
92+
print(iob, "\"", filepath, "\"")
93+
elseif val == "basename"
94+
print(iob, "\"", basename(filepath), "\"")
95+
elseif val == "line"
96+
print(iob, line)
97+
elseif val == "group"
98+
print(iob, "\"", group, "\"")
99+
elseif val == "module"
100+
print(iob, "\"", _module, "\"")
101+
elseif val == "id"
102+
print(iob, "\"", id, "\"")
103+
elseif val == "message"
104+
print(iob, "\"")
105+
showmessage(iob, message, logger, logger.mode)
106+
if length(kwargs) > 0 && !isempty(message)
107+
print(iob, " ")
108+
end
109+
110+
iscomma = false
111+
for (k, v) in kwargs
112+
if string(k) == v
113+
print(iob, k)
114+
iscomma = false
115+
else
116+
iscomma && print(iob, ", ")
117+
print(iob, k, " = ")
118+
showvalue(iob, v, logger, logger.mode)
119+
iscomma = true
120+
end
121+
end
122+
print(iob, "\"")
123+
else
124+
print(iob, val)
125+
end
126+
end
127+
write(io, postprocess(logger.mode, logger.squash_delimiter, buf))
128+
129+
if logger.flush
130+
if logger.flush_threshold <= 0
131+
flush(io)
132+
else
133+
t = Dates.value(Dates.now())
134+
if t - logger.lastflush[] >= logger.flush_threshold
135+
logger.lastflush[] = t
136+
flush(io)
137+
end
138+
end
139+
end
140+
nothing
141+
end

src/minilogger.jl

Lines changed: 6 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
abstract type AbstractMode end
2-
struct NoTransformations <: AbstractMode end
3-
struct Squash <: AbstractMode end
4-
struct FullSquash <: AbstractMode end
5-
struct MDown <: AbstractMode end
6-
7-
struct MiniLogger{AM <: AbstractMode, IOT1 <: IO, IOT2 <: IO, DFT <: DateFormat} <: AbstractLogger
1+
struct MiniLogger{AM <: AbstractMode, IOT1 <: IO, IOT2 <: IO, DFT <: DateFormat} <: AbstractMiniLogger
82
io::IOT1
93
ioerr::IOT2
104
errlevel::LogLevel
@@ -20,12 +14,6 @@ struct MiniLogger{AM <: AbstractMode, IOT1 <: IO, IOT2 <: IO, DFT <: DateFormat}
2014
lock::ReentrantLock # thread-safety of message_limits Dict
2115
end
2216

23-
getio(io, append) = io
24-
getio(io::AbstractString, append) = open(io, append ? "a" : "w")
25-
26-
getflushthreshold(x::Integer) = x
27-
getflushthreshold(x::TimePeriod) = Dates.value(Millisecond(x))
28-
2917
getmode(mode) = mode
3018
getmode(mode::AbstractString) = getmode(Symbol(mode))
3119
function getmode(mode::Symbol)
@@ -40,16 +28,6 @@ function getmode(mode::Symbol)
4028
end
4129
end
4230

43-
function Base.close(logger::MiniLogger)
44-
if logger.io != stdout && logger.io != stderr && isopen(logger.io)
45-
close(logger.io)
46-
end
47-
48-
if logger.ioerr != stdout && logger.ioerr != stderr && isopen(logger.ioerr)
49-
close(logger.ioerr)
50-
end
51-
end
52-
5331
"""
5432
MiniLogger(; <keyword arguments>)
5533
@@ -107,83 +85,6 @@ function MiniLogger(; io = stdout, ioerr = stderr, errlevel = Error, minlevel =
10785
ReentrantLock())
10886
end
10987

110-
shouldlog(logger::MiniLogger, level, _module, group, id) = lock(logger.lock) do
111-
get(logger.message_limits, id, 1) > 0
112-
end
113-
114-
min_enabled_level(logger::MiniLogger) = logger.minlevel
115-
116-
catch_exceptions(logger::MiniLogger) = true
117-
118-
# Formatting of values in key value pairs
119-
squash(msg, logger, mode) = string(msg)
120-
function squash(msg, logger, mode::Squash)
121-
smsg = string(msg)
122-
smsg = replace(smsg, "\r" => "")
123-
smsg = replace(smsg, "\n" => logger.squash_delimiter)
124-
end
125-
126-
showvalue(io, msg, logger, mode) = print(io, squash(msg, logger, mode))
127-
128-
function showvalue(io, e::Tuple{Exception,Any}, logger, mode)
129-
ex, bt = e
130-
Base.showerror(io, ex, bt; backtrace = bt!==nothing)
131-
end
132-
showvalue(io, ex::Exception, logger, mode) = Base.showerror(io, ex)
133-
showvalue(io, ex::AbstractVector{Union{Ptr{Nothing}, Base.InterpreterIP}}, logger, mode) = Base.show_backtrace(io, ex)
134-
135-
# Here we are fighting with multiple dispatch.
136-
# If message is `Exception` or `Tuple{Exception, Any}` or anything else
137-
# then we want to ignore third argument.
138-
# But if it is any other sort of message we want to dispatch result
139-
# on the type of message transformation
140-
_showmessage(io, msg, logger, mode) = print(io, squash(msg, logger, mode))
141-
_showmessage(io, msg, logger, ::MDown) = show(io, MIME"text/plain"(), Markdown.parse(msg))
142-
143-
showmessage(io, msg, logger, mode) = _showmessage(io, msg, logger, mode)
144-
showmessage(io, e::Tuple{Exception,Any}, logger, mode) = showvalue(io, e, logger, mode)
145-
showmessage(io, ex::Exception, logger, mode) = showvalue(io, ex, logger, mode)
146-
showmessage(io, ex::AbstractVector{Union{Ptr{Nothing}, Base.InterpreterIP}}, logger, mode) = Base.show_backtrace(io, ex)
147-
148-
function postprocess(mode, delimiter, iobuf)
149-
print(iobuf, "\n")
150-
take!(iobuf)
151-
end
152-
153-
function postprocess(mode::FullSquash, delimiter, iobuf)
154-
buf = take!(iobuf)
155-
delm = Vector{UInt8}(delimiter)
156-
res = similar(buf)
157-
L = length(res)
158-
j = 1
159-
@inbounds for (i, c) in pairs(buf)
160-
c == UInt8('\r') && continue
161-
if c == UInt8('\n')
162-
for c2 in delm
163-
if j > L
164-
resize!(res, 2*L)
165-
L *= 2
166-
end
167-
res[j] = c2
168-
j += 1
169-
end
170-
continue
171-
end
172-
res[j] = c
173-
j += 1
174-
end
175-
if j > L
176-
resize!(res, 2*L)
177-
L *= 2
178-
end
179-
res[j] = UInt8('\n')
180-
181-
resize!(res, j)
182-
183-
return res
184-
end
185-
186-
tsnow(dtf) = Dates.format(Dates.now(), dtf)
18788

18889
function colorfunc(level::LogLevel, _module, group, id, filepath, line, element)
18990
level < Info ? Color(Base.debug_color()) :
@@ -243,14 +144,14 @@ function handle_message(logger::MiniLogger, level, message, _module, group, id,
243144
end
244145

245146
iscomma = false
246-
for (key, val) in kwargs
247-
if string(key) == val
248-
print(iob, key)
147+
for (k, v) in kwargs
148+
if string(k) == v
149+
print(iob, k)
249150
iscomma = false
250151
else
251152
iscomma && print(iob, ", ")
252-
print(iob, key, " = ")
253-
showvalue(iob, val, logger, logger.mode)
153+
print(iob, k, " = ")
154+
showvalue(iob, v, logger, logger.mode)
254155
iscomma = true
255156
end
256157
end

0 commit comments

Comments
 (0)