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