3  日志记录

3 日志记录

Erlang/OTP 21.0 提供了一个标准的 API,用于通过 Logger 进行日志记录,它是 Kernel 应用程序的一部分。Logger 包含用于发出日志事件的 API,以及一个可定制的后端,其中可以插入日志处理程序、过滤器和格式化程序。

默认情况下,Kernel 应用程序在系统启动时安装一个日志处理程序。此处理程序名为 default。它接收并处理由 Erlang 运行时系统、标准行为和不同的 Erlang/OTP 应用程序产生的标准日志事件。默认情况下,日志事件被写入终端。

您还可以配置系统,使默认处理程序将日志事件打印到单个文件,或通过 disk_log 打印到一组包装日志。

通过配置,您还可以修改或禁用默认处理程序,用自定义处理程序替换它,并安装额外的处理程序。

注意

由于 Logger 是 Erlang/OTP 21.0 中的新功能,因此我们保留在发布此版本后的补丁中对 Logger API 和功能进行更改的权利。这些更改可能与初始版本向后兼容,也可能不兼容。

一个**日志事件**包含一个**日志级别**、要记录的**消息**和**元数据**。

Logger 后端从 API 转发日志事件,首先通过一组**主过滤器**,然后通过附加到每个日志处理程序的一组次级过滤器。次级过滤器称为**处理程序过滤器**。

每个过滤器集包含一个**日志级别检查**,后面跟着零个或多个**过滤器函数**。

下图显示了 Logger 的概念概述。该图显示了两个日志处理程序,但可以安装任意数量的处理程序。

IMAGE MISSING

图 3.1:  概念概述

日志级别以原子表示。在 Logger 内部,原子映射到整数值,并且如果日志事件的日志级别的整数值小于或等于当前配置的日志级别,则日志事件通过日志级别检查。也就是说,如果事件的严重程度等于或高于配置的级别,则检查通过。有关所有日志级别的列表和说明,请参见 日志级别 部分。

主日志级别可以通过每个模块配置的日志级别覆盖。例如,这允许从系统的特定部分进行更详细的日志记录。

过滤器函数可用于比日志级别检查提供的更复杂的过滤。过滤器函数可以根据事件的任何内容来停止或传递日志事件。它还可以修改日志事件的所有部分。有关更多详细信息,请参见 过滤器 部分。

如果日志事件通过所有主过滤器和特定处理程序的所有处理程序过滤器,Logger 将事件转发到**处理程序回调**。处理程序将事件格式化并打印到其目的地。有关更多详细信息,请参见 处理程序 部分。

从调用处理程序回调之前的所有操作,都在客户端进程中执行,即发出日志事件的进程。是否涉及其他进程取决于处理程序的实现。

处理程序按顺序调用,顺序未定义。

用于日志记录的 API 包含一组 ,以及一组形式为 logger:Level/1,2,3 的函数,它们都是 logger:log(Level,Arg1[,Arg2[,Arg3]]) 的快捷方式。

这些宏在 logger.hrl 中定义,该文件包含在使用以下指令的模块中

-include_lib("kernel/include/logger.hrl").

使用宏与使用导出函数之间的区别在于,宏将位置(发起者)信息添加到元数据中,并通过将 logger 调用包装在 case 语句中来执行延迟评估,因此只有当事件的日志级别通过主日志级别检查时才会对其进行评估。

日志级别表示事件的严重程度。根据 Syslog 协议 RFC 5424,可以指定八个日志级别。下表按名称(原子)、整数值和描述列出了所有可能的日志级别

级别 整数 描述
emergency 0 系统不可用
alert 1 必须立即采取措施
critical 2 临界条件
error 3 错误条件
warning 4 警告条件
notice 5 正常但重要的条件
info 6 信息性消息
debug 7 调试级别消息

表 3.1:  日志级别

请注意,整数值仅在 Logger 内部使用。在 API 中,您必须始终使用原子。要比较两个日志级别的严重程度,请使用 logger:compare_levels/2

日志消息包含要记录的信息。消息可以包含格式字符串和参数(在 Logger API 中作为两个单独的参数给出)、字符串或报告。

示例,格式字符串和参数

logger:error("The file does not exist: ~ts",[Filename])

示例,字符串

logger:notice("Something strange happened!")

报告(是映射或键值对列表)是使用 Logger 进行日志记录的首选方法,因为它使不同的后端能够根据需要过滤和格式化日志事件。

示例,报告

?LOG_ERROR(#{ user => joe, filename => Filename, reason => enoent })

报告可以附带一个**报告回调**,该回调在日志事件的 元数据 中指定。报告回调是一个方便函数,格式化程序 可以使用它将报告转换为格式字符串和参数,或直接转换为字符串。如果未提供回调,或者需要自定义格式,格式化程序也可以使用自己的转换函数。

报告回调必须是具有一个或两个参数的 fun。如果它带有一个参数,则该参数是报告本身,并且 fun 返回格式字符串和参数

fun((logger:report()) -> {io:format(),[term()]})

如果它带两个参数,则第一个是报告,第二个是包含额外数据的映射,这些数据允许直接转换为字符串

fun 必须遵守第二个参数中提供的 depthchars_limit 参数,因为格式化程序无法对返回的字符串中的这些参数进行任何有用的操作。额外数据还包含一个名为 single_line 的字段,指示打印的日志消息是否可以包含换行符。当报告的格式取决于大小或单行参数时,将使用此变体。

示例,报告,以及带报告回调的元数据

logger:debug(#{got => connection_request, id => Id, state => State},
             #{report_cb => fun(R) -> {"~p",[R]} end})

日志消息也可以通过 fun 提供以进行延迟评估。只有当主日志级别检查通过时才会评估 fun,因此如果生成消息很昂贵,则建议使用它。延迟 fun 必须返回字符串、报告或包含格式字符串和参数的元组。

元数据包含与日志消息关联的附加数据。Logger 默认情况下插入一些元数据字段,客户端可以通过三种不同的方式添加自定义元数据

主元数据应用于所有日志事件的基元数据。在启动时,可以使用内核配置参数 logger_metadata 设置它。在运行时,可以使用 logger:set_primary_config/1 logger:update_primary_config/1 分别设置和更新它。

进程元数据分别使用 logger:set_process_metadata/1 logger:update_process_metadata/1 设置和更新。此元数据适用于执行这些调用的进程,Logger 将元数据添加到该进程发出的所有日志事件中。

与一个特定日志事件关联的元数据在发出事件时作为日志宏或 Logger API 函数的最后一个参数给出。例如

?LOG_ERROR("Connection closed",#{context => server})

有关 Logger 插入哪些默认键以及如何合并不同的元数据映射的信息,请参见 logger:metadata() 类型的说明。

过滤器可以是主要的,也可以附加到特定的处理程序。Logger 首先调用主过滤器,如果它们都通过,则调用每个处理程序的处理程序过滤器。只有当附加到相关处理程序的所有过滤器也都通过时,Logger 才会调用处理程序回调。

过滤器定义为

{FilterFun, Extra}

其中 FilterFun 是一个参数为 2 的函数,Extra 是任何项。应用过滤器时,Logger 会使用日志事件作为第一个参数,Extra 的值作为第二个参数来调用该函数。有关类型定义,请参见 logger:filter()

过滤器函数可以返回 stopignore 或(可能已修改的)日志事件。

如果返回 stop,则日志事件立即被丢弃。如果过滤器是主要的,则不会调用任何处理程序过滤器或回调。如果它是处理程序过滤器,则不会调用相应的处理程序回调,但日志事件将被转发到附加到下一个处理程序的过滤器(如果有)。

如果返回了日志事件,则使用返回的值作为第一个参数调用下一个过滤器函数。也就是说,如果过滤器函数修改了日志事件,则下一个过滤器函数将接收修改后的事件。从最后一个过滤器函数返回的值是处理程序回调接收的值。

如果过滤器函数返回 ignore,则表示它不识别日志事件,因此由其他过滤器决定事件的命运。

配置选项 filter_default 指定了所有过滤器函数都返回 ignore 或者没有过滤器时的情况下的行为。filter_default 默认情况下设置为 log,这意味着如果所有现有的过滤器都忽略了一个日志事件,Logger 会将事件转发到处理程序回调。如果 filter_default 设置为 stop,Logger 会丢弃此类事件。

主过滤器使用 logger:add_primary_filter/2 添加,使用 logger:remove_primary_filter/1 删除。它们也可以在系统启动时通过内核配置参数 logger 添加。

处理程序过滤器使用 logger:add_handler_filter/3 添加,使用 logger:remove_handler_filter/2 删除。它们也可以在使用 logger:add_handler/3 添加处理程序时在配置中直接指定,或者通过内核配置参数 logger 指定。

要查看当前系统中安装了哪些过滤器,请使用 logger:get_config/0,或者 logger:get_primary_config/0 logger:get_handler_config/1。过滤器按应用顺序排列,也就是说,列表中的第一个过滤器最先应用,依此类推。

为了方便,存在以下内置过滤器

提供了一种基于 Metadata 中的 domain 字段过滤日志事件的方法。

提供了一种基于日志级别过滤日志事件的方法。

停止或允许来自 supervisorapplication_controller 的进度报告。

停止或允许来自其组领导者位于远程节点上的进程的日志事件。

处理程序被定义为一个模块,至少导出以下回调函数

当一个日志事件通过所有主过滤器以及附加到该处理程序的所有处理程序过滤器时,就会调用此函数。函数调用在客户端进程上执行,处理程序实现是否涉及其他进程由处理程序实现决定。

Logger 允许添加处理程序回调的多个实例。也就是说,如果回调模块实现允许,可以使用同一个回调模块添加多个处理程序实例。不同的实例通过唯一的处理程序标识进行区分。

除了强制的回调函数 log/2 之外,处理程序模块还可以导出可选的回调函数 adding_handler/1changing_config/3filter_config/1removing_handler/1。有关这些函数的更多信息,请参见 logger(3) 手册页中的 处理程序回调函数 部分。

存在以下内置处理程序

这是 OTP 使用的默认处理程序。可以启动多个实例,每个实例都会将日志事件写入指定的目标,终端或文件。

此处理程序的行为与 logger_std_h 非常相似,不同之处在于它使用 disk_log 作为其目标。

此处理程序仅用于向后兼容。它默认情况下不会启动,但会在第一次使用 error_logger:add_report_handler/1,2 添加 error_logger 事件处理程序时自动启动。

STDLIB 和 SASL 中的旧 error_logger 事件处理程序仍然存在,但它们不会被 Erlang/OTP 21.0 或更高版本添加。

处理程序实现可以使用格式化程序对日志事件进行最终格式化,然后打印到处理程序的目标。处理程序回调将格式化程序信息作为处理程序配置的一部分接收,该信息作为第二个参数传递给 HModule:log/2

格式化程序信息由格式化程序模块 FModule 及其配置 FConfig 组成。FModule 必须导出以下函数,该函数可以由处理程序调用

处理程序的格式化程序信息在添加处理程序时作为其配置的一部分设置。它也可以在运行时使用 logger:set_handler_config(HandlerId,formatter,{FModule,FConfig}) 更改,该函数会覆盖当前格式化程序信息,或者使用 logger:update_formatter_config/2,3 更改,该函数只会修改格式化程序配置。

如果格式化程序模块导出可选的回调函数 check_config(FConfig),Logger 会在设置或修改格式化程序信息时调用此函数,以验证格式化程序配置的有效性。

如果没有为处理程序指定格式化程序信息,Logger 会使用 logger_formatter 作为默认值。有关此模块的更多信息,请参见 logger_formatter(3) 手册页。

在系统启动时,Logger 通过内核配置参数进行配置。适用于 Logger 的参数在 内核配置参数 部分进行了描述。示例可以在 配置示例 部分找到。

在运行时,Logger 配置通过 API 函数更改。请参见 logger(3) 手册页中的 配置 API 函数 部分。

适用于主 Logger 配置的 Logger API 函数是

主 Logger 配置是一个包含以下键的映射

指定主日志级别,也就是说,严重程度等于或高于此级别的日志事件会被转发到主过滤器。严重程度较低的日志事件会立即被丢弃。

有关可能的日志级别的列表和描述,请参见 日志级别 部分。

此选项的初始值由内核配置参数 logger_level 设置。它可以在运行时使用 logger:set_primary_config(level,Level) 更改。

默认值为 notice

指定主过滤器。

此选项的初始值由内核配置参数 logger 设置。在运行时,主过滤器分别使用 logger:add_primary_filter/2 logger:remove_primary_filter/1 添加和删除。

有关更多详细信息,请参见 过滤器 部分。

默认值为 []

指定了所有过滤器都返回 ignore 或者没有过滤器时,日志事件的处理方式。

有关此选项的使用方式的更多信息,请参见 过滤器 部分。

默认值为 log

用于所有日志调用的主元数据。

有关此选项的使用方式的更多信息,请参见 元数据 部分。

默认值为 #{}

适用于处理程序配置的 Logger API 函数是

处理程序的配置是一个包含以下键的映射

由 Logger 自动插入。该值与添加处理程序时指定的 HandlerId 相同,并且无法更改。

由 Logger 自动插入。该值与添加处理程序时指定的 Module 相同,并且无法更改。

指定处理程序的日志级别,也就是说,严重程度等于或高于此级别的日志事件会被转发到此处理程序的处理程序过滤器。

有关可能的日志级别的列表和描述,请参见 日志级别 部分。

日志级别是在添加处理程序时指定的,或者在运行时使用例如 logger:set_handler_config(HandlerId,level,Level) 更改的。

默认值为 all

指定处理程序过滤器。

处理程序过滤器是在添加处理程序时指定的,或者在运行时分别使用 logger:add_handler_filter/3 logger:remove_handler_filter/2 添加和删除的。

有关更多详细信息,请参见 过滤器 部分。

默认值为 []

指定了所有过滤器都返回 ignore 或者没有过滤器时,日志事件的处理方式。

有关此选项的使用方式的更多信息,请参见 过滤器 部分。

默认值为 log

指定一个格式化程序,处理程序可以使用它将日志事件项转换为可打印的字符串。

格式化程序信息是在添加处理程序时指定的。格式化程序配置可以在运行时使用 logger:update_formatter_config/2,3 更改,或者可以使用例如 logger:set_handler_config/3 覆盖完整的格式化程序信息。

有关更多详细信息,请参见 格式化程序 部分。

默认情况下为 {logger_formatter,DefaultFormatterConfig}。有关此格式化程序及其默认配置的信息,请参见 logger_formatter(3) 手册页。

特定于处理程序的配置,即与特定处理程序实现相关的配置数据。

内置处理程序的配置在 logger_std_h(3)logger_disk_log_h(3) 手册页中描述。

请注意,levelfilters 由 Logger 本身在将日志事件转发到每个处理程序之前遵守,而 formatter 和所有特定于处理程序的选项则留给处理程序实现。

以下内核配置参数适用于 Logger

指定 Logger 的配置,但不包括主要日志级别(由 logger_level 指定)以及与 SASL 错误日志记录 的兼容性(由 logger_sasl_compatible 指定)。

使用此参数,您可以修改或禁用默认处理程序,添加自定义处理程序和主要日志记录器过滤器,设置每个模块的日志级别,以及修改 代理 配置。

Config 是以下任何(零个或多个)项:

禁用默认处理程序。这允许另一个应用程序添加自己的默认处理程序。

只允许出现一个此类条目。

如果 HandlerIddefault,则此条目修改默认处理程序,等效于调用

然后调用

对于 HandlerId 的所有其他值,此条目添加一个新的处理程序,等效于调用

允许出现多个此类条目。

添加指定的首要过滤器。

  • FilterDefault = log | stop
  • Filter = {FilterId, {FilterFun, FilterConfig}}

等效于调用

对于每个 Filter

FilterDefault 指定如果所有首要过滤器都返回 ignore 的行为,请参见部分 过滤器

只允许出现一个此类条目。

为给定模块设置模块日志级别。等效于调用

对于每个 Module

允许出现多个此类条目。

设置代理配置,等效于调用

只允许出现一个此类条目。

有关使用 logger 参数进行系统配置的示例,请参见部分 配置示例

指定首要元数据。有关此参数的更多信息,请参见 kernel(6) 手册页。

指定首要日志级别。有关此参数的更多信息,请参见 kernel(6) 手册页。

指定 Logger 与 SASL 错误日志记录 的兼容性。有关此参数的更多信息,请参见 kernel(6) 手册页。

内核配置参数 logger 的值是一个元组列表。在启动 Erlang 节点时,可以在命令行上写入该项,但随着该项的增长,更好的方法是使用系统配置文件。有关此文件的更多信息,请参见 config(4) 手册页。

以下每个示例都显示了一个简单的系统配置文件,该文件根据描述配置 Logger。

修改默认处理程序,使其打印到文件而不是 standard_io

[{kernel,
  [{logger,
    [{handler, default, logger_std_h,  % {handler, HandlerId, Module,
      #{config => #{file => "log/erlang.log"}}}  % Config}
    ]}]}].

修改默认处理程序,使其将每个日志事件打印为单行

[{kernel,
  [{logger,
    [{handler, default, logger_std_h,
      #{formatter => {logger_formatter, #{single_line => true}}}}
    ]}]}].

修改默认处理程序,使其为每个日志事件打印日志记录进程的 pid

[{kernel,
  [{logger,
    [{handler, default, logger_std_h,
      #{formatter => {logger_formatter,
                        #{template => [time," ",pid," ",msg,"\n"]}}}}
    ]}]}].

修改默认处理程序,使其仅将错误和更严重的日志事件打印到 “log/erlang.log”,并添加另一个处理程序,使其将所有日志事件打印到 “log/debug.log”。

[{kernel,
  [{logger,
    [{handler, default, logger_std_h,
      #{level => error,
        config => #{file => "log/erlang.log"}}},
     {handler, info, logger_std_h,
      #{level => debug,
        config => #{file => "log/debug.log"}}}
    ]}]}].

Logger 通过以下方式提供与 error_logger 的向后兼容性

error_logger API 仍然存在,但应仅由旧代码使用。它将在以后的版本中删除。

error_logger:error_report/1,2 error_logger:error_msg/1,2 以及相应的警告和信息消息函数的调用,都将转发到 Logger,作为对 logger:log(Level,Report,Metadata) 的调用。

Level = error | warning | info 来自函数名。 Report 包含实际的日志消息,而 Metadata 包含其他信息,这些信息可用于为旧版 error_logger 事件处理程序创建向后兼容的事件,请参见部分 旧版事件处理程序

要获取与 error_logger_tty_herror_logger_file_h 生成的日志事件相同的格式,请使用默认格式化程序 logger_formatter,并将配置参数 legacy_header 设置为 true。这是由 Kernel 启动的 default 处理程序的默认配置。

默认情况下,所有源自 OTP 内部(除了以前所谓的 “SASL 报告”)的日志事件看起来都与以前相同。

SASL 报告是指主管报告、崩溃报告和进度报告。

在 Erlang/OTP 21.0 之前,这些报告仅在 SASL 应用程序运行时才被记录,并且它们通过 SASL 自身的事件处理程序 sasl_report_tty_hsasl_report_file_h 打印。

这些日志事件的目标由 SASL 配置参数 配置。

由于特定的事件处理程序,输出格式略有不同于其他日志事件。

从 Erlang/OTP 21.0 开始,SASL 报告的概念已被删除,这意味着默认行为如下:

  • 主管报告、崩溃报告和进度报告不再与 SASL 应用程序相关联。
  • 主管报告和崩溃报告作为 error 级别的日志事件发出,并通过 Kernel 启动的默认处理程序记录。
  • 进度报告作为 info 级别的日志事件发出,由于默认首要日志级别为 notice,因此默认情况下不会记录这些事件。要启用打印进度报告,请将 首要日志级别 设置为 info
  • 所有日志事件的输出格式都相同。

如果您偏好旧的行为,则可以将内核配置参数 logger_sasl_compatible 设置为 true。然后可以使用 SASL 配置参数,如果 SASL 应用程序正在运行,则 SASL 报告将通过名为 sasl 的第二个日志处理程序打印。

所有 SASL 报告都有一个元数据字段 domain,该字段被设置为 [otp,sasl]。此字段可用于过滤器停止或允许日志事件。

有关旧 SASL 错误日志记录功能的更多信息,请参见部分 SASL 用户指南

要使用为 error_logger 编写的事件处理程序,只需使用以下命令添加您的事件处理程序:

error_logger:add_report_handler/1,2.

这将自动启动错误日志记录器事件管理器,并将 error_logger 作为处理程序添加到 Logger,配置如下:

#{level => info,
  filter_default => log,
  filters => []}.
注意

此处理程序会忽略未源自 error_logger API 或 OTP 内部的事件。这意味着,如果您的代码使用 Logger API 进行日志记录,则您的日志事件将被此处理程序丢弃。

该处理程序不受过载保护。

在某种程度上,Logger 会在将日志事件转发到过滤器和处理程序之前检查其输入数据。但是,它不会评估报告回调,也不会检查格式字符串和参数的有效性。这意味着所有过滤器和处理程序在格式化日志事件的数据时必须小心,确保不会因不良输入数据或错误的回调而崩溃。

如果过滤器或处理程序仍然崩溃,Logger 将从配置中删除有问题的过滤器或处理程序,并在终端打印一条简短的错误消息。还会发出一个包含崩溃原因和其他详细信息的调试事件。

有关报告回调和日志消息有效形式的更多信息,请参见部分 日志消息

在启动 Erlang 节点时,默认行为是将所有 notice 级别或更严重级别的日志事件通过默认处理程序记录到终端。要也记录信息事件,您可以将首要日志级别更改为 info

1> logger:set_primary_config(level, info).
ok

或者仅为一个或几个模块设置级别

2> logger:set_module_level(mymodule, info).
ok

这允许信息事件通过默认处理程序,并也打印到终端。如果存在许多信息事件,则将它们打印到文件可能很有用。

首先,将默认处理程序的日志级别设置为 notice,防止它将信息事件打印到终端

3> logger:set_handler_config(default, level, notice).
ok

然后,添加一个新的处理程序,它将打印到文件。您可以使用处理程序模块 logger_std_h,并将其配置为记录到文件

4> Config = #{config => #{file => "./info.log"}, level => info}.
#{config => #{file => "./info.log"},level => info}
5> logger:add_handler(myhandler, logger_std_h, Config).
ok

由于 filter_default 默认设置为 log,因此此处理程序现在会接收所有日志事件。如果您只想将信息事件记录到文件中,则必须添加一个过滤器以停止所有非信息事件。内置过滤器 logger_filters:level/2 可以做到这一点

6> logger:add_handler_filter(myhandler, stop_non_info,
                             {fun logger_filters:level/2, {stop, neq, info}}).
ok

有关过滤器和filter_default 配置参数的更多信息,请参阅过滤器 部分。

logger(3) 手册页中的处理程序回调函数 部分描述了可为日志记录器处理程序实现的回调函数。

处理程序回调模块必须导出

  • log(Log, Config)

它还可以选择导出以下部分或全部内容

  • adding_handler(Config)
  • removing_handler(Config)
  • changing_config(SetOrUpdate, OldConfig, NewConfig)
  • filter_config(Config)

当通过例如调用 logger:add_handler(Id, HModule, Config) 添加处理程序时,日志记录器首先调用HModule:adding_handler(Config)。如果此函数返回{ok,Config1},则日志记录器将Config1 写入配置数据库,并且logger:add_handler/3 调用返回。在此之后,处理程序将被安装,并且必须准备好接收日志事件,作为对HModule:log/2 的调用。

可以通过调用 logger:remove_handler(Id) 来删除处理程序。日志记录器调用HModule:removing_handler(Config),并从配置数据库中删除处理程序的配置。

当调用 logger:set_handler_config/2,3 logger:update_handler_config/2,3 时,日志记录器调用HModule:changing_config(SetOrUpdate, OldConfig, NewConfig)。如果此函数返回{ok,NewConfig1},则日志记录器将NewConfig1 写入配置数据库。

当调用 logger:get_config/0 logger:get_handler_config/0,1 时,日志记录器调用HModule:filter_config(Config)。此函数必须返回处理程序配置,其中内部数据已删除。

一个简单的打印到终端的处理程序可以这样实现

-module(myhandler1).
-export([log/2]).

log(LogEvent, #{formatter := {FModule, FConfig}}) ->
    io:put_chars(FModule:format(LogEvent, FConfig)).

请注意,上述处理程序没有任何过载保护,并且所有日志事件都直接从客户端进程打印出来。

有关过载保护的信息和示例,请参阅保护处理程序免受过载 部分以及logger_std_hlogger_disk_log_h 的实现。

以下是一个更简单的处理程序示例,它通过一个单独的进程将日志记录到文件中

-module(myhandler2).
-export([adding_handler/1, removing_handler/1, log/2]).
-export([init/1, handle_call/3, handle_cast/2, terminate/2]).

adding_handler(Config) ->
    MyConfig = maps:get(config,Config,#{file => "myhandler2.log"}),
    {ok, Pid} = gen_server:start(?MODULE, MyConfig, []),
    {ok, Config#{config => MyConfig#{pid => Pid}}}.

removing_handler(#{config := #{pid := Pid}}) ->
    gen_server:stop(Pid).

log(LogEvent,#{config := #{pid := Pid}} = Config) ->
    gen_server:cast(Pid, {log, LogEvent, Config}).

init(#{file := File}) ->
    {ok, Fd} = file:open(File, [append, {encoding, utf8}]),
    {ok, #{file => File, fd => Fd}}.

handle_call(_, _, State) ->
    {reply, {error, bad_request}, State}.

handle_cast({log, LogEvent, Config}, #{fd := Fd} = State) ->
    do_log(Fd, LogEvent, Config),
    {noreply, State}.

terminate(_Reason, #{fd := Fd}) ->
    _ = file:close(Fd),
    ok.

do_log(Fd, LogEvent, #{formatter := {FModule, FConfig}}) ->
    String = FModule:format(LogEvent, FConfig),
    io:put_chars(Fd, String).

默认处理程序 logger_std_h logger_disk_log_h 具有过载保护机制,该机制使处理程序能够在高负载期间(当必须处理大量传入的日志请求时)存活并保持响应能力。该机制的工作原理如下

处理程序进程跟踪其消息队列的长度,并在当前长度超过可配置阈值时采取某种形式的操作。目的是使处理程序保持在可以跟上传入日志事件速度的状态,或者尽快使处理程序进入该状态。处理程序的内存使用量绝不能越来越大,因为这最终会导致处理程序崩溃。存在以下三个阈值及其关联操作

只要消息队列的长度低于此值,所有日志事件都将异步处理。这意味着通过在日志记录器 API 中调用日志函数发送日志事件的客户端进程不会等待处理程序的响应,而是在发送事件后立即继续执行。它不受处理程序将事件打印到日志设备所需时间的影響。如果消息队列的长度超过此值,则处理程序将开始同步处理日志事件,这意味着发送事件的客户端进程必须等待响应。当处理程序将消息队列缩减到低于sync_mode_qlen 阈值的级别时,将恢复异步操作。从异步模式切换到同步模式可能会降低一个或几个繁忙发送者的日志记录速度,但在许多繁忙的并发发送者情况下无法充分保护处理程序。

默认为10 条消息。

当消息队列的长度超过此阈值时,处理程序将切换到一种模式,在这种模式下,它会丢弃发送者想要记录的所有新事件。在此模式下丢弃事件意味着对日志函数的调用永远不会导致消息被发送到处理程序,但该函数会返回而不会采取任何操作。处理程序会继续记录已在其消息队列中的事件,并且当消息队列的长度减少到低于阈值的级别时,将恢复同步或异步模式。请注意,当处理程序激活或停用丢弃模式时,有关它的信息会打印在日志中。

默认为200 条消息。

如果消息队列的长度超过此阈值,则会执行刷新(删除)操作。要刷新事件,处理程序会通过在循环中接收它们而无需记录来丢弃消息队列中的消息。等待来自同步日志请求的响应的客户端进程会收到来自处理程序的回复,表明该请求已丢弃。处理程序进程在刷新循环期间会提高其优先级,以确保在操作期间不会接收新的事件。请注意,在执行刷新操作后,处理程序会在日志中打印有关已删除多少事件的信息。

默认为1000 条消息。

为了使过载保护算法正常工作,要求

sync_mode_qlen =< drop_mode_qlen =< flush_qlen

以及

drop_mode_qlen > 1

要禁用某些模式,请执行以下操作

  • 如果将sync_mode_qlen 设置为0,则所有日志事件都将同步处理。也就是说,异步日志记录被禁用。
  • 如果将sync_mode_qlen 设置为与drop_mode_qlen 相同的值,则同步模式将被禁用。也就是说,处理程序始终以异步模式运行,除非调用丢弃或刷新。
  • 如果将drop_mode_qlen 设置为与flush_qlen 相同的值,则丢弃模式将被禁用,并且永远不会发生。

在高负载情况下,处理程序消息队列的长度很少以线性且可预测的方式增长。相反,每当处理程序进程被调度时,消息队列中可能会有几乎任意数量的消息等待。正因为如此,过载保护机制的重点是快速且相当激进地采取行动,例如在检测到大型队列长度时立即丢弃或刷新消息。

用户可以指定前面列出的阈值的值。这样,可以将处理程序配置为例如,除非处理程序进程的消息队列长度变得非常大,否则不丢弃或刷新消息。请注意,在这种情况下,节点可能需要大量的内存。用户配置的另一个示例是,出于性能原因,客户端进程绝不能被同步日志请求阻塞。也许可以接受丢弃或刷新事件,因为它不会影响发送日志事件的客户端进程的性能。

配置示例

logger:add_handler(my_standard_h, logger_std_h,
                   #{config => #{file => "./system_info.log",
                                 sync_mode_qlen => 100,
                                 drop_mode_qlen => 1000,
                                 flush_qlen => 2000}}).

大量的日志事件突发 - 在短时间内处理程序接收到的许多事件 - 可能导致问题,例如

  • 日志文件非常快地变得非常大。
  • 循环日志过快地包装,因此重要数据被覆盖。
  • 写入缓冲区变得很大,这会减慢文件同步操作。

因此,两个内置处理程序都提供了指定在特定时间范围内处理的最大事件数的可能性。通过启用此突发控制功能,处理程序可以避免用大量打印输出阻塞日志。配置参数如下

true 启用突发控制,false 禁用突发控制。

默认为true

这是在burst_limit_window_time 时间范围内处理的最大事件数。达到限制后,后续事件将被丢弃,直到时间范围结束。

默认为500 个事件。

请参阅前面对burst_limit_max_count 的描述。

默认为1000 毫秒。

配置示例

logger:add_handler(my_disk_log_h, logger_disk_log_h,
                   #{config => #{file => "./my_disk_log",
                                 burst_limit_enable => true,
                                 burst_limit_max_count => 20,
                                 burst_limit_window_time => 500}}).

即使处理程序能够在不崩溃的情况下成功管理高负载峰值,它也可能建立一个大型消息队列或使用大量内存。过载保护机制包括一个自动终止和重新启动功能,用于确保处理程序不会超出范围。该功能通过以下参数配置

true 启用该功能,false 禁用该功能。

默认为false

这是允许的最大队列长度。如果消息队列的长度超过此长度,则处理程序进程将被终止。

默认为20000 条消息。

这是处理程序进程允许使用的最大内存大小。如果处理程序的内存大小超过此大小,则该进程将被终止。

默认为3000000 字节。

如果处理程序被终止,它将在以毫秒为单位指定的延迟后自动重新启动。值infinity 会阻止重新启动。

默认为5000 毫秒。

如果处理程序进程因过载而被终止,它会在日志中打印有关它的信息。它还会打印有关何时进行重新启动以及处理程序恢复运行的信息。

注意

日志事件的大小会影响处理程序的内存需求。有关如何限制日志事件大小的信息,请参阅logger_formatter(3) 手册页。

日志记录器代理是一个 Erlang 进程,它是内核应用程序监督树的一部分。在启动期间,代理进程将其自己注册为system_logger,这意味着模拟器产生的日志事件将发送到此进程。

当在组领导者位于远程节点的进程上发出日志事件时,日志记录器会自动将日志事件转发到组领导者的节点。为了实现这一点,它首先将日志事件作为 Erlang 消息从原始客户端进程发送到本地节点的代理,然后代理依次将事件转发到远程节点的代理。

代理进程在从模拟器或远程节点接收日志事件时,会调用日志记录器 API 来记录该事件。

代理进程的过载保护方式与保护处理程序免受过载 部分所述相同,但默认值如下

    #{sync_mode_qlen => 500,
      drop_mode_qlen => 1000,
      flush_qlen => 5000,
      burst_limit_enable => false,
      overload_kill_enable => false}

对于来自模拟器的日志事件,同步消息传递模式不适用,因为模拟器异步传递所有消息。通过将system_logger 设置为undefined 来实现丢弃模式,这会迫使模拟器丢弃事件,直到它被设置回代理 pid 为止。

代理在将日志事件发送到远程节点时使用 erlang:send_nosuspend/2。如果消息无法在不挂起发送者的情况下发送,则会将其丢弃。这是为了避免阻塞代理进程。

disk_log(3), erlang(3), error_logger(3), logger(3), logger_disk_log_h(3), logger_filters(3), logger_formatter(3), logger_std_h(3), sasl(6)