日志记录

Logging 模块提供了一种方法来记录计算的历史和进度,作为事件日志。事件是通过在源代码中插入日志记录语句创建的,例如

@warn "Abandon printf debugging, all ye who enter here!"
┌ Warning: Abandon printf debugging, all ye who enter here!
└ @ Main REPL[1]:1

该系统提供了比在源代码中散布 println() 调用更多的优势。首先,它允许您控制消息的可见性和呈现方式,而无需编辑源代码。例如,与上面的 @warn 相比

@debug "The sum of some values $(sum(rand(100)))"

默认情况下不会产生任何输出。此外,将这样的调试语句保留在源代码中非常便宜,因为如果稍后会被忽略,该系统会避免评估消息。在这种情况下,除非启用了调试日志记录,否则 sum(rand(100)) 和相关的字符串处理永远不会执行。

其次,日志记录工具允许您将任意数据作为一组键值对附加到每个事件。这允许您捕获局部变量和其他程序状态以供以后分析。例如,要将局部数组变量 A 和向量 v 的总和作为键 s 附加,您可以使用

A = ones(Int, 4, 4)
v = ones(100)
@info "Some variables"  A  s=sum(v)

# output
┌ Info: Some variables
│   A =
│    4×4 Matrix{Int64}:
│     1  1  1  1
│     1  1  1  1
│     1  1  1  1
│     1  1  1  1
└   s = 100.0

所有日志记录宏 @debug@info@warn@error 共享一些共同的功能,这些功能在更通用宏 @logmsg 的文档中进行了详细描述。

日志事件结构

每个事件都会生成几部分数据,一些由用户提供,一些自动提取。让我们先检查一下用户定义的数据

  • 日志级别是消息的一个广泛类别,用于早期过滤。有几个标准级别,类型为 LogLevel;用户定义的级别也是可能的。每个级别的用途都不同

    • Logging.Debug(日志级别 -1000)是供程序开发者使用的信息。这些事件默认情况下是禁用的。
    • Logging.Info(日志级别 0)用于向用户提供一般信息。可以将其视为直接使用 println 的替代方案。
    • Logging.Warn(日志级别 1000)表示某些内容出错,可能需要采取措施,但目前程序仍在工作。
    • Logging.Error(日志级别 2000)表示某些内容出错,并且不太可能恢复,至少对于代码的这一部分而言是这样。通常,此日志级别是不必要的,因为抛出异常可以传达所有必要的信息。
  • 消息是一个描述事件的对象。按照惯例,作为消息传递的 AbstractString 被认为是 markdown 格式。其他类型将使用 print(io, obj)string(obj) 用于基于文本的输出,并可能使用 show(io,mime,obj) 用于安装的日志记录器中使用的其他多媒体显示。

  • 可选的键值对允许将任意数据附加到每个事件。一些键具有约定意义,可以影响事件的解释方式(请参阅 @logmsg)。

系统还会为每个事件生成一些标准信息

  • 扩展日志记录宏的模块
  • 源代码中日志记录宏所在的文件
  • 一个消息id,它是日志记录宏出现的源代码语句的唯一固定标识符。即使文件源代码发生更改,只要日志记录语句本身保持不变,此标识符也旨在保持相当稳定。
  • 事件的,默认情况下设置为文件的基名,不带扩展名。这可以用于将消息分组到比日志级别更细的类别中(例如,所有弃用警告的组都是 :depwarn),或跨模块或模块内进行逻辑分组。

请注意,一些有用的信息(例如事件时间)默认情况下不包含在内。这是因为提取此类信息可能代价高昂,并且当前日志记录器也可以动态访问此类信息。定义一个 自定义日志记录器 以根据需要使用时间、回溯、全局变量的值和其他有用信息来扩充事件数据非常简单。

处理日志事件

如您在示例中所见,日志记录语句没有提及日志事件的去向或如何处理。这是系统可组合且适合并发使用的关键设计特性。它通过分离两个不同的关注点来实现这一点

  • 创建日志事件是模块作者的关注点,他们需要决定在何处触发事件以及包含哪些信息。
  • 处理日志事件——即显示、过滤、聚合和记录——是应用程序作者的关注点,他们需要将多个模块组合成一个协作应用程序。

日志记录器

事件的处理由日志记录器执行,日志记录器是第一个看到事件的用户可配置代码。所有日志记录器都必须是 AbstractLogger 的子类型。

当触发事件时,会通过查找具有全局日志记录器作为回退的任务本地日志记录器来找到相应的日志记录器。这里的想法是,应用程序代码知道如何处理日志事件,并且存在于调用堆栈的顶部某个位置。因此,我们应该向上查找调用堆栈以发现日志记录器——也就是说,日志记录器应该是动态作用域的。(这与日志记录框架形成对比,在日志记录框架中,日志记录器是词法作用域的;由模块作者显式提供或作为简单的全局变量。在这样的系统中,在组合来自多个模块的功能时,控制日志记录很麻烦。)

可以使用 global_logger 设置全局日志记录器,并使用 with_logger 控制任务本地日志记录器。新生成的子任务会继承父任务的日志记录器。

库提供了三种日志记录器类型。 ConsoleLogger 是您在启动 REPL 时看到的默认日志记录器。它以可读的文本格式显示事件,并尝试提供对格式和过滤的简单但用户友好的控制。 NullLogger 是一种方便的方法,用于在必要时丢弃所有消息;它是 devnull 流的日志记录等效项。 SimpleLogger 是一个非常简单的文本格式化日志记录器,主要用于调试日志记录系统本身。

自定义日志记录器应附带对 参考部分 中描述的函数的重载。

早期过滤和消息处理

当事件发生时,会进行一些早期过滤步骤,以避免生成会被丢弃的消息。

  1. 消息日志级别会与全局最小级别进行检查(通过 disable_logging 设置)。这是一个粗略但极其廉价的全局设置。
  2. 查找当前的日志记录器状态,并将消息级别与日志记录器的缓存最小级别进行检查,该级别可以通过调用 Logging.min_enabled_level 找到。此行为可以通过环境变量覆盖(稍后详细介绍)。
  3. 调用 Logging.shouldlog 函数,该函数使用当前日志记录器,并传入一些最少的信息(级别、模块、组、ID),这些信息可以静态计算。最有用的是,shouldlog 传递了一个事件 id,可用于基于缓存的谓词尽早丢弃事件。

如果所有这些检查都通过,则完整评估消息和键值对,并通过 Logging.handle_message 函数传递给当前日志记录器。handle_message() 可以根据需要执行额外的过滤,并将事件显示到屏幕上、保存到文件等。

在生成日志事件期间发生的异常默认情况下会被捕获并记录。这可以防止单个损坏的事件导致应用程序崩溃,这在生产系统中启用很少使用的调试事件时非常有用。此行为可以通过扩展 Logging.catch_exceptions 为每个日志记录器类型自定义。

测试日志事件

日志事件是运行正常代码的副作用,但您可能会发现自己希望测试特定的信息消息和警告。Test 模块提供了一个 @test_logs 宏,可用于对日志事件流进行模式匹配。

环境变量

消息过滤可以通过 JULIA_DEBUG 环境变量进行影响,并作为一种启用文件或模块调试日志记录的简单方法。使用 JULIA_DEBUG=loading 加载 julia 将激活 loading.jl 中的 @debug 日志消息。例如,在 Linux shell 中

$ JULIA_DEBUG=loading julia -e 'using OhMyREPL'
┌ Debug: Rejecting cache file /home/user/.julia/compiled/v0.7/OhMyREPL.ji due to it containing an invalid cache header
└ @ Base loading.jl:1328
[ Info: Recompiling stale cache file /home/user/.julia/compiled/v0.7/OhMyREPL.ji for module OhMyREPL
┌ Debug: Rejecting cache file /home/user/.julia/compiled/v0.7/Tokenize.ji due to it containing an invalid cache header
└ @ Base loading.jl:1328
...

在 Windows 上,可以通过在 CMD 中首先运行 set JULIA_DEBUG="loading" 以及在 Powershell 中运行 $env:JULIA_DEBUG="loading" 来实现相同的效果。

类似地,环境变量可用于启用模块(例如 Pkg)或模块根(请参阅 Base.moduleroot)的调试日志记录。要启用所有调试日志记录,请使用特殊值 all

要在 REPL 中打开调试日志记录,请将 ENV["JULIA_DEBUG"] 设置为感兴趣的模块的名称。在 REPL 中定义的函数属于模块 Main;可以像这样启用它们的日志记录

julia> foo() = @debug "foo"
foo (generic function with 1 method)

julia> foo()

julia> ENV["JULIA_DEBUG"] = Main
Main

julia> foo()
┌ Debug: foo
└ @ Main REPL[1]:1

使用逗号分隔符为多个模块启用调试:JULIA_DEBUG=loading,Main

示例

示例:将日志事件写入文件

有时将日志事件写入文件可能很有用。以下是如何使用任务本地和全局日志记录器将信息写入文本文件的示例。

# Load the logging module
julia> using Logging

# Open a textfile for writing
julia> io = open("log.txt", "w+")
IOStream(<file log.txt>)

# Create a simple logger
julia> logger = SimpleLogger(io)
SimpleLogger(IOStream(<file log.txt>), Info, Dict{Any,Int64}())

# Log a task-specific message
julia> with_logger(logger) do
           @info("a context specific log message")
       end

# Write all buffered messages to the file
julia> flush(io)

# Set the global logger to logger
julia> global_logger(logger)
SimpleLogger(IOStream(<file log.txt>), Info, Dict{Any,Int64}())

# This message will now also be written to the file
julia> @info("a global log message")

# Close the file
julia> close(io)

示例:启用调试级别消息

以下是如何创建一个 ConsoleLogger 的示例,该日志记录器允许通过任何日志级别高于或等于 Logging.Debug 的消息。

julia> using Logging

# Create a ConsoleLogger that prints any log messages with level >= Debug to stderr
julia> debuglogger = ConsoleLogger(stderr, Logging.Debug)

# Enable debuglogger for a task
julia> with_logger(debuglogger) do
           @debug "a context specific log message"
       end

# Set the global logger
julia> global_logger(debuglogger)

参考

Logging 模块

Logging.Logging模块

用于捕获、过滤和呈现日志事件流的实用程序。通常,您不需要导入 Logging 来创建日志事件;为此,标准日志宏(如 @info)已由 Base 导出,默认情况下可用。

创建事件

Logging.@logmsg
@debug message  [key=value | value ...]
@info  message  [key=value | value ...]
@warn  message  [key=value | value ...]
@error message  [key=value | value ...]

@logmsg level message [key=value | value ...]

使用信息性 message 创建日志记录。为了方便起见,定义了四个日志宏 @debug@info@warn@error,它们分别在标准严重性级别 DebugInfoWarnError 下记录日志。@logmsg 允许以编程方式将 level 设置为任何 LogLevel 或自定义日志级别类型。

message 应为一个表达式,该表达式计算结果为一个字符串,该字符串是对日志事件的人类可读描述。按照惯例,此字符串在呈现时将格式化为 markdown。

可选的 key=value 对列表支持任意用户定义的元数据,这些元数据将作为日志记录的一部分传递到日志记录后端。如果仅提供 value 表达式,则将使用 Symbol 生成表示该表达式的键。例如,x 变成 x=xfoo(10) 变成 Symbol("foo(10)")=foo(10)。要展开键值对列表,请使用正常的展开语法 @info "blah" kws...

有一些键允许覆盖自动生成的日志数据。

  • _module=mod 可用于指定与消息源位置不同的源模块。
  • _group=symbol 可用于覆盖消息组(这通常从源文件的基名称派生)。
  • _id=symbol 可用于覆盖自动生成的唯一消息标识符。如果您需要非常密切地关联在不同源行上生成的消息,这将很有用。
  • _file=string_line=integer 可用于覆盖日志消息的明显源位置。

还有一些键值对具有约定意义。

  • maxlog=integer 应作为对后端的提示使用,指示消息最多应显示 maxlog 次。
  • exception=ex 应用于传输带有日志消息的异常,通常与 @error 一起使用。可以使用元组 exception=(ex,bt) 附加关联的回溯 bt

示例

@debug "Verbose debugging information.  Invisible by default"
@info  "An informational message"
@warn  "Something was odd.  You should pay attention"
@error "A non fatal error occurred"

x = 10
@info "Some variables attached to the message" x a=42.0

@debug begin
    sA = sum(A)
    "sum(A) = $sA is an expensive operation, evaluated only when `shouldlog` returns true"
end

for i=1:10000
    @info "With the default backend, you will only see (i = $i) ten times"  maxlog=10
    @debug "Algorithm1" i progress=i/10000
end
来源
Logging.LogLevel类型
LogLevel(level)

日志记录的严重性/详细程度。

日志级别提供了一个键,可以根据该键过滤潜在的日志记录,然后再执行任何其他工作来构造日志记录数据结构本身。

示例

julia> Logging.LogLevel(0) == Logging.Info
true
来源

使用 AbstractLogger 处理事件

事件处理由覆盖与 AbstractLogger 关联的函数控制。

要实现的方法简要描述
Logging.handle_message处理日志事件
Logging.shouldlog事件的早期过滤
Logging.min_enabled_level接受事件的日志级别的下限
可选方法默认定义简要描述
Logging.catch_exceptionstrue捕获事件评估期间的异常
Logging.AbstractLogger类型

日志记录器控制如何过滤和调度日志记录。当生成日志记录时,日志记录器是第一个用户可配置的代码片段,它可以检查记录并决定如何处理它。

来源
Logging.handle_message函数
handle_message(logger, level, message, _module, group, id, file, line; key1=val1, ...)

将消息记录到 logger 中的 level 级别。消息生成的逻辑位置由模块 _modulegroup 给出;源位置由 fileline 给出。id 是一个任意唯一值(通常是 Symbol),用作在过滤时识别日志语句的键。

来源
Logging.shouldlog函数
shouldlog(logger, level, _module, group, id)

logger 接受在 level 级别生成的消息时,返回 true,该消息为 _modulegroup 生成,并具有唯一的日志标识符 id

来源
Logging.min_enabled_level函数
min_enabled_level(logger)

返回 logger 用于早期过滤的最低启用级别。也就是说,所有消息都被过滤掉的日志级别低于或等于该级别。

来源
Logging.catch_exceptions函数
catch_exceptions(logger)

如果日志记录器应该捕获日志记录构造期间发生的异常,则返回 true。默认情况下,消息会被捕获。

默认情况下,所有异常都会被捕获以防止日志消息生成导致程序崩溃。这允许用户在生产系统中自信地切换很少使用的功能(例如调试日志记录)。

如果要将日志记录用作审计跟踪,则应为您的日志记录器类型禁用此功能。

来源
Logging.disable_logging函数
disable_logging(level)

禁用所有日志级别等于或小于 level 的日志消息。这是一个全局设置,旨在在禁用时使调试日志记录极其廉价。

示例

Logging.disable_logging(Logging.Info) # Disable debug and info
来源

使用日志记录器

日志记录器安装和检查

Logging.global_logger函数
global_logger()

返回全局日志记录器,当当前任务不存在特定日志记录器时,用于接收消息。

global_logger(logger)

将全局日志记录器设置为logger,并返回之前的全局日志记录器。

来源
Logging.with_logger函数
with_logger(function, logger)

执行function,并将所有日志消息定向到logger

示例

function test(x)
    @info "x = $x"
end

with_logger(logger) do
    test(1)
    test([1,2])
end
来源
Logging.current_logger函数
current_logger()

返回当前任务的日志记录器,如果任务未附加任何日志记录器,则返回全局日志记录器。

来源

系统提供的日志记录器

Logging.NullLogger类型
NullLogger()

禁用所有消息且不产生任何输出的日志记录器 - 等同于/dev/null的日志记录器。

来源
Logging.ConsoleLogger类型
ConsoleLogger([stream,] min_level=Info; meta_formatter=default_metafmt,
              show_limited=true, right_justify=0)

格式针对文本控制台的可读性进行了优化的日志记录器,例如与Julia REPL交互式工作。

小于min_level的日志级别会被过滤掉。

消息格式可以通过设置关键字参数来控制

  • meta_formatter是一个函数,它接收日志事件元数据(level, _module, group, id, file, line)并返回日志消息的颜色(如传递给printstyled一样)、前缀和后缀。默认情况下,以日志级别作为前缀,并以包含模块、文件和行位置的后缀结尾。
  • show_limited通过在格式化期间设置:limit IOContext键来限制大型数据结构的打印内容,使其适合屏幕显示。
  • right_justify是日志元数据右对齐的整数列。默认值为零(元数据位于其自己的行上)。
Logging.SimpleLogger类型
SimpleLogger([stream,] min_level=Info)

一个简单的日志记录器,用于将所有级别大于或等于min_level的消息记录到stream。如果stream已关闭,则级别大于或等于Warn的消息将记录到stderr,低于Warn的消息将记录到stdout

来源