Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案

PEP 391 – 基於字典的日誌配置

作者:
Vinay Sajip <vinay_sajip at red-dove.com>
狀態:
最終版
型別:
標準跟蹤
建立日期:
2009年10月15日
Python 版本:
2.7, 3.2
釋出歷史:


目錄

摘要

本PEP描述了一種使用字典來儲存配置資訊的新日誌配置方式。

基本原理

目前配置Python日誌包的方法是:要麼使用日誌API以程式設計方式配置日誌,要麼透過基於ConfigParser的配置檔案進行配置。

程式設計配置雖然提供了最大的控制權,但將配置固定在Python程式碼中。這不利於在執行時輕鬆更改它,因此,靈活地調整使用應用程式不同部分的日誌詳細程度的能力就喪失了。這限制了日誌作為診斷問題輔助工具的可用性——有時,日誌是生產環境中唯一可用的診斷輔助工具。

基於ConfigParser的配置系統可用,但無法讓使用者配置日誌包的所有方面。例如,無法使用此係統配置過濾器。此外,ConfigParser格式似乎在某些方面引起了人們的反感(有時是強烈的反感)。儘管它被選中是因為它是當時Python標準中唯一支援的配置格式,但許多人認為它(或者可能只是日誌配置所選擇的特定模式)“老舊”或“醜陋”,在某些情況下顯然純粹是出於審美原因。

Python的最新版本在標準庫中包含了JSON支援,這也可以用作配置格式。在其他環境中,例如Google App Engine,YAML用於配置應用程式,通常日誌的配置被認為是應用程式配置不可或缺的一部分。儘管標準庫目前不包含YAML支援,但JSON和YAML的支援可以透過通用方式提供,因為這兩種序列化格式都允許反序列化為Python字典。

透過提供一種透過在字典中傳遞配置來配置日誌的方式,不僅對於JSON和/或YAML的使用者來說,日誌將更容易配置,而且對於自定義配置方法的使用者來說也更容易配置,因為它提供了一種描述所需配置的通用格式。

當前基於ConfigParser的配置系統的另一個缺點是它不支援增量配置:新的配置會完全替換現有配置。儘管在多執行緒環境中很難提供增量配置的完全靈活性,但新的配置機制將允許提供對增量配置的有限支援。

規範

該規範由兩部分組成:API 和用於傳遞配置資訊的字典格式(即它必須符合的模式)。

命名

從歷史上看,日誌包一直不符合 PEP 8。將來,這將透過更改包中的方法和函式名稱來糾正,以符合 PEP 8。然而,為了保持一致性,對API的擬議新增內容採用了與日誌當前使用的方案一致的命名方案。

API

logging.config 模組將新增以下內容

  • 一個名為 dictConfig() 的函式,它接受一個引數——包含配置的字典。如果在處理字典時出現錯誤,將引發異常。

可以自定義此API——請參閱API定製部分。增量配置在單獨的部分中介紹。

字典模式 - 概述

在詳細描述模式之前,值得提一下物件連線、對使用者定義物件的支援以及對外部和內部物件的訪問。

物件連線

該模式旨在描述一組日誌物件——記錄器、處理程式、格式化程式、過濾器——它們在物件圖中相互連線。因此,該模式需要表示物件之間的連線。例如,假設配置完成後,某個特定記錄器附加了某個特定處理程式。為了討論的目的,我們可以說記錄器代表連線的源,而處理程式代表連線的目的地。當然,在配置的物件中,這由記錄器持有處理程式的引用來表示。在配置字典中,這是透過為每個目標物件提供一個唯一標識它的ID,然後在源物件的配置中使用該ID來指示源和具有該ID的目標物件之間存在連線來完成的。

例如,考慮以下YAML片段

formatters:
  brief:
    # configuration for formatter with id 'brief' goes here
  precise:
    # configuration for formatter with id 'precise' goes here
handlers:
  h1: #This is an id
   # configuration of handler with id 'h1' goes here
   formatter: brief
  h2: #This is another id
   # configuration of handler with id 'h2' goes here
   formatter: precise
loggers:
  foo.bar.baz:
    # other configuration for logger 'foo.bar.baz'
    handlers: [h1, h2]

(注意:本文件將使用YAML,因為它比等效的Python原始碼形式的字典更具可讀性。)

記錄器(logger)的ID是用於透過程式設計方式獲取這些記錄器引用的記錄器名稱,例如 foo.bar.baz。格式化程式(Formatter)和過濾器(Filter)的ID可以是任何字串值(例如上面提到的 brief, precise),它們是臨時的,因為它們僅在處理配置字典時有意義,用於確定物件之間的連線,並且在配置呼叫完成後不會持久化到任何地方。

處理程式ID將得到特殊處理,請參閱下面的處理程式ID部分。

上述程式碼片段表示名為 foo.bar.baz 的記錄器應該附加兩個處理程式,這兩個處理程式由處理程式ID h1h2 描述。h1 的格式化程式由ID brief 描述,h2 的格式化程式由ID precise 描述。

使用者定義物件

該模式應支援處理程式、過濾器和格式化程式的使用者定義物件。(記錄器不需要為不同的例項擁有不同的型別,因此在配置中不支援使用者定義的記錄器類。)

要配置的物件通常由詳細說明其配置的字典來描述。在某些地方,日誌系統將能夠根據上下文推斷出如何例項化物件,但是當要例項化使用者定義的物件時,系統將不知道如何執行此操作。為了提供使用者定義物件例項化的完全靈活性,使用者將需要提供一個“工廠”——一個可呼叫物件,它以配置字典作為引數被呼叫並返回例項化的物件。這將透過特殊鍵 '()' 下提供工廠的絕對匯入路徑來發出訊號。這是一個具體示例

formatters:
  brief:
    format: '%(message)s'
  default:
    format: '%(asctime)s %(levelname)-8s %(name)-15s %(message)s'
    datefmt: '%Y-%m-%d %H:%M:%S'
  custom:
      (): my.package.customFormatterFactory
      bar: baz
      spam: 99.9
      answer: 42

上述YAML程式碼片段定義了三個格式化程式。第一個,ID為 brief,是一個標準的 logging.Formatter 例項,具有指定的格式字串。第二個,ID為 default,具有較長的格式,並明確定義了時間格式,將生成一個使用這兩個格式字串初始化的 logging.Formatter。以Python原始碼形式顯示,briefdefault 格式化程式的配置子字典分別為

{
  'format' : '%(message)s'
}

{
  'format' : '%(asctime)s %(levelname)-8s %(name)-15s %(message)s',
  'datefmt' : '%Y-%m-%d %H:%M:%S'
}

因此,由於這些字典不包含特殊鍵 '()',因此從上下文推斷出例項化:結果,建立了標準的 logging.Formatter 例項。第三個格式化程式(ID 為 custom)的配置子字典為

{
  '()' : 'my.package.customFormatterFactory',
  'bar' : 'baz',
  'spam' : 99.9,
  'answer' : 42
}

它包含特殊鍵 '()',這意味著需要使用者定義的例項化。在這種情況下,將使用指定的工廠可呼叫物件。如果它是一個實際的可呼叫物件,它將直接使用——否則,如果指定一個字串(如示例所示),實際的可呼叫物件將使用正常的匯入機制定位。可呼叫物件將以配置子字典中的*其餘*項作為關鍵字引數進行呼叫。在上面的示例中,ID 為 custom 的格式化程式將被假定為由以下呼叫返回

my.package.customFormatterFactory(bar='baz', spam=99.9, answer=42)

'()' 被用作特殊鍵,因為它不是一個有效的關鍵字引數名稱,因此不會與呼叫中使用的關鍵字引數名稱衝突。'()' 也作為一個助記符,表示相應的值是一個可呼叫物件。

訪問外部物件

有時配置需要引用配置之外的物件,例如 sys.stderr。如果配置字典是使用Python程式碼構建的,那麼這很簡單,但是當配置透過文字檔案(例如JSON,YAML)提供時會出現問題。在文字檔案中,沒有標準的方法來區分 sys.stderr 和字面字串 'sys.stderr'。為了方便這種區分,配置系統將在字串值中查詢某些特殊字首並對其進行特殊處理。例如,如果字面字串 'ext://sys.stderr' 作為配置中的值提供,那麼 ext:// 將被剝離,值的其餘部分將使用正常的匯入機制進行處理。

此類字首的處理將以類似於協議處理的方式進行:將有一個通用機制來查詢與正則表示式 ^(?P<prefix>[a-z]+)://(?P<suffix>.*)$ 匹配的字首,如果 prefix 被識別,則 suffix 將以依賴於字首的方式進行處理,處理結果將替換字串值。如果字首未被識別,則字串值將保持不變。

該實現將提供一組標準字首,例如 ext://,但可以完全停用該機制,或為特殊處理提供額外或不同的字首。

訪問內部物件

除了外部物件,有時還需要引用配置中的物件。配置系統將隱式地為它知道的事物完成此操作。例如,記錄器或處理程式中 level 的字串值 'DEBUG' 將自動轉換為值 logging.DEBUG,並且 handlersfiltersformatter 條目將接受一個物件ID並解析為相應的目標物件。

然而,對於日誌系統未知的使用者定義物件,需要提供一個更通用的機制。例如,以 logging.handlers.MemoryHandler 例項為例,它接受一個 target,該 target 是另一個要委託到的處理程式。由於系統已經知道這個類,那麼在配置中,給定的 target 只需要是相關目標處理程式的物件ID,系統就會從ID解析出處理程式。但是,如果使用者定義了一個具有 alternate 處理程式的 my.package.MyHandler,配置系統將不知道 alternate 指的是一個處理程式。為了解決這個問題,將提供一個通用的解析系統,允許使用者指定

handlers:
  file:
    # configuration of file handler goes here

  custom:
    (): my.package.MyHandler
    alternate: cfg://handlers.file

字面字串 'cfg://handlers.file' 將以與帶有 ext:// 字首的字串類似的方式進行解析,但會在配置本身而非匯入名稱空間中查詢。該機制將允許透過點或索引進行訪問,其方式類似於 str.format 所提供的方式。因此,給定以下程式碼片段

handlers:
  email:
    class: logging.handlers.SMTPHandler
    mailhost: localhost
    fromaddr: my_app@domain.tld
    toaddrs:
      - support_team@domain.tld
      - dev_team@domain.tld
    subject: Houston, we have a problem.

在配置中,字串 'cfg://handlers' 將解析為鍵為 handlers 的字典,字串 'cfg://handlers.email' 將解析為 handlers 字典中鍵為 email 的字典,依此類推。字串 'cfg://handlers.email.toaddrs[1]' 將解析為 'dev_team.domain.tld',字串 'cfg://handlers.email.toaddrs[0]' 將解析為值 'support_team@domain.tld'subject 值可以透過 'cfg://handlers.email.subject' 或等效地 'cfg://handlers.email[subject]' 進行訪問。後一種形式僅在鍵包含空格或非字母數字字元時才需要使用。如果索引值僅由十進位制數字組成,將嘗試使用相應的整數值進行訪問,如果失敗則回退到字串值。

給定字串 cfg://handlers.myhandler.mykey.123,它將解析為 config_dict['handlers']['myhandler']['mykey']['123']。如果字串指定為 cfg://handlers.myhandler.mykey[123],系統將嘗試從 config_dict['handlers']['myhandler']['mykey'][123] 中檢索值,如果失敗則回退到 config_dict['handlers']['myhandler']['mykey']['123']

處理程式ID

一些特定的日誌配置需要使用處理程式級別才能達到預期的效果。但是,與始終可以透過其名稱識別的日誌記錄器不同,處理程式沒有持久控制代碼,無法透過增量配置呼叫更改級別。

因此,本PEP提議為處理程式新增一個可選的 name 屬性。如果使用,這將向一個字典中新增一個條目,該字典將名稱對映到處理程式。(當處理程式關閉時,該條目將被刪除。)當進行增量配置呼叫時,將在該字典中查詢處理程式,以根據配置中的值設定處理程式級別。有關更多詳細資訊,請參閱增量配置部分。

理論上,這種“持久名稱”功能也可以用於過濾器和格式化器。然而,目前沒有強有力的理由來證明能夠增量配置這些功能。基於實用性勝過純粹性的原則,只有處理程式將擁有這個新的 name 屬性。處理程式在配置中的ID將成為它的 name

處理程式名稱查詢字典僅供配置使用,不會成為包公共 API 的一部分。

字典模式 - 細節

傳遞給 dictConfig() 的字典必須包含以下鍵

  • version - 設定為一個表示模式版本的整數值。目前唯一有效的值是 1,但擁有此鍵允許模式演進,同時仍然保持向後相容性。

所有其他鍵都是可選的,但如果存在,將按以下描述進行解釋。在以下所有提及“配置字典”的情況下,都會檢查特殊鍵 '()',以檢視是否需要自定義例項化。如果需要,則使用上述機制進行例項化;否則,根據上下文確定如何例項化。

  • formatters - 相應的值將是一個字典,其中每個鍵是格式化程式ID,每個值是一個描述如何配置相應Formatter例項的字典。

    配置字典中會查詢鍵 formatdatefmt (預設為 None),這些鍵用於構造 logging.Formatter 例項。

  • filters - 相應的值將是一個字典,其中每個鍵是過濾器ID,每個值是一個描述如何配置相應Filter例項的字典。

    配置字典中會查詢鍵 name(預設為空字串),並使用它來構造 logging.Filter 例項。

  • handlers - 相應的值將是一個字典,其中每個鍵是處理程式ID,每個值是一個描述如何配置相應Handler例項的字典。

    配置字典將查詢以下鍵

    • class(必填)。這是處理程式類的完全限定名。
    • level(可選)。處理程式的級別。
    • formatter(可選)。此處理程式的格式化程式ID。
    • filters(可選)。此處理程式的過濾器ID列表。

    所有*其他*鍵都作為關鍵字引數傳遞給處理程式的建構函式。例如,給定程式碼片段

    handlers:
      console:
        class : logging.StreamHandler
        formatter: brief
        level   : INFO
        filters: [allow_foo]
        stream  : ext://sys.stdout
      file:
        class : logging.handlers.RotatingFileHandler
        formatter: precise
        filename: logconfig.log
        maxBytes: 1024
        backupCount: 3
    

    ID 為 console 的處理程式被例項化為 logging.StreamHandler,使用 sys.stdout 作為底層流。ID 為 file 的處理程式被例項化為 logging.handlers.RotatingFileHandler,其關鍵字引數為 filename='logconfig.log', maxBytes=1024, backupCount=3

  • loggers - 相應的值將是一個字典,其中每個鍵是記錄器名稱,每個值是一個描述如何配置相應Logger例項的字典。

    配置字典將查詢以下鍵

    • level(可選)。記錄器的級別。
    • propagate(可選)。記錄器的傳播設定。
    • filters(可選)。此記錄器的過濾器ID列表。
    • handlers(可選)。此記錄器的處理程式ID列表。

    指定的記錄器將根據指定的級別、傳播、過濾器和處理程式進行配置。

  • root - 這將是根記錄器的配置。配置的處理方式與任何記錄器相同,但 propagate 設定不適用。
  • incremental - 配置是否應被解釋為對現有配置的增量。該值預設為 False,這意味著指定的配置將替換現有配置,並採用與現有 fileConfig() API 相同的語義。

    如果指定的值為 True,則配置將按照下面增量配置一節中的描述進行處理。

  • disable_existing_loggers - 是否停用任何現有記錄器。此設定與 fileConfig() 中同名引數相對應。如果缺失,此引數預設為 True。如果 incrementalTrue,則此值將被忽略。

一個工作示例

以下是一個實際可用的YAML格式配置(除了電子郵件地址是虛構的)

formatters:
  brief:
    format: '%(levelname)-8s: %(name)-15s: %(message)s'
  precise:
    format: '%(asctime)s %(name)-15s %(levelname)-8s %(message)s'
filters:
  allow_foo:
    name: foo
handlers:
  console:
    class : logging.StreamHandler
    formatter: brief
    level   : INFO
    stream  : ext://sys.stdout
    filters: [allow_foo]
  file:
    class : logging.handlers.RotatingFileHandler
    formatter: precise
    filename: logconfig.log
    maxBytes: 1024
    backupCount: 3
  debugfile:
    class : logging.FileHandler
    formatter: precise
    filename: logconfig-detail.log
    mode: a
  email:
    class: logging.handlers.SMTPHandler
    mailhost: localhost
    fromaddr: my_app@domain.tld
    toaddrs:
      - support_team@domain.tld
      - dev_team@domain.tld
    subject: Houston, we have a problem.
loggers:
  foo:
    level : ERROR
    handlers: [debugfile]
  spam:
    level : CRITICAL
    handlers: [debugfile]
    propagate: no
  bar.baz:
    level: WARNING
root:
  level     : DEBUG
  handlers  : [console, file]

增量配置

提供增量配置的完全靈活性很困難。例如,由於過濾器和格式化程式等物件是匿名的,一旦設定了配置,在增加配置時就無法引用這些匿名物件。

此外,一旦配置設定完成,在執行時任意更改記錄器、處理程式、過濾器、格式化程式的物件圖並沒有令人信服的理由;記錄器和處理程式的詳細程度可以透過設定級別(以及記錄器的傳播標誌)來控制。在多執行緒環境中以安全的方式任意更改物件圖是存在問題的;雖然並非不可能,但收益不值得增加實現的複雜性。

因此,當配置字典的 incremental 鍵存在且為 True 時,系統將完全忽略所有 formattersfilters 條目,並且只處理 handlers 條目中的 level 設定,以及 loggersroot 條目中的 levelpropagate 設定。

當然可以透過其他方式提供增量配置,例如讓 dictConfig() 接受一個預設為 Falseincremental 關鍵字引數。建議在配置字典中使用一個值的原因是,它允許將配置作為醃製字典透過網路傳送到套接字監聽器。因此,長期執行的應用程式的日誌詳細程度可以隨著時間改變,而無需停止和重新啟動應用程式。

注意:歡迎您根據實際經驗,就增量配置需求提供反饋。

API定製

基本的 dictConfig() API 無法滿足所有用例。透過提供以下內容,將實現 API 的定製:

  • 一個名為 DictConfigurator 的類,其建構函式接受用於配置的字典,並具有一個 configure() 方法。
  • 一個可呼叫物件,名為 dictConfigClass,它(預設情況下)將設定為 DictConfigurator。提供此物件是為了在需要時,可以將 DictConfigurator 替換為適當的使用者定義實現。

函式 dictConfig() 將呼叫 dictConfigClass,傳入指定的字典,然後呼叫返回物件上的 configure() 方法以實際使配置生效。

def dictConfig(config):
    dictConfigClass(config).configure()

這應該能滿足所有定製需求。例如,DictConfigurator 的子類可以在自己的 __init__() 中呼叫 DictConfigurator.__init__(),然後設定自定義字首,這些字首將在隨後的 configure() call 中使用。dictConfigClass 將繫結到子類,然後可以像在預設的、未定製的狀態下一樣呼叫 dictConfig()

套接字監聽器實現更改

現有的套接字監聽器實現將按如下方式修改:當收到配置訊息時,將嘗試使用json模組反序列化為字典。如果此步驟失敗,則訊息將被假定為fileConfig格式並照常處理。如果反序列化成功,則將呼叫 dictConfig() 來處理生成的字典。

配置錯誤

如果在配置過程中遇到錯誤,系統將引發一個帶有適當描述性訊息的 ValueErrorTypeErrorAttributeErrorImportError。以下是可能引發錯誤的條件列表(可能不完整)

  • 一個 level 既不是字串,也不是對應實際日誌級別的字串
  • 一個 propagate 值不是布林值
  • 沒有對應目標的ID
  • 在增量呼叫期間發現不存在的處理程式 ID
  • 無效的記錄器名稱
  • 無法解析為內部或外部物件

社群討論

該PEP已在python-dev和python-list上公佈。雖然討論不多,但這對於一個利基話題來說可能是意料之中的。

python-dev 上的討論帖子

https://mail.python.org/pipermail/python-dev/2009-October/092695.html https://mail.python.org/pipermail/python-dev/2009-October/092782.html https://mail.python.org/pipermail/python-dev/2009-October/093062.html

以及 python-list 上

https://mail.python.org/pipermail/python-list/2009-October/1223658.html https://mail.python.org/pipermail/python-list/2009-October/1224228.html

有一些支援該提案的評論,沒有反對整個提案的意見,以及一些關於具體細節的問題和反對意見。作者認為這些問題已透過對PEP的修改得到解決。

參考實現

更改的參考實現可作為模組 dictconfig.py 獲得,並在 test_dictconfig.py 中附帶單元測試,網址為

http://bitbucket.org/vinay.sajip/dictconfig

這包含了除套接字監聽器更改之外的所有功能。


來源:https://github.com/python/peps/blob/main/peps/pep-0391.rst

上次修改:2025-02-01 08:59:27 GMT