查看源代码 日志记录

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

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

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

通过配置,您还可以修改或禁用默认处理程序,将其替换为自定义处理程序,并安装其他处理程序。

概述

一个日志事件日志级别、要记录的消息元数据组成。

Logger 后端将日志事件从 API 转发,首先通过一组主要过滤器,然后通过附加到每个日志处理程序的一组次要过滤器。次要过滤器在下面命名为处理程序过滤器

每个过滤器集由日志级别检查和零个或多个过滤器函数组成。

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

---
title: Conceptual Overview
---
flowchart TD
    DB[(Config DB)]
    API ---> ML[Module Level <hr> Global Level <hr> Global Filters]
    API -.Update configuration.-> DB
    ML -.-> DB
    ML ---> HL1[Hander Level <hr> Handler Filter]
    ML ---> HL2[Hander Level <hr> Handler Filter]
    HL1 ---> HC1[Handler Callback]
    HL2 ---> HC2[Handler Callback]
    HL1 -.-> DB
    HL2 -.-> DB
    subgraph Legend
        direction LR
        start1[ ] -->|Log event flow| stop1[ ]
        style start1 height:0px;
        style stop1 height:0px;
        start2[ ] -.->|Look up configuration| stop2[ ]
        style start2 height:0px;
        style stop2 height:0px;
    end

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

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

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

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

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

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

Logger API

用于日志记录的 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,可以指定八个日志级别。下表按名称(原子)、整数值和描述列出了所有可能的日志级别

级别整数描述
紧急0系统不可用
警报1必须立即采取行动
严重2严重情况
错误3错误情况
警告4警告情况
注意5正常但重要的情况
信息6信息性消息
调试7调试级别消息

表:日志级别

请注意,整数值仅在 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((logger:report(),logger:report_cb_config()) -> unicode:chardata())

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

示例:报告和带有报告回调的元数据

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

日志消息也可以通过 fun 提供,以便进行惰性求值。只有在主要日志级别检查通过时才会对 fun 进行求值,因此如果生成消息的开销很大,则建议使用 fun。惰性 fun 必须返回字符串、报告或带有格式字符串和参数的元组。

元数据

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

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

  • 设置进程元数据 - 进程元数据分别使用 logger:set_process_metadata/1logger:update_process_metadata/1 设置和更新。此元数据应用于进行这些调用的进程,并且 Logger 会将元数据添加到在该进程上发出的所有日志事件。

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

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

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

过滤器

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

过滤器定义为

{FilterFun, Extra}

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

过滤器函数可以返回 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/0logger:get_handler_config/1。过滤器按它们应用的顺序排列,也就是说,列表中的第一个过滤器首先应用,依此类推。

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

处理程序

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

log(LogEvent, Config) -> term()

当日志事件通过所有主要过滤器以及附加到相关处理程序的所有处理程序过滤器后,将调用此函数。函数调用在客户端进程上执行,并且是否涉及其他进程取决于处理程序的实现。

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

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

存在以下内置处理程序

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

  • logger_disk_log_h - 此处理程序的行为与 logger_std_h 非常相似,只是它使用 disk_log 作为其目标。

  • error_logger - 此处理程序仅为向后兼容性而提供。默认情况下它不会启动,但是当第一次使用 error_logger:add_report_handler/1,2 添加 error_logger 事件处理程序时,它将自动启动。

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

格式化器

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

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

format(LogEvent,FConfig)
	-> FormattedLogEntry

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

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

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

配置

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

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

主要 Logger 配置

适用于主要 Logger 配置的 Logger API 函数有

主要 Logger 配置是一个具有以下键的映射

  • level =logger:level/0 | all | none - 指定主要日志级别,也就是说,等于或高于此级别的日志事件将被转发到主要过滤器。较低级别的日志事件将被立即丢弃。

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

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

    默认为 notice

  • filters = [{FilterId,Filter}] - 指定主要过滤器。

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

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

    默认为 []

  • filter_default = log | stop - 指定如果所有过滤器都返回 ignore,或者如果不存在过滤器,则日志事件会发生什么。

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

    默认为 log

  • metadata =metadata() - 用于所有日志调用的主要元数据。

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

    默认为 #{}

处理程序配置

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

处理程序的配置是一个具有以下键的映射

  • id = logger_handler:id/0 - 由 Logger 自动插入。该值与添加处理程序时指定的 HandlerId 相同,并且无法更改。

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

  • level = logger:level/0 | all | none - 指定处理程序的日志级别,也就是说,等于或高于此级别的日志事件将转发到此处理程序的处理程序过滤器。

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

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

    默认为 all

  • filters = [{FilterId,Filter}] - 指定处理程序过滤器。

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

    有关更详细的信息,请参见 过滤器

    默认为 []

  • filter_default = log | stop - 指定如果所有过滤器都返回 ignore,或者如果不存在过滤器,则日志事件会发生什么。

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

    默认为 log

  • formatter = {FormatterModule,FormatterConfig} - 指定处理程序可用于将日志事件项转换为可打印字符串的格式化程序。

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

    有关更详细的信息,请参见 格式化器 部分。

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

  • config = term() - 处理程序特定配置,即与特定处理程序实现相关的配置数据。

    内置处理程序的配置在 logger_std_hlogger_disk_log_h 手册页中描述。

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

内核配置参数

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

  • logger = [Config] - 指定 Logger 的配置,但主要日志级别除外,该级别由 logger_level 指定,以及与 SASL 错误日志的兼容性,该兼容性由 logger_sasl_compatible 指定。

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

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

    • {handler, default, undefined} - 禁用默认处理程序。这允许其他应用程序添加其自己的默认处理程序。

      只允许使用此类型的一个条目。

    • {handler, HandlerId, Module, HandlerConfig} - 如果 HandlerIddefault,则此条目会修改默认处理程序,相当于调用

              logger:remove_handler(default)
      

      接着调用

              logger:add_handler(default, Module, HandlerConfig)
      

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

              logger:add_handler(HandlerId, Module, HandlerConfig)
      

      允许使用此类型的多个条目。

    • {filters, FilterDefault, [Filter]} - 添加指定的主要筛选器。

      • FilterDefault = log | stop

      • Filter = {FilterId, {FilterFun, FilterConfig}}

      相当于调用

              logger:add_primary_filter(FilterId, {FilterFun, FilterConfig})
      

      对于每个 Filter

      如果所有主要筛选器都返回 ignore,则 FilterDefault 指定行为,请参阅 筛选器 部分。

      只允许使用此类型的一个条目。

    • {module_level, Level, [Module]} - 为给定模块设置模块日志级别。相当于调用

              logger:set_module_level(Module, Level)

      对于每个 Module

      允许使用此类型的多个条目。

    • {proxy, ProxyConfig} - 设置代理配置,相当于调用

              logger:set_proxy_config(ProxyConfig)
      

      只允许使用此类型的一个条目。

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

  • logger_metadata = map() - 指定主要元数据。有关此参数的更多信息,请参阅 kernel(6) 手册页。

  • logger_level = Level - 指定主要日志级别。有关此参数的更多信息,请参阅 kernel(6) 手册页。

  • logger_sasl_compatible = true | false - 指定 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"}}}
    ]}]}].

与 error_logger 的向后兼容性

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

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

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

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

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

  • 来自 OTP 的日志事件的默认格式 - 默认情况下,除了以前所谓的“SASL 报告”之外,所有源自 OTP 内的日志事件看起来都与以前相同。

  • SASL 报告
    SASL 报告是指 supervisor 报告、崩溃报告和进度报告。

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

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

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

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

    • Supervisor 报告、崩溃报告和进度报告不再与 SASL 应用程序相关联。
    • Supervisor 报告和崩溃报告以 error 级别日志事件的形式发布,并通过内核启动的默认处理程序进行记录。
    • 进度报告以 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,此处理程序现在接收所有日志事件。如果您只想在文件中记录 info 事件,则必须添加一个过滤器来阻止所有非 info 事件。内置过滤器 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_handler 描述了可以为 Logger 处理程序实现的 回调函数。

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

  • log(Log, Config)

它还可以选择性地导出以下部分或全部函数

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

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

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

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

当调用 logger:get_config/0logger:get_handler_config/0,1 时,Logger 调用 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_hlogger_disk_log_h 具有过载保护机制,这使得处理程序可以在高负载期间(必须处理大量传入日志请求时)幸存下来并保持响应。该机制的工作原理如下:

消息队列长度

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

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

    默认为 10 条消息。

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

    默认为 200 条消息。

  • flush_qlen - 如果消息队列的长度大于此阈值,则会执行刷新(删除)操作。为了刷新事件,处理程序通过循环接收消息而不进行日志记录来丢弃消息队列中的消息。等待同步日志请求响应的客户端进程会收到处理程序的答复,指示该请求被丢弃。处理程序会在刷新循环期间提高其优先级,以确保在此操作期间不会收到新事件。请注意,执行刷新操作后,处理程序会在日志中打印有关已删除多少个事件的信息。

    默认为 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}}).

控制日志请求的突发

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

  • 日志文件增长非常快。
  • 循环日志包装得太快,导致重要数据被覆盖。
  • 写缓冲区变得很大,这会减慢文件同步操作的速度。

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

  • burst_limit_enable - 值 true 启用突发控制,而 false 禁用突发控制。

    默认为 true

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

    默认为 500 个事件。

  • burst_limit_window_time - 请参阅先前对 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}}).

终止过载的处理程序

即使处理程序可以成功地管理高负载峰值而不会崩溃,也可能会累积较大的消息队列或使用大量内存。过载保护机制包括一个自动终止和重启功能,目的是保证处理程序不会超出范围。该功能使用以下参数进行配置:

  • overload_kill_enable - 值 true 启用该功能,而 false 禁用该功能。

    默认为 false

  • overload_kill_qlen - 这是允许的最大队列长度。如果消息队列的长度大于此值,则将终止处理程序进程。

    默认为 20000 条消息。

  • overload_kill_mem_size - 这是允许处理程序进程使用的最大内存大小。如果处理程序变得大于此值,则终止该进程。

    默认为 3000000 字节。

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

    默认为 5000 毫秒。

如果处理程序进程因过载而被终止,它会在日志中打印相关信息。它还会打印重启发生的时间信息,以及处理程序恢复工作的信息。

注意

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

Logger 代理

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

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

当接收到日志事件(无论是来自模拟器还是来自远程节点)时,代理会调用 Logger 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, erlang, error_logger, logger, logger_disk_log_h, logger_filters, logger_formatter, logger_std_h, sasl(6)