实现一个新的日志记录器

在此教程中,我们将会经历实现一个新记录器的必需步骤。这包括定义一个新的结构体,它是 Logging.AbstractLogger 的子类型,并且为此记录器接口实现必需的方法。

Note

通常,除非你要实现一个新的记录器接收器(logger sink),否则不需要定义新的记录器来获取你想要的行为。LoggingExtras.jl 包提供了任意路由,转换,和过滤日志事件的记录器。例如,在此示例中实现的记录器使用 TransformerLogger 实现起来很简单(请参阅本页的最后一部分)。

作为一个玩具示例,我们将实现一个加密记录器–一个使用 Caesar cipher) 来“加密”消息的记录器。我们想要记录器接收任何日志事件,并且可以使用输出流进行配置。开始吧!

Tip

跟随教程把代码片段复制粘贴到 Julia REPL 中执行,是个不错的注意!

第一步是加载 Logging.jl 标准库,它定义了记录日志的基础设置和必须方法,我们需要为新记录器进行扩展。然后我们定义新记录器的结构,确保使用 Logging.AbstractLogger 作为超类(supertype):

using Logging

struct CipherLogger <: Logging.AbstractLogger
    io::IO
    key::Int
end

CipherLogger(key::Int=3) = CipherLogger(stderr, key)

输入到记录器结构的是 I/O 流和凯撒密码的密钥,所有的日志消息都会被写入此流,密钥只是一个整数。一个供外部使用的方便结构也会定义一些默认值:I/O 流默认为 stderr,密钥默认为 3

下一步,我们需要为新的 CipherLogger 扩展必需的方法:

Logging.min_enabled_level

此方法应该返回日志记录器接收消息的最小等级。Julia REPL 中默认记录器只接收 Logging.Info 及以上级别的消息,例如,我们想要我们的记录器接收任何消息,因此我们简单的返回 Logging.BelowMinLevel,这是最小的合理级别:

Logging.min_enabled_level(logger::CipherLogger) = Logging.BelowMinLevel

Logging.shouldlog

此方法是过滤日志消息的下一个机会。输入参数是记录器,日志消息级别,被创建日志事件的所属模块,和唯一标识事件创建位置的日志事件 id。基于此信息,我们可以决定记录器是否应该接收消息并返回 true,或者它应该丢弃消息并返回 false。再一次,因为我们想让我们的记录器接收任何消息,无论输入参数是什么,我们都简单的返回 true

function Logging.shouldlog(logger::CipherLogger, level, _module, group, id)
   return true
end

Logging.catch_exceptions

此方法决定我们的记录器是否应该捕获来自日志记录系统的异常。这是可以做到的,例如,当生成日志消息时发生的异常。如果 catch_exceptions 返回 true,日志记录系统将发送一个 error 日志消息到记录器,否则不发送。让我们接收这些 error 日志消息:

Logging.catch_exceptions(logger::CipherLogger) = true

Logging.handle_message

此方法是日志事件最终到达的位置。输入参数是记录器,日志级别,消息,和关于消息来源位置的元数据(module, group, id, file, 和 line)。另外,日志事件可能有关键字参数,例如:

@info "hello, world" time = time()

记录器将会发送关键字参数 time => time()(详细信息请查看 Logging 基本用法教程)。基于此信息,我们现在将为我们的记录器生成一个日志消息,把它打印到记录器 I/O 流。当然,一般来说此方法不必写入常规流,它可以把日志事件发送为一个 HTTP 请求(和 LokiLogger.jl 类似),或者作为短信发送到你的手机。我们 CipherLoggerhandle_message 函数是非常简单的:我们把消息加密,然后输出级别和加密后的消息:

function Logging.handle_message(logger::CipherLogger,
                                lvl, msg, _mod, group, id, file, line;
                                kwargs...)
    # Apply Ceasar cipher on the log message
    msg = caesar(msg, logger.key)
    # Write the formatted log message to logger.io
    println(logger.io, "[", lvl, "] ", msg)
end

现在唯一缺失的部分是 caesar 函数,它应该用记录器的密钥加密消息。这里的加密只应用到 ASCII 字母 A-Za-z 上:

function caesar(msg::String, key::Int)
    io = IOBuffer()
    for c in msg
        shift = ('a' <= c <= 'z') ? Int('a') : ('A' <= c <= 'Z') ? Int('A') : 0
        if shift > 0
            c = Char((Int(c) - shift + key) % 26 + shift)
        end
        print(io, c)
    end
    return String(take!(io))
end

就是这样 – 我们实现了一个新的记录器!让我们去兜兜风吧。如果你一直跟着学习到了这儿,并且从开始就复制粘贴代码片段到 Julia REPL 中执行,你应该看到和下面相同的输出:

julia> using Logging

julia> cipher_logger = CipherLogger(3); # new logger with 3 as the key

julia> global_logger(cipher_logger); # set the logger as the global logger

julia> @info "Hello, world!"
[Info] Khoor, zruog!

julia> @info "This is an info message."
[Info] Wklv lv dq lqir phvvdjh.

julia> @warn "This is a warning."
[Warn] Wklv lv d zduqlqj.

julia> @error "This is an error message."
[Error] Wklv lv dq huuru phvvdjh.

我们也可以确保我们的记录器接收比 Logging.Info 级别低的日志事件(默认的记录器是不支持的):

julia> @debug "Is this visible?"
[Debug] Lv wklv ylvleoh?

最后,我们来确保记录器也能捕获日志事件异常。例如,这里我们尝试创建一个带有未定义变量 name 的消息字符串:

julia> @info "hello, $name"
[Error] Hafhswlrq zkloh jhqhudwlqj orj uhfrug lq prgxoh Pdlq dw UHSO[17]:1

你能破解密文并且理解其意思吗?

使用已存在的功能构建 CipherLogger

如本页开头所示,除非你想要对接一个新类型的记录器接收器(logger sink),通常无需实现自己的记录器。相反,最好是组合现有的日志记录器以实现路由、转换、和过滤日志事件。上面的 CipherLogger 可以像下面这样使用来自 LoggingExtras.jl 包的 TransformerLogger 轻而易举地实现:

using Logging, LoggingExtras

encryption_logger = TransformerLogger(SimpleLogger(stderr)) do args
    message = caesar(args.message, 3)
    return (; args..., message=message)
end

global_logger(encryption_logger)

结果看起来像下面这样:

julia> @info "hello, world"
┌ Info: khoor, zruog
└ @ Main REPL[5]:1

使用已存在的 TransformerLogger 的另一个好处是它构造的很好。因此,我们可以用另一层来解密消息:

decryption_logger = TransformerLogger(encryption_logger) do args
    message = caesar(args.message, -3) # to decrypt just negate the key
    return (; args..., message=message)
end

global_logger(decryption_logger)

现在消息再打印到终端之前被加密解密,因此这种情况下,两个记录器只是相互撤销对方的行为。这是个非常没用的例子,但是可组合性对其他一些事情非常有用!

julia> @info "hello, world"
┌ Info: hello, world
└ @ Main REPL[8]:1