在Erlang/OTP 21.0提供了一个通过Logger进行日志记录的API,Logger是内核程序的一部分。Logger由可用于发出日志事件的API和可自定义的后端组成,其中可以插入日志处理程序、过滤器和格式化程序。
工作原理
日志事件由日志级别、要记录的消息和元数据组成。
Logger后端从API转发日志事件,首先通过一组主过滤器,然后通过附加到每个日志处理程序的一组辅助过滤器。辅助过滤器位于以下指定的处理程序过滤器中。每个过滤器集包含一个日志级别检查,然后是零个或多个过滤器函数。
日志级别用原子表示。在Logger内部,原子被映射到整数值,日志事件通过日志级别检查,检查其日志级别的整数值是否小于或等于当前配置的日志级别。也就是说,如果事件与配置级别相同或更严重,则通过检查。
每个模块配置的日志级别可以覆盖主日志级别。例如,这是为了允许从系统的特定部分进行更详细的日志记录。可以使用筛选器函数进行比日志级别检查更复杂的筛选。过滤器函数可以根据事件的任何内容停止或传递日志事件。它还可以修改日志事件的所有部分。如果日志事件通过特定处理程序的所有主过滤器和所有处理程序过滤器,Logger将事件转发给处理程序回调。处理程序将事件格式化并将其打印到目的地。直到并包括对处理程序回调的调用在内的所有内容都在客户机进程上执行,即在发出日志事件的进程上执行。是否涉及其他流程取决于处理程序实现。处理程序是按顺序调用的,并且没有定义顺序。
Logger API
用于日志记录的API由一组宏和一组窗体日志记录器上的函数logger:Level/1,2,3,它们都是日志记录器的快捷方式 logger:log(Level, Args1,Args2 [,Args3][])。
宏是在logger.hrl中定义的,它与指令一起包含在一个模块中。
1 | -include_lib("kernel/include/logger.hrl"). |
使用宏和导出的函数之间的区别在于,宏位置(发起者)信息添加到元数据,并执行延迟评价通过包装记录器在case语句调用,所有只有评估如果事件的日志级别通过主日志级别检查。
logger_std_h
他的是标准的处理日志。可以将此处理程序的多个实例添加到Logger,每个实例将日志打印standard_io、standard_error或file。
处理程序有一个重载保护机制,在日志事件的高负载期间保持处理程序进程和内核应用程序处于活动状态。如果过载保护工作,以及如何配置。请参见下面。
保护处理程序不超载
默认的处理程序logger_std_h和logger_disk_log_h具有过载保护机制,这使处理程序能够在高负载期间(当必须处理大量传入日志请求时)存活并保持响应。其工作原理如下:
消息队列的长度
处理程序进程跟踪其消息队列的长度,并在当前长度超过可配置阈值时采用某种形式的操作。其目的是保持处理程序处于或尽可能快地处理程序处于一种状态,在这种状态中它可以跟上传入日志事件的速度。处理程序的内存使用量不能越来越大,因为这最终会导致处理程序崩溃。
sync_mode_qlen
只要消息队列的长度小于此值,就会异步处理所有日志事件。这意味着通过调用Logger API中的日志函数来发送日志事件的客户机进程不会等待处理程序的响应,而是在事件发送后立即继续执行。它不受处理程序将事件打印到日志设备所需时间的影响。如果消息队列大于此值,则处理程序将同步处理日志事件,这意味着发送事件的客户机进程必须等待响应。当处理程序将消息队列降低到低于sync_mode_qlen阈值的级别时,将恢复异步操作。从异步模式切换到同步模式可能会减慢一个或几个繁忙的发送方的日志记录速度,但在许多繁忙的并发发送方的情况下不能充分保护处理程序。
默认为10条消息。
drop_mode_qlen
当消息队列的大小超过这个阈值时,处理程序将切换到一种模式,在这种模式中,它将删除发送者想要记录的所有新事件。在这种模式下删除事件意味着对日志函数的调用不会导致消息发送给处理程序,但是函数返回时不采取任何操作。处理程序将继续记录已经在其消息队列中的事件,当消息队列的长度降低到阈值以下时,将恢复同步或异步模式。当处理程序激活或停用drop模式时,有关打印信息将打印到日志中。
默认为200条消息。
flush_qlen
如果消息队列的长度增产超过此阈值,则执行刷新(删除)操作。为了刷新事件,处理程序通过在不进行日志记录的循环中接受消息来丢弃消息队列中的消息。等待同步日志请求响应的客户机进程从处理程序接收一个响应,该响应指示请求已被删除。处理程序进程在刷新循环期间增加其优先级,以确保在操作期间没有收到新事件。在执行刷新操作之后,处理程序将在日志中打印关于删除了多少事件的信息。
默认为1000条消息。
为了使过载保护算法正常工作,需要:
sync_mode_qlen =< drop_mode_qlen =< flush_qlen
要禁用某些模式,请执行以下操作
- 如果sync_mode_qlen设置为0,那么所有日志事件都将被同步处理。也就是说,异步日志记录被禁用。
- 如果sync_mode_qlen设置为与drop_mode_qlen相同的值,则禁用同步模式。也就是说,除非调用删除或刷新,否则处理程序始终以异步模式运行。
- 如果drop_mode_qlen被设置为与flush_qlen相同的值,那么drop模式将被禁用,并且永远不会发生。
在高负载情况下,处理程序消息队列的长度很少以线性和可预测的方式增长。相反,无论何时调度处理程序进程,它都可以在消息队列中有几乎任意数量的消息等待。正是由于这个原因,过载保护机制的重点是在检测到较大的队列长度时迅速而彻底地执行操作,例如立即删除或刷新消息。
1 | logger:add_handler(my_standard_h, logger_std_h, |
logger_std_h 支持参数
1.type = standard_io | standard_error | file
指定日志目标。
该值是在添加处理程序时设置的,并且不能在运行时更改。
默认为standard_io,除非提供参数文件,在这种情况下,它默认为file。
2.file = file:filename()
当type指定为file类型时,这是指定的日志文件的名称。
该值是在添加处理程序时设置的,并且不能在运行时更改。
默认为与当前目录中的处理程序标识相同的名称。
3.mode = [file:mode()]
这指定了打开日志文件时使用的文件模式,see file:open/2。如果没有指定模式,则默认列表是[raw, append, delayed_write]。如果指定了模式,该列表将用以下调整代替默认模式列表:
- 如果未在列表中找到raw,则添加它。
- 如果在列表中没有发现write、append或exclusive,则添加append。
- 如果在列表中没有找到delayed_write或{delayed_write, Size, Delay},则添加delayed_write。
日志文件总是为UTF-8编码的。不能通过设置模式{encoding, encoding}来改变编码。
该值是在添加处理程序时设置的,并且不能在运行时更改。
4.max_no_bytes = pos_integer() | infinity
此参数指定日志文件是否应该rotated。如果设置该值为infinity表示日志文件将无限增长,而整数值指定文件的大小(字节)
默认为infinity
5.max_no_files = non_neg_integer()
此参数指定要保留的日志文件存档的数量。这只有max_no_files设置为整数值时才有意义。
日志归档被命名为FileName.0,FileName1,…FileName.N。其中FileName是当前日志文件的名称。0是最新的档案,N的最大值是max_no_files的值减1。
如果该值设定为0,则不保存文档。
默认为0。
6.compress_on_rotate = boolean()
此参数指定归档文件是否应该压缩。如果设置为true,所有归档文件都用gzip压缩,并重命名为FileName.N.gz。
如果max_no_bytes值为infinity,则compress_on_rotate没有意义。
默认为false。
7.file_check = non_neg_integer()
当logger_std_h记录到一个文件时,它每次写操作之前读取日志文件的文件信息。这是为了确保仍然存在,并且具有与打开时相同的inode。这意味着一些性能损失,但是确保当文件被外部参与者删除或重命名时不会丢失日志事件。
为了使性能损失最小化,可以将file_check参数设置为正整数值N。然后,只要距离上一次读取事件不超过N毫秒,处理程序就会跳过在写入之前读取文件信息的步骤。
注意:当file_check值增加时,丢失日志事件的风险也随之增加。
默认值为0。
8.filesync_repeat_interval = pos_integer() | no_repeat
此值(以毫秒为单位)指定处理程序执行文件同步操作以将缓冲数据写入磁盘的频率。处理程序重复尝试该操作,但仅在确实记录了某些内容才执行新的同步。
如果将no_repeat设置值,重复文件同步操作将被禁用,操作系统设置将决定数据写入磁盘的速度快慢。用户还可以调用filesync/1函数来执行文件同步。
默认为5000。
如何使用logger_std_h
代码中使用
1 | logger:add_handler(my_standard_h, logger_std_h, |
shell中使用
1 | erl -kernel logger '[{handler,default,logger_std_h, |
控制日志请求的爆发
日志事件的大量爆发–处理程序在短时间内接收到的许多事件–可能会导致问题,比如:
- 日志文件增长非常大,非常快。
- 循环日志的换行速度太快,以至于覆盖了重要的数据。
- 写缓冲区变大,这会减慢文件同步操作。
由于这个原因,这两个内置的处理程序都提供了指定在某个时间段内要处理的事件的最大数量的可能性。启用此突发控制功能后,处理程序可以避免大量打印输出阻塞日志。
有以下配置参数
1.burst_limit_enable
值true启用突发控制,值false禁用突发控制。
默认值为true。
2.burst_limit_max_count
这是在burst_limit_window_time时间段内要处理的最大事件数。达到极限后,将删除连续事件,直到时间框架结束。
默认为500个事件。
3.burst_limit_window_time
默认为1000毫秒。
配置例子
1 | logger:add_handler(my_disk_log_h, logger_disk_log_h, |
终止重载处理程序
即使处理程序能够成功地管理高负载的峰值而不会崩溃,它也可能构建一个大型消息队列,或使用大量内存。过载保护机制包括自动终止和重新启动功能,以确保处理程序不会超出界限。可以配置以下参数:
1.overload_kill_enable
当设置的值为true时,启用该特征;设置为false时,禁用该特征。
默认值为false。
2.overload_kill_qlen
这是允许的最大队列长度。如果消息队列变得更大,处理程序进程将终止。
默认为20000条消息。
3.overload_kill_mem_size
这是允许处理程序进程使用的最大内存大小。如果处理程序变大,则进程终止。默认为3000000字节。
4.overload_kill_restart_after
如果处理程序终止,它将在指定的延迟(以毫秒为单位)后自动重新启动。值无穷大防止重新启动。
默认为5000毫秒。
日志代理
日志记录器代理是一个Erlang进程,它是内核应用程序监视树的一部分。在启动期间,代理进程将自己注册为system_logger,这意味着仿真器生成的日志事件被发送到这个进程。
当一个具有远程节点上的组长的进程发出日志事件时,Logger会自动将日志事件转发到组长的节点。为此,它首先将日志事件作为Erlang消息从原始客户端流程发送到本地节点上的代理,然后代理将事件转发到远程节点上的代理。
当从模拟器或远程节点接收日志事件时,代理调用Logger API来记录事件。
代理进程被重载保护,与保护处理程序免受重载的方法相同,但具有以下默认值:
1 | #{sync_mode_qlen => 500, |
对于来自仿真器的日志事件,同步消息传递模式不适用,因为所有消息都是由仿真器异步传递的。Drop模式是通过将system_logger设置为undefined来实现的,这迫使仿真器删除事件,直到再次将其设置回代理pid。
代理在向远程节点发送日志事件时使用erlang:send_nosuspend/2。如果不能在不挂起发送方的情况下发送消息,将其删除。这是为了避免阻塞代理进程。
logger_formatter
每个Logger handler都有一个已配置的格式化程序,该格式化程序制定为模块和配置术语。格式化程序的目的是将日志事件转换为最终的可打印字符串(unicode:chardata()),该字符串可被写入处理程序的输出设备。
logger_formatter是Logger使用的默认格式化程序。
数据类型
config() =
#{
chars_limit = integer() >= 1 | unlimited,
depth => integer() >= 1 | unlimited,
legacy_header => boolean(),
max_size => integer() >= 1 | unlimited,
report_cb => logger:report_cb(),
single_line => boolean(),
template => template(),
time_designator => byte(),
time_offset => integer() | [byte()]
}
logger_formatter的配置是map,可以将以下键设置为配置参数:
1.chars_limit = integer() > 0 | unlimited
填写一个正整数,表示在调用io_lib:format/3时使用相同名称的选项的值。此值限制为每个日志事件打印的字符总数。这是一个软限制。有关硬截断限制,请参见选项max_size。默认为:unlimited。
2.depth = integer() > 0 | unlimited
表示此格式程序应打印的最大深度的正整数。将格式控制的~p和~w分别替换为~p和~w,值作为深度参数。默认为:unlimited。
3.legacy_header = boolean()
如果设置为true,则头字段将添加到logger_formatter的元数据部分。该字段的值是一个与旧的error_logger事件处理程序创建的头类似的字符串。通过将列表[logger_formatter, header]添加到模板中,可以将它包含在日志事件中。默认值为false。
4.max_size = integer() > 0 | unlimited
表示此格式化程序返回的字符串的绝对最大大小的正整数。如果格式化的字符串更长,可能会受到chars_limit或depth的限制之后,它将被截断。默认为unlimited.
5.report_cb = logger:report_cb()
格式化程序使用报表回调将报表上的日志消息转换为格式字符串和参数。可以在日志事件的元数据中指定报表回调。如果元数据中不存在报表回调,则logger_formatter将使用logger:format_report/1作为默认回调。
如果设置了此配置参数,它将替换默认的报表回调以及在元数据中找到的任何报表回调。也就是说,所有报告都由这个配置的函数转换。
6.single_line = boolean()
如果设置为true,则将每个日志事件打印为一行。为此,logger_formatter将字符串格式中的所有p和~p控制序列的字段宽度设置为0,并将消息中的所有换行符替换为“,”。删除换行后直接跟随的空白。默认值为true。
7.template = template()
该模版描述了如果通过组合来自日志事件的不同数据值来组合格式化的字符串。
8.time_designator = byte()
imestamps是根据RFC3339进行格式化的,时间指示器是用作日期和时间分隔符的字符。默认为$T。
此参数的值用作日历的time_designator calendar:system_time_to_rcf3339/2.
9.time_offset = integer() | [byte()]
格式化时间戳使用的时间偏移量,可以是字符串,也可以是整数。适用于时区。
数据模型 template
template() = [metakey() | {metakey(), template(), template()} | string()]
该模板是atoms、atom lists、tuples和strings。atoms级别或msh分别作为严重性级别和日志消息的占位符。其它atoms或atom lists被解释为元数据的占位符,在元数据是嵌套映射时,atoms被期望匹配顶级键,而atom lists表示子键的路径。例如,列表[key1,key2]被下面嵌套映射中的key2字段的值替换。atom key1本身被key1字段的完整值替换。这些值转换为字符串。
1 | #{key1 => #{key2 => my_value, |
模版中的元组表示是否存在元数据键的测试。例如,下面的元组表示,如果再元数据映射中存在key1,则打印”key1=Value”,其中Value是key1再元数据映射中关联的值。如果key1不存在,则什么也不打印。
1 | {key1, ["key1=",key1], []} |
logger_filters 过滤器
过滤器可以是主处理程序,也可以附加到特定的处理程序。Logger首先调用主过滤器,如果它们都通过,它将每个处理程序调用处理程序过滤器。Logger只在附加到处理程序的所有过滤器都通过时才调用处理程序回调。
过滤器定义为:
1 | {FilterFun, Extra} |
FilterFun是arity为2的函数,而Extra是任意项。在应用过滤器时,Logger调用函数,日志事件作为第一个参数,Extra的值作为第二个参数。类型定义在logger:filter().
过滤器函数可以返回stop、ignore或(possibly modified)日志事件。
如果返回stop,则立即丢弃日志事件。如果筛选器是主筛选器,则不会调用处理程序筛选器或回调。如果是处理程序筛选器,则不调用相应的处理程序回调,但将日志事件转发到附加下一个处理程序(如果有)的筛选器。
如果返回日志事件,则使用返回的值作为第一个参数调用下一个筛选器函数。也就是说,如果一个过滤器函数修改了日志事件,下一个过滤器函数将接收修改后的时间。最后一个过滤器函数返回的值是处理程序回调接受的值。
如果filter函数返回ignore,则意味着它不识别日志事件,因此将事件的命运留给其他过滤器决定。
配置选项filter_default指定在所有筛选器函数都返回ignore或不存在筛选器时的行为。filter_default默认设置为log,这意味着如果现有的过滤器都忽略一个日志事件,Logger将该事件转发给处理程序回调。如果将filter_default设置为stop,则Logger将丢弃此类事件。
主过滤器由记录器 logger:add_primary_filter/2添加,由记录器 logger:remove_primary_filter/1删除。它们也可以通过内核配置参数记录器在系统时添加。
处理程序过滤器用logger:add_handle_filter/3添加,用logger:remove_handle_filter/2删除。当使用logger:add_handle/3或通过内核配置参数logger添加一个处理程序时,也可以直接在配置中指定它们。
要查看当前系统中安装了哪些过滤器,可以使用logger:get_config/0,或者logger:get_primary_config/0和logger:get_handle_config/1。筛选器按应用顺序列出,即首先应用列表中的第一个筛选器,依次类推。
为了方便,内置了以下过滤器:
1.logger_filters:domain/2
提供了一种基于元数据中域字段过滤日志事件的方法。
2.logger_filters:level/2
提供了一种基于日志级别过滤日志事件的方法。
3.logger_filters:progress/2
停止或允许来自supervisor和application_controller的进度报告。
4.logger_filters:remote_gl/2
停止或允许来自远程节点上有其组长的进程的日志事件。