檢視原始碼 日誌記錄

Erlang 通過 Logger 提供了一個標準的日誌記錄 API,它是核心應用程式的一部分。Logger 包含發出日誌事件的 API,以及一個可自訂的後端,可以插入日誌處理器、篩選器和格式化器。

預設情況下,核心應用程式會在系統啟動時安裝一個日誌處理器。這個處理器名為 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,可以指定八個日誌級別。下表列出了所有可能的日誌級別,包括名稱(原子)、整數值和說明

級別整數說明
emergency0系統無法使用
alert1必須立即採取行動
critical2嚴重狀況
error3錯誤狀況
warning4警告狀況
notice5正常但重要的狀況
info6資訊性訊息
debug7偵錯級別訊息

表:日誌級別

請注意,整數值僅在 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((logger:report()) -> {io:format(),[term()]})

如果它接受兩個引數,第一個是報告,第二個是包含額外資料的映射,允許直接轉換為字串

fun((logger:report(),logger:report_cb_config()) -> unicode:chardata())

該函式必須遵守第二個引數中提供的 depthchars_limit 參數,因為格式化器無法使用傳回的字串對這些參數執行任何有用的操作。額外資料還包含一個名為 single_line 的欄位,表示列印的日誌訊息是否可以包含換行符。當報告的格式化取決於大小或單行參數時,會使用此變體。

範例:報告,以及帶有報告回呼的中繼資料

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

日誌訊息也可以通過函式提供,以進行延遲評估。僅當主要日誌級別檢查通過時才會評估函式,因此如果產生訊息的成本很高,則建議使用該函式。延遲函式必須傳回字串、報告或帶有格式化字串和引數的元組。

中繼資料

中繼資料包含與日誌訊息關聯的其他資料。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:metadata/0 類型的說明,以了解 Logger 插入哪些預設鍵,以及如何合併不同的中繼資料映射。

篩選器

篩選器可以是主要的,也可以附加到特定的處理器。Logger 會先呼叫主要篩選器,如果它們全部通過,則會呼叫每個處理器的處理器篩選器。僅當附加到相關處理器的所有篩選器也通過時,Logger 才會呼叫處理器回呼。

篩選器定義為

{FilterFun, Extra}

其中 FilterFun 是一個 arity 為 2 的函式,而 Extra 是任何 term。在應用篩選器時,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 及其組態 FConfigFModule 必須匯出以下可由處理常式呼叫的函式

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 會透過核心組態參數進行組態。適用於 Logger 的參數在「核心組態參數」一節中說明。範例可以在「組態範例」一節中找到。

在執行時,Logger 組態會透過 API 函式變更。請參閱 logger 手冊頁中的「組態 API 函式」一節。

主要 Logger 組態

適用於主要 Logger 組態的 Logger API 函式有

主要 Logger 組態是具有以下鍵的映射

  • level =logger:level/0 | all | none - 指定主要日誌層級,也就是說,等於或高於此層級的日誌事件會轉發到主要篩選器。低於此層級的日誌事件會立即丟棄。

    有關可能的日誌層級的清單和說明,請參閱「日誌層級」一節。

    此選項的初始值由核心組態參數 logger_level 設定。它會在執行時透過 logger:set_primary_config(level,Level) 變更。

    預設值為 notice

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

    此選項的初始值由核心組態參數 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 手冊頁中說明。

請注意,levelfilters 是 Logger 本身在將日誌事件轉發到每個處理器之前就遵守的規則,而 formatter 和所有處理器特定的選項則留給處理器實作。

核心配置參數

以下核心配置參數適用於 Logger

  • logger = [Config] - 指定 Logger 的配置,但主要日誌級別除外,該級別使用 logger_level 指定,以及與 SASL 錯誤記錄的相容性除外,該相容性使用 logger_sasl_compatible 指定。

    使用此參數,您可以修改或停用預設處理器、新增自訂處理器和主要 Logger 篩選器、設定每個模組的日誌級別,以及修改 proxy 配置。

    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

      FilterDefault 指定如果所有主要篩選器都傳回 ignore 時的行為,請參閱 篩選器 章節。

      只允許一個此類型的條目。

    • {module_level, Level, [Module]} - 設定給定模組的模組日誌級別。相當於呼叫

              logger:set_module_level(Module, Level)

      針對每個 Module

      允許此類型的多個條目。

    • {proxy, ProxyConfig} - 設定 proxy 配置,相當於呼叫

              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。這是 Kernel 啟動的 default 處理器的預設配置。

  • OTP 的日誌事件預設格式 - 預設情況下,除了先前的所謂「SASL 報告」外,所有源自 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 設定為 trueSASL 配置參數 隨後可以像以前一樣使用,並且只有在 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_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 Proxy

Logger proxy 是 Erlang 程序,它是 Kernel 應用程式監督樹的一部分。在啟動期間,proxy 程序會將其自身註冊為 system_logger,這表示模擬器產生的日誌事件會傳送到此程序。

當在群組領導者位於遠端節點的程序上發出日誌事件時,Logger 會自動將日誌事件轉發到群組領導者的節點。為了達到此目的,它首先會將日誌事件作為 Erlang 訊息從原始用戶端程序傳送到本機節點上的 proxy,而 proxy 又會將事件轉發到遠端節點上的 proxy。

當接收到來自模擬器或遠端節點的日誌事件時,proxy 會呼叫 Logger API 來記錄事件。

代理程序 (proxy process) 的過載保護機制與 保護處理程序免於過載 章節描述的方式相同,但預設值如下:

    #{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_logerlangerror_loggerloggerlogger_disk_log_hlogger_filterslogger_formatterlogger_std_hsasl(6)