|
| 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 |
0 commit comments