
如果你正在找一個(gè)Python日志庫既不想花一天時(shí)間配logging模塊又不希望輸出像默認(rèn)logging那樣干巴巴的只有一行文字acrilog包會是一個(gè)值得試一下的選擇。我最早注意到這個(gè)包是因?yàn)槟翘煜挛缗R時(shí)要接手一個(gè)寫了一半的爬蟲項(xiàng)目里面全是print和散落的日志壓根沒辦法定位任務(wù)掛在哪一步。換上acrilog之后結(jié)構(gòu)化輸出、顏色區(qū)分、不同級別過濾這些事都變簡單了整個(gè)排錯(cuò)過程舒服不少。這篇文章就把它的語法、常用參數(shù)和實(shí)際項(xiàng)目里的落地用法梳理一遍適合初學(xué)Python的讀者也可以給已經(jīng)在用logging想換換手感的人做參考。1. acrilog包到底是干什么的1.1 為什么不用print也不用默認(rèn)logging很多人寫腳本都喜歡用print調(diào)試這里打印一下那里打印一下好像挺方便。但項(xiàng)目一旦超過幾百行或者在后臺跑定時(shí)任務(wù)print的弊端就暴露得很明顯沒有時(shí)間戳、沒有日志級別、不能統(tǒng)一開關(guān)、不好做文件輸出更別提給日志染上顏色來區(qū)分錯(cuò)誤了。Python標(biāo)準(zhǔn)庫的logging功能其實(shí)很強(qiáng)但默認(rèn)配置特別勸退新手。你需要記憶Handler、Formatter、Filter這些組件的用法還要搞清楚Logger、Handler和Propagate之間的關(guān)系。每次換項(xiàng)目都要重新寫一遍配置代碼復(fù)制來復(fù)制去稍不小心就會遇到日志重復(fù)輸出、等級不生效這種玄學(xué)問題。我在很多教程里看到最后直接勸退。acrilog這個(gè)包做的事情很簡單把logging常用的配置壓縮成幾個(gè)參數(shù)讓日志系統(tǒng)開箱即用。它不是要取代標(biāo)準(zhǔn)庫而是把底層細(xì)節(jié)包裝得更好看一點(diǎn)。你用acrilog本質(zhì)上還是在用logging的能力只是不再需要從零搭配置了。1.2 acrilog的設(shè)計(jì)思路和核心價(jià)值第一次看這個(gè)包的結(jié)構(gòu)我感覺它最明顯的特點(diǎn)是把“日志輸出”拆成了兩個(gè)層面一個(gè)是給自己的開發(fā)調(diào)試看的另一個(gè)是給業(yè)務(wù)分析用的。開發(fā)時(shí)你希望看到帶顏色、帶模塊名、帶行號的信息上線后你希望輸出一段能被日志收集工具直接解析的JSON。acrilog恰恰允許你通過參數(shù)切換這兩種模式不用改業(yè)務(wù)代碼只在初始化Logger的時(shí)候調(diào)整配置就行。我之前在多個(gè)小項(xiàng)目里試過用它整體感受是省心。它解決的核心問題有三個(gè)第一格式化日志時(shí)間、級別、調(diào)用來源這些重復(fù)勞動第二提供更直觀的API比如直接傳ctx上下文信息不用手動拼字符串第三保留標(biāo)準(zhǔn)庫logging的Handler體系所以原本你會用RotatingFileHandler、StreamHandler這些它也都支持。適合用它的人我覺得大概是兩類。一類是剛學(xué)Python不久想快速給腳本加正規(guī)日志但又不想先啃一遍logging源碼的初學(xué)者另一類是像我這樣已經(jīng)寫了不少項(xiàng)目但希望日志模塊的樣板代碼越少越好的開發(fā)者。2. 安裝與基礎(chǔ)語法先跑起來2.1 環(huán)境準(zhǔn)備和安裝步驟安裝沒什么特別的走pip就行。如果你已經(jīng)在虛擬環(huán)境里直接執(zhí)行下面這條命令pip install acrilog如果你用的是比較新的Python環(huán)境有些依賴包可能需要編譯比如帶顏色輸出的平臺相關(guān)模塊。萬一遇到安裝失敗先升級一下pip和setuptools再做嘗試python -m pip install --upgrade pip setuptools wheel pip install acrilog我在macOS和Linux系統(tǒng)上都裝過整個(gè)過程沒碰到特別大的問題。Windows環(huán)境我沒深入測過但考慮到它有顏色輸出功能有條件的話建議在終端里先跑一條簡單的日志試試確認(rèn)不是所有日志都輸出成亂碼。裝完之后可以看一下包的版本和主要對象import acrilog print(acrilog.__version__) print(dir(acrilog))如果正常打印出版本號和函數(shù)列表說明安裝沒問題。2.2 三行代碼上手先跑起來我非常喜歡這個(gè)包的入門方式因?yàn)樗娴目梢宰龅饺写a出日志import acrilog logger acrilog.getLogger(demo) logger.info(hello acrilog)跑下來你會看到終端里不僅有時(shí)間、日志級別、Logger名還有帶顏色的輸出。準(zhǔn)確顏色取決于你的終端是否支持ANSI轉(zhuǎn)義序列VSCode終端和macOS的Terminal一般都可以。默認(rèn)配置下acrilog.getLogger()會返回一個(gè)標(biāo)準(zhǔn)Logger所以函數(shù)名、括號參數(shù)這些語法和logging.getLogger的使用習(xí)慣是接近的。你不需要在每次調(diào)用時(shí)都指定格式Logger內(nèi)部已經(jīng)幫你配好了默認(rèn)Formatter。這條最基礎(chǔ)的用法適合單獨(dú)腳本或者調(diào)試階段。你只要保證在模塊頂層初始化Logger后面所有地方都能import到同一個(gè)logger實(shí)例。2.3 五種日志級別怎么選acrilog保留了 logging 的標(biāo)準(zhǔn)級別從低到高分別是DEBUG、INFO、WARNING、ERROR、CRITICAL。每個(gè)級別對應(yīng)一個(gè)同名方法調(diào)用方式跟標(biāo)準(zhǔn)庫一致logger.debug(debug message) logger.info(info message) logger.warning(warning message) logger.error(error message) logger.critical(critical message)不同級別適合不同場合。在我的習(xí)慣里DEBUG級別我用來記錄變量值、進(jìn)入某個(gè)函數(shù)、連接狀態(tài)這類細(xì)節(jié)INFO級別記錄程序的主要流程比如任務(wù)開始、任務(wù)結(jié)束、請求總量WARNING記錄可恢復(fù)的問題比如接口超時(shí)重試ERROR和CRITICAL分別對應(yīng)單次失敗和程序無法繼續(xù)運(yùn)行的情況。初始Logger默認(rèn)顯示級別是INFO所以DEBUG日志不會打出來。要讓它顯示DEBUG級詳情可以在構(gòu)造時(shí)把level參數(shù)設(shè)為DEBUGlogger acrilog.getLogger(debug_demo, levelDEBUG)如果你只想給某個(gè)業(yè)務(wù)模塊開啟更細(xì)的日志另外建一個(gè)帶獨(dú)立級別的Logger會更方便這樣不會把其他模塊的信息刷得鋪天蓋地。3. 核心參數(shù)解析到底有哪些關(guān)鍵配置3.1 高頻參數(shù)對照表如果只是無腦用默認(rèn)配置其實(shí)已經(jīng)比print好用了。但要發(fā)揮這個(gè)包的價(jià)值還是要看懂幾個(gè)核心參數(shù)。下面這個(gè)表我按自己使用的頻率整理出來不一定包含包的全部參數(shù)但覆蓋面已經(jīng)能應(yīng)對大多數(shù)項(xiàng)目。參數(shù)名類型默認(rèn)值作用namestr必傳Logger的名稱位置參數(shù)levelstr/intINFO日志級別控制輸出下限fmtstr內(nèi)置模板日志輸出格式模板use_colorsboolTrue是否啟用顏色輸出json_outputboolFalse是否輸出為JSON格式handlerslistNone自定義Handler列表propagateboolFalse是否向父Logger傳播日志ctxdictNone綁定的默認(rèn)上下文信息file_pathstrNone直接把日志寫入文件file_modestra寫文件時(shí)的模式初次接觸的人最容易忽略的是propagate參數(shù)。因?yàn)閍crilog內(nèi)部創(chuàng)建Logger后會默認(rèn)把propagate設(shè)為False避免日志在根Logger上再打印一次造成重復(fù)。如果你自己額外加了一個(gè)根Logger配置就要特別注意這個(gè)行為否則可能要么沒日志要么日志出現(xiàn)兩遍。3.2 自定義輸出格式和JSON格式fmt參數(shù)是控制日志模板的核心。它和logging的format概念類似但acrilog做了一點(diǎn)簡化。你可以直接寫常規(guī)的Formatter格式字符串logger acrilog.getLogger( custom_fmt, fmt%(asctime)s | %(levelname)s | %(name)s | %(message)s )這樣每條日志會按照模板里的順序輸出。acrilog內(nèi)部還是會用標(biāo)準(zhǔn)庫的%格式語法所以你能用的字段名和logging是一致的。比如:%(asctime)s 時(shí)間%(levelname)s 級別%(name)s Logger名%(message)s 日志正文%(module)s 輸出日志的模塊名%(lineno)d 行號如果你不想記那么多格式符號直接把logfmt或者純文本串塞進(jìn)去也行關(guān)鍵是讓它包含你排錯(cuò)需要的信息。我自己排錯(cuò)時(shí)最喜歡帶模塊和行號所以至少會在fmt里保留%(name)s和%(lineno)d。在微服務(wù)或者需要接入日志平臺的場景下JSON輸出會更有用。設(shè)置json_outputTrue每條日志會以單行JSON形式輸出這樣ELK、Loki或者自建日志平臺解析起來更省事logger acrilog.getLogger( json_demo, json_outputTrue, fmt%(asctime)s | %(levelname)s | %(message)s )此時(shí)日志整體會被封裝成一個(gè)JSON對象同時(shí)帶上level、time、message這些字段。如果你把context傳進(jìn)去它也會成為JSON里的一個(gè)獨(dú)立字段。這個(gè)模式對機(jī)器讀取非常友好人工閱讀雖然有少許犧牲但勝在結(jié)構(gòu)清晰。3.3 上下文綁定與trace_id追蹤我最喜歡acrilog的一點(diǎn)是它設(shè)計(jì)了上下文信息入口。日常寫業(yè)務(wù)日志時(shí)我們經(jīng)常需要把用戶ID、訂單號、請求ID之類的信息一起打出來。用print或者原生logging時(shí)最容易的做法是拼字符串logger.info(user %s pay %s amount %s, user_id, order_id, amount)拼是能拼但日志一多字段順序容易亂后端要解析也不方便。acrilog可以在創(chuàng)建Logger時(shí)綁定統(tǒng)一的ctx字典也可以在一次日志調(diào)用里覆蓋logger acrilog.getLogger( app_logger, ctx{app: web-server, env: prod} ) logger.info(payment success, ctx{user_id: 12345, order_id: A10086})這樣輸出的內(nèi)容里會附帶一份上下文信息不管是排查單個(gè)用戶的問題還是做指標(biāo)統(tǒng)計(jì)都比純文本搜索方便得多。更進(jìn)一步你可以把請求進(jìn)入到網(wǎng)關(guān)時(shí)生成的trace_id綁定到ctx里這樣一整條調(diào)用鏈上的日志都能通過同一個(gè)trace_id串起來logger.info(request started, ctx{trace_id: trace_id}) logger.info(db query done, ctx{trace_id: trace_id})如果是在異步框架或者多線程場景里手動傳trace_id有點(diǎn)煩。通常我會在協(xié)程或線程入口處拿一下上下文里的trace_id然后把它塞到Logger的ctx中保證整段處理過程里trace_id始終一致。4. 實(shí)際應(yīng)用案例從接口日志到多線程任務(wù)4.1 案例1FastAPI接口日志在Web開發(fā)里日志不止是給程序員調(diào)試用更多時(shí)候要記錄請求來源、接口耗時(shí)、響應(yīng)狀態(tài)和異常信息。我用FastAPI搭服務(wù)的時(shí)候習(xí)慣在中間件里統(tǒng)一記錄日志這樣不用每個(gè)路由重復(fù)寫。下面是一個(gè)很典型的中間件日志場景import time from fastapi import FastAPI, Request import acrilog logger acrilog.getLogger( api_access, levelINFO, json_outputTrue, ctx{app: shop-api} ) app FastAPI() app.middleware(http) async def access_log(request: Request, call_next): start time.time() try: response await call_next(request) logger.info( request handled, ctx{ method: request.method, path: request.url.path, status: response.status_code, duration_ms: round((time.time() - start) * 1000, 2) } ) return response except Exception as exc: logger.error( request failed, ctx{ method: request.method, path: request.url.path, exception: repr(exc) } ) raise這個(gè)例子用到了json_output和ctx每個(gè)路由的訪問記錄都會被包裝成一行JSON里面帶上了請求方法和狀態(tài)碼。后來我把日志接入日志收集平臺解析這批JSON字段時(shí)基本沒做額外清洗。如果你團(tuán)隊(duì)習(xí)慣看純文本日志也可以把json_output關(guān)掉但ctx還是會以keyvalue的形式拼接在message后面信息不丟只是展示方式不同。4.2 案例2多線程任務(wù)跟蹤用print調(diào)試多線程程序是最痛苦的因?yàn)槎鄠€(gè)線程的日志會穿插在一起分不清先后順序也不知道每行日志來自哪個(gè)線程。acrilog的context機(jī)制在這里就能發(fā)揮很好的作用。拿一個(gè)簡單爬蟲任務(wù)舉例我用ThreadPoolExecutor同時(shí)抓取多個(gè)頁面每個(gè)線程需要一個(gè)唯一ID來關(guān)聯(lián)日志import threading import time from concurrent.futures import ThreadPoolExecutor import acrilog logger acrilog.getLogger(crawler, levelINFO, use_colorsFalse) def fetch_page(page_id): thread_name threading.current_thread().name logger.info(start fetch, ctx{thread: thread_name, page_id: page_id}) time.sleep(0.5) if page_id % 3 0: logger.warning(timeout, retry, ctx{thread: thread_name, page_id: page_id}) else: logger.info(finish fetch, ctx{thread: thread_name, page_id: page_id}) with ThreadPoolExecutor(max_workers4) as pool: for pid in range(10): pool.submit(fetch_page, pid)運(yùn)行后日志里的ctx會帶上thread和page_id兩個(gè)字段。即使多個(gè)線程交錯(cuò)打印只要過濾page_id或者thread字段就能把一趟任務(wù)的生命周期完全還原出來。這在排查并發(fā)請求超時(shí)或資源競爭問題的時(shí)候特別管用。4.3 案例3日志落盤與滾動開發(fā)環(huán)境看終端生產(chǎn)環(huán)境要落盤這是日志的基本要求。acrilog通過handlers參數(shù)和file_path參數(shù)可以很方便地接入標(biāo)準(zhǔn)libgging的文件Handler。最簡單的做法是直接把日志寫到指定文件logger acrilog.getLogger( file_demo, file_pathlogs/app.log, file_modea, levelINFO )但這個(gè)寫法只適合日志量小的場景。大量日志會撐爆磁盤所以生產(chǎn)環(huán)境我一般會加上RotatingFileHandler按文件大小滾動切分import logging from logging.handlers import RotatingFileHandler import acrilog file_handler RotatingFileHandler( logs/app.log, maxBytes10 * 1024 * 1024, backupCount5, encodingutf-8 ) file_handler.setLevel(INFO) logger acrilog.getLogger( file_demo, levelINFO, handlers[file_handler] )這樣單個(gè)日志文件達(dá)到10MB就會自動切一個(gè)備份最多保留5個(gè)歷史文件。手動的Handler和acrilog自帶的配置并不沖突它會把這個(gè)Handler掛到Logger上再用自己的Formatter去格式化輸出。如果你希望文件里的格式和終端里的格式不一樣后續(xù)自定義一個(gè)Formatter覆蓋到handler上就行。對異常信息我建議用logger.exception代替logger.error。exception方法會自動把當(dāng)前異常堆棧打出來排錯(cuò)省很多時(shí)間。不過example里要展示的話我還是用error加exc_infoTrue更通用try: risky_call() except Exception: logger.error(something broken, exc_infoTrue)5. 常見問題與排查技巧實(shí)錄5.1 日志重復(fù)輸出原因和解決思路這個(gè)問題幾乎所有Python日志庫都會遇到acrilog也不能免俗。日志重復(fù)輸出通常有兩個(gè)來源一個(gè)是自己又一次手動加了StreamHandler到Logger上另一個(gè)是Logger的父級也有Handler而且propagate被設(shè)為了True。acrilog默認(rèn)propagate是False所以如果你只是單純用getLogger不會莫名其妙重復(fù)。怕就怕你既用了acrilog又額外給同一個(gè)Logger添加Handler或者在項(xiàng)目里混用了原生logging根配置。我在一個(gè)老項(xiàng)目里就見過輸出控制臺的日志變成了兩行一模一樣的排查半天才發(fā)現(xiàn)是項(xiàng)目入口處配置了一次日志又在插件代碼里重復(fù)配置。解決方式也很直白檢查Logger.handlers如果列表里已經(jīng)有想要的Handler就不要再次addHandler了。要看得直觀一點(diǎn)可以在啟動時(shí)打印一下logger acrilog.getLogger(probe) print(logger.handlers)如果看到兩個(gè)StreamHandler刪掉一個(gè)就好。再有就是確認(rèn)propagate參數(shù)必要時(shí)直接設(shè)成False。5.2 中文亂碼或者輸出顏色在Windows下不對顏色輸出和編碼問題跨平臺項(xiàng)目里基本繞不開。Linux和macOS終端對ANSI顏色支持得比較好Windows上如果是舊版cmd或者PowerShell顏色可能顯示成[32m之類的轉(zhuǎn)義字符看起來非常難受。最簡單的處理方式是在acrilog初始化時(shí)關(guān)掉顏色logger acrilog.getLogger(win_demo, use_colorsFalse)這樣確實(shí)失去了一點(diǎn)視覺上的級別區(qū)分但至少信息不會亂掉。在Windows上還經(jīng)常遇到中文亂碼特別是日志寫入文件的時(shí)候。寫入文件如果沒指定encoding默認(rèn)可能是系統(tǒng)本地編碼有些環(huán)境不支持中文導(dǎo)致亂碼。我用RotatingFileHandler時(shí)都會帶上encodingutf-8參數(shù)基本能避免這個(gè)問題。如果終端里直接print中文亂碼大概率不是acrilog的問題而是終端本身的編碼設(shè)置。檢查一下終端的編碼改成UTF-8或者把環(huán)境變量PYTHONIOENCODING設(shè)為utf-8再試。5.3 API版本更新帶來的兼容性問題在使用第三方庫時(shí)最讓人頭大的就是版本升級后參數(shù)發(fā)生變化。我最早接觸acrilog時(shí)很多配置都寫在getLogger函數(shù)的關(guān)鍵字參數(shù)里后來版本調(diào)整后可能是統(tǒng)一的配置類接管了部分參數(shù)。如果你在升級包之后發(fā)現(xiàn)原來能跑的代碼忽然報(bào)錯(cuò)多半就是參數(shù)名變了。遇到這種情況我一般的排查套路是三步運(yùn)行help(acrilog.getLogger)看一下當(dāng)前版本支持哪些參數(shù)。運(yùn)行print(acrilog.__version__)記錄版本號。把舊代碼里的LEVEL_INFO替換成新參數(shù)等操作逐步調(diào)整。這里提醒一句如果項(xiàng)目的核心邏輯依賴某個(gè)特定版本的acrilog別急著追新版本。可以先在requirements.txt里鎖住版本等兼容性確認(rèn)后再升級。我吃過太多莫名奇妙的lib升級虧日志這種橫切面組件尤其要穩(wěn)。6. 我的幾個(gè)實(shí)際使用心得寫到這里最后分享幾個(gè)我在真實(shí)項(xiàng)目里摸索出來、容易踩坑但又不常寫進(jìn)文檔里的經(jīng)驗(yàn)。第一不要在模塊底層到處創(chuàng)建acrilog.getLogger。每個(gè)Python模塊都新建一個(gè)Logger代碼是能跑但后期做日志級別調(diào)整時(shí)你得改很多地方。我現(xiàn)在的習(xí)慣是項(xiàng)目里有一個(gè)logger.py把項(xiàng)目中需要用到的Logger統(tǒng)一創(chuàng)建好其他模塊直接從這里import。這樣所有Logger配置都集中在同一處要開DEBUG的時(shí)候改一個(gè)文件就夠了。第二ctx字段盡量用小寫加下劃線。雖然包本身沒有強(qiáng)制但日志平臺對JSON字段命名通常有規(guī)范比如user_id、trace_id這種解析時(shí)才能保持統(tǒng)一。如果一會兒駝峰一會兒下劃線后續(xù)做告警和指標(biāo)會非常痛苦。第三日志格式里一定要有請求或業(yè)務(wù)標(biāo)識。無論是trace_id還是order_id只要出現(xiàn)異常你能靠它把散落的日志連起來。沒有這個(gè)字段日志越多越亂最后變成一堆難以定位的噪音。第四建議在測試環(huán)境先把日志跑上一兩個(gè)小時(shí)。日志系統(tǒng)看起來簡單但真正到了高并發(fā)、告警接入、磁盤配額這些場景很多小問題才會暴露出來。提前讓日志在真實(shí)環(huán)境里轉(zhuǎn)起來比上線后臨時(shí)修要好太多。我這里說的pre-production驗(yàn)證也就是看看文件增長速率、錯(cuò)誤日志是否完整、JSON是否能被正確解析這幾件事。我自己用了acrilog之后最直接的改變是日志代碼變短了排錯(cuò)效率提高了。以前寫一套logging配置要幾十行還要小心翼翼安排Handler現(xiàn)在大部分項(xiàng)目只需要初始化一個(gè)Logger然后在需要的地方調(diào)用就能持續(xù)得到整齊一致的日志。如果你也在找一個(gè)輕量又實(shí)用的Python日志方案不妨拿個(gè)十幾分鐘把a(bǔ)crilog的語法和參數(shù)試一遍尤其是ctx和json_output這兩個(gè)功能在真實(shí)項(xiàng)目里會逐漸顯出價(jià)值。