在軟體開發中,錯誤並非意外,而是必然。無論是邏輯異常、第三方 API 崩潰,還是無聲的流程阻塞,當系統失靈時,開發者的核心挑戰在於:如何從海量訊息中,秒速鎖定事故現場?
本文將深入探討 Django 的強大 Logging 機制,示範如何透過模組化設計與 Trace ID 追蹤,將混亂的程式執行過程轉化為透明、可控的事件紀錄,讓錯誤不再是難以追蹤的黑盒子。
一、log (日誌) 簡介
Django 的 log 整體架構分為四個核心元件:
| 元件 | 職責 | 譬喻 |
| Logger | 接收訊息的入口 | 郵局櫃檯 |
| Filter | 決定哪些訊息該通過 | 安全檢查員 |
| Handler | 決定訊息送到哪 (檔案/螢幕) | 運輸工具 |
| Formatter | 設定訊息的外觀格式 | 信件格式 |
- logger:日誌的入口點,定義名稱與等級(如 INFO, ERROR)
- handler:決定 Log 要送去哪裡(console、file、email、第三方服務)
- formatter:決定 Log 的格式(時間、模組、訊息等)
- filter:額外條件控制,例如只記某個模組的錯誤
其中 formatter 應該是跟我們看到的訊息最息息相關的設定。通常一條 Log 訊息的組成如下:
2025-09-08 14:26:29 | WARNING | django.request | trace_id=adaaf8b8-1345-4415-bfd2-c3640ae5a55a | Unauthorized: /api/analysis/rbac/report/
%(asctime)s | %(levelname)s | %(name)s | trace_id=%(trace_id)s | %(message)s
由左到右分別是:
- asctime:流程執行的時間
- levelname:流程的層級 (DEBUG、INFO…)
- name:流程從哪裡執行 (django.server)
- trace_id:流程的識別碼,用來判斷哪幾條訊息是屬於同一個流程的。這並不是 Django 預設欄位,所以後面我們要自己加上去。
- message:訊息的細節內容。
後面的則是自訂的欄位,可以在 settings.py 調整。
而在 log 訊息是有分層級的,各層級的說明大致如下:
| 等級 | 數字 | 名稱 | 說明 |
|---|---|---|---|
| DEBUG | 10 | 除錯資訊 | 開發階段的細節追蹤,例如 ORM 轉換的 SQL 語法、檔案操作流程等,開發者通常不會自訂,除非開啟 Debug 模式。 |
| INFO | 20 | 一般資訊 | 系統正常運作的紀錄,如收到 webhook 或 API 成功回應。適用於記錄正常流程的里程碑、稽核追蹤、系統健康檢查。 |
| WARNING | 30 | 警告 | 非致命但可能異常的狀況,如重試、過期 token、使用備援方案、沒有權限等。表示系統遇到異常但已處理,可用於記錄非預期行為。 |
| ERROR | 40 | 錯誤 | 發生錯誤但系統仍可繼續運作,如 API 呼叫失敗、驗證錯誤等。表示某個操作失敗,可能需要人工介入,可搭配例外處理程式(exception handler)使用。 |
| CRITICAL | 50 | 致命錯誤 | 系統無法繼續運作,如資料庫連線失敗、配置錯誤、系統崩潰。通常會觸發警報,應立即通知維運人員,可搭配警報系統(如 Sentry、Prometheus)使用。 |
接下來,我們要快速從零開始建立一個 Django 專案,然後一樣從零開始,試著在系統配置 Log 的設定。
二、快速建立一個 Django 專案
可以參考 如何用 Django Channels 與 Celery 建立可保存訊息的即時聊天系統 建立 Django 專案的說明,這邊就快速帶過指令:
- 建立專案資料夾:
mkdir <你要建立資料夾的路徑> - 安裝 uv
- Windows (Powershell):
irm https://astral.sh/uv/install.ps1 | iex - Mac/Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
- Windows (Powershell):
- 安裝最新版 Python:
uv python install - 建立一個 Python 專案:
uv init <專案名稱> - 執行
uv sync,自動產生專案的虛擬環境 - 執行 main.py:
uv run main.py,然後刪掉 main.py - 安裝 Django 相關套件:
uv add django - 建立 Django 專案結構:
uv run django-admin startproject <專案名> - Django 專案的外層和內層應該各有一個以
<專案名>命名的資料夾,將外層改名成 src,然後進入 src - 生成資料庫遷移檔案:
uv run manage.py makemigrations - 執行資料庫遷移:
uv run manage.py migrate - 設定 superuser:
uv run manage.py createsuperuser - 運行伺服器:
uv run manage.py runserver - 這時我們可以用剛才建立的 superuser 登入 admin 後台,網址是 localhost:8000/admin。
只要看到這個畫面,就可以確定 Django 專案建立成功了!

三、簡單加入一個功能
我們快速幫這個專案加入一個功能:可以讀取 zip 檔,並進行解壓縮和分類的 API。可以將程式碼複製貼上到你的專案,有興趣再自行研究,或是有機會再寫一篇文章說明:
- 首先建立一個 Django 應用程式,命名為 file:
uv run manage.py startapp file - 然後這個 API 是以 RESTful 風格設計的,所以要安裝 DRF (Django RESTful Framework)套件:
uv add djangorestframework - 按照註解的檔案路徑,將程式碼貼到對應的檔案,沒有檔案可以自行建立:
# /src/file/views.py
# 負責處理檔案上傳的 API 請求與回應。
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import UploadSerializer
from .services import FileProcessingService
class FileUploadView(APIView):
"""
提供 ZIP 檔案上傳與自動解壓功能的 View。
"""
def post(self, request, *args, **kwargs):
"""
處理 POST 請求,驗證 ZIP 檔案後調用服務進行解壓。
"""
serializer = UploadSerializer(data=request.data)
if serializer.is_valid():
uploaded_file = serializer.validated_data['file']
try:
FileProcessingService.process_zip(uploaded_file)
return Response(
{"message": "檔案上傳並解壓成功"},
status=status.HTTP_201_CREATED
)
except Exception as e:
return Response(
{"error": f"處理 ZIP 檔案時發生錯誤: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# /src/file/services.py
# 負責處理 ZIP 檔案解壓與檔案分類存儲的業務邏輯。
import os
import zipfile
import io
from abc import ABC, abstractmethod
from django.conf import settings
class StorageStrategy(ABC):
"""
檔案存儲策略的抽象基底類別。
"""
@abstractmethod
def get_storage_path(self, filename: str) -> str:
"""
根據檔名返回目標存儲路徑。
"""
pass
class JpgStorageStrategy(StorageStrategy):
"""
處理 JPG 檔案的存儲策略。
"""
def get_storage_path(self, filename: str) -> str:
return os.path.join(settings.MEDIA_ROOT, 'jpg', filename)
class PdfStorageStrategy(StorageStrategy):
"""
處理 PDF 檔案的存儲策略。
"""
def get_storage_path(self, filename: str) -> str:
return os.path.join(settings.MEDIA_ROOT, 'pdf', filename)
class PngStorageStrategy(StorageStrategy):
"""
處理 PNG 檔案的存儲策略。
"""
def get_storage_path(self, filename: str) -> str:
return os.path.join(settings.MEDIA_ROOT, 'png', filename)
class DefaultStorageStrategy(StorageStrategy):
"""
處理其他未知類型檔案的預設存儲策略。
"""
def get_storage_path(self, filename: str) -> str:
return os.path.join(settings.MEDIA_ROOT, 'others', filename)
class FileProcessingService:
"""
協調 ZIP 解壓與策略選取的服務類別。
"""
_strategies = {
'.jpg': JpgStorageStrategy(),
'.jpeg': JpgStorageStrategy(),
'.pdf': PdfStorageStrategy(),
'.png': PngStorageStrategy(),
}
@staticmethod
def process_zip(zip_file) -> None:
"""
解析 ZIP 檔案並根據副檔名分發至對應存儲路徑。
"""
with zipfile.ZipFile(zip_file, 'r') as zf:
for file_info in zf.infolist():
if file_info.is_dir():
continue
filename = os.path.basename(file_info.filename)
_, ext = os.path.splitext(filename.lower())
strategy = FileProcessingService._strategies.get(ext, DefaultStorageStrategy())
target_path = strategy.get_storage_path(filename)
# 確保目錄存在並寫入檔案
os.makedirs(os.path.dirname(target_path), exist_ok=True)
with zf.open(file_info) as source, open(target_path, 'wb') as target:
target.write(source.read())
# /src/file/serializers.py
# 負責驗證上傳的 ZIP 檔案。
from rest_framework.serializers import Serializer, FileField, ValidationError
class UploadSerializer(Serializer):
"""
處理 ZIP 檔案上傳驗證的序列化器。
"""
file = FileField(help_text="請上傳 ZIP 格式的壓縮檔")
def validate_file(self, value):
"""
驗證檔案是否為 zip 格式。
"""
if not value.name.endswith('.zip'):
raise ValidationError("僅支援 ZIP 格式檔案。")
return value
# /src/file/urls.py
# file APP 內部的路由。
from django.urls import path
from .views import FileUploadView
urlpatterns = [
path('upload/', FileUploadView.as_view(), name='file-upload'),
]
# /src/project_logs_demo/urls.py
# 系統的路由。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('file.urls')),
]
# /src/project_logs_demo/settings.py
# 系統設定檔
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'file',
]
...
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
- 貼上後,可以執行
uv run manage.py runserver啟動伺服器,在 Postman 測試這支 API 看看:- 欄位驗證:僅支援 ZIP 格式檔案
- 成功處理 ZIP 檔案

四、實戰進階:實作 Trace ID 與 Middleware 實現請求全鏈路追蹤
雖然是執行成功了,但執行過程中到底發生了什麼事,現在的我們並不曉得。儘管有對 zip 以外的檔案進行欄位驗證,但萬一之後在處理其他特殊類型檔案時,發生了預期之外的錯誤 (500 Internal Server Error),我們要怎麼知道是哪一個環節引發的錯誤?如果要檢查數天前是否有引發相同的錯誤,終端機上的紀錄是不會留著的,到時候要怎麼除錯?
留下可追溯的詳細報告不只是為了排錯,更能協助效能監控、安全稽核與系統維運。這就是 log 的重要性與價值。
一般來說,我們會在 Django 的程序接收到請求,交給 View 處理前,給這個請求一組具備唯一性的 Unique ID;目的是讓接下來的 log 產生前,可以加上這組 ID,我們就可以知道這條 log 是在哪個請求處理時產生的。
而在「程序接收到請求,交給 View 處理前」的這個階段叫做 “middleware” (中間層),我們可以在這一個階段對輸入的請求 (request) 和輸出的回應 (response) 進行格式處理,或是插入資料;因此我們這邊要做的,就是在 middleware 產生一組 Trace ID 到請求裡面,供後續 log 使用。
因此,我們要在 Django 的專案資料夾 (project_log_demo, 與 settings.py 同層) 建立兩個檔案:處理 Middleware 行為的 middleware.py 與 Trace ID 相關操作的 trace_id.py:
# /src/project_log_demo/trace_id.py
# 負責在日誌中注入與管理唯一追蹤 ID (Trace ID)。
import uuid
import logging
from asgiref.local import Local
# 使用 Local 存儲每個線程/協程的 Trace ID
_storage = Local()
def get_trace_id():
"""
獲取當前請求的 Trace ID,如果不存在則生成一個。
"""
return getattr(_storage, 'trace_id', 'no-trace-id')
def set_trace_id(trace_id=None):
"""
設定當前請求的 Trace ID。
"""
if trace_id is None:
trace_id = str(uuid.uuid4())
_storage.trace_id = trace_id
return trace_id
def clear_trace_id():
"""
清除當前請求的 Trace ID。
"""
if hasattr(_storage, 'trace_id'):
del _storage.trace_id
class TraceIDFilter(logging.Filter):
"""
日誌過濾器,將 Trace ID 注入到每筆日誌紀錄中。
"""
def filter(self, record):
record.trace_id = get_trace_id()
return True
# /src/project_log_demo/middleware.py
# 負責處理每個請求的 Trace ID 生命週期。
from .trace_id import set_trace_id, clear_trace_id
class TraceIDMiddleware:
"""
中介軟體,用於在請求開始時生成 Trace ID 並在結束時清除。
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 從請求標頭中嘗試獲取傳入的 trace_id,否則生成新的
trace_id = request.headers.get('X-Trace-ID')
trace_id = set_trace_id(trace_id)
response = self.get_response(request)
# 在回應標頭中附帶 Trace ID 以便追蹤
response['X-Trace-ID'] = trace_id
# 請求結束,清除存儲
clear_trace_id()
return response
在 Django 如果有添加 Middleware 的設定,要去 settings.py 註冊,就像新的 app 一樣;只是 app 是寫在 INSTALLED_APPS,Middleware 要寫在 MIDDLEWARE:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'project_log_demo.middleware.TraceIDMiddleware', # 這裡
...
]
現在,可以開始添加 log 的設定了。在 settings.py 輸入以下的內容:
# LOGGING Configuration
import sys
import os
from datetime import datetime
from colorlog import ColoredFormatter
# 建立 logs 目錄(依日期分資料夾)
today_str = datetime.now().strftime("%Y-%m-%d")
LOG_BASE_DIR = BASE_DIR / 'logs' / today_str
LOG_BASE_DIR.mkdir(parents=True, exist_ok=True)
# 設定 logs 訊息格式
LOG_FORMAT = "%(log_color)s%(asctime)s | %(levelname)s | %(name)s | trace_id=%(trace_id)s | %(message)s"
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'trace_id': {
'()': 'project_log_demo.trace_id.TraceIDFilter',
},
},
'formatters': {
'standard': {
'format': "%(asctime)s | %(levelname)s | %(name)s | trace_id=%(trace_id)s | %(message)s",
'datefmt': DATE_FORMAT
},
'colored': {
'()': ColoredFormatter,
'format': LOG_FORMAT,
'datefmt': DATE_FORMAT,
'log_colors': {
'DEBUG': 'blue',
'INFO': 'black,bg_green',
'WARNING': 'black,bg_yellow',
'ERROR': 'white,bg_red',
'CRITICAL': 'white,bg_purple',
},
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'colored',
'filters': ['trace_id'],
'stream': sys.stdout,
},
'system_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': LOG_BASE_DIR / 'system.log',
'when': 'midnight',
'backupCount': 30,
'encoding': 'utf-8',
'formatter': 'standard',
'filters': ['trace_id'],
},
'audit_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': LOG_BASE_DIR / 'audit.log',
'when': 'midnight',
'backupCount': 30,
'encoding': 'utf-8',
'formatter': 'standard',
'filters': ['trace_id'],
},
'debug_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': LOG_BASE_DIR / 'debug.log',
'when': 'midnight',
'backupCount': 30,
'encoding': 'utf-8',
'formatter': 'standard',
'filters': ['trace_id'],
'level': 'DEBUG',
},
},
'loggers': {
'django': {
'handlers': ['console', 'debug_file'],
'level': 'DEBUG',
'propagate': True,
},
'app.system': {
'handlers': ['console', 'system_file', 'debug_file'],
'level': 'INFO',
'propagate': False,
},
'audit': {
'handlers': ['console', 'audit_file'],
'level': 'INFO',
'propagate': False,
},
},
'root': {
'handlers': ['console', 'debug_file'],
'level': 'INFO',
},
}
我們逐一解析這些設定:
- 我們這邊想將 log 依照日期歸檔,並分成使用者操作 (audit)、系統流程 (system) 與 Django 自動產生 (debug) 三種類型。理想上的資料夾結構會長這樣:
src/logs > 2026-01-29 > audit.log | debug.log | system.log,所以 LOG_BASE_DIR 就是我們自己定義,要儲存 log 的日期資料夾。 - 我們想在 log 訊息自訂 Trace ID,所以要新增一個 LOG_FORMAT 定義訊息格式;而時間格式我們也想要自訂,所以也寫了 DATE_FORMAT。
接下來就進入 log 本身的設定了。可以看到除了 disable_existing_loggers 以外,最外層的四個 key 值,剛好就直接對應前面提到的四個核心:filter、formatter、handler 與 logger。
disable_existing_loggers是讓你決定在使用自訂的 log 設定時,要不要把 Django 內建的 log 設定停用?這邊我們選 false,因為我們只有一點點想自訂的配置,大部分還是要沿用 Django 的內建配置。filter:掌控了自訂的訊息欄位
由於我們自行添加了 trace_id,所以就要在 filter 說明這個欄位的管理方法寫在哪個檔案。formatteer:掌控了訊息的格式
standard 設定訊息最基礎的純文字格式
colored 用來將 log 訊息添加顏色。()指定使用的套件,寫入我們使用的ColoredFormatter;每一個 Log 層級的訊息要以什麼顏色顯示,就是在這邊設定。handler:掌控了訊息的輸出位置 (日誌的出口)
這邊我們定義了四個出口:console就是終端機螢幕(stdout),讓開發者當下可以直接看到。同時我們在 level 設定 DEBUG 以上的訊息才顯示 (就是所有 log 訊息的意思)、formatter 加上色彩設定、filter 加上 trace_id。system_file / audit_file / debug_file則是為了達到前面提到的分層設計,自行區隔的輸出模式。
除了輸出路徑 (filename) 以外,設計都非常相似。它們都使用了TimedRotatingFileHandler,並設定'when': 'midnight'與backupCount: 30,在每天午夜(midnight)會自動把舊的日誌改名備份,並開一個新檔案,且最多幫你留 30 天,避免硬碟爆掉。
logger:掌控了訊息的輸出來源 (日誌的入口)django是系統預設的 logger,會負責搬運 Django 框架本身的訊息(如啟動資訊、錯誤等)。門檻設為 DEBUG,所以再小的細節它都會抓。app.system和audit都是我們為了區隔來源而自訂的 logger;
前者負責搬運你寫的應用程式邏輯。門檻是 INFO,所以它會忽略微小的除錯資訊,只記錄重要動作。
後者專門處理「稽核」日誌(例如誰登入了、誰刪了資料)。這通常是非常重要且需要獨立存放的紀錄。root是「保底」的記錄器。如果某個程式沒被上面三個認領,就會由它統一處理。
最後,我們就可以在程式使用 log 訊息:
# /src/file/services.py
# 負責處理 ZIP 檔案解壓與檔案分類存儲的業務邏輯。
...
import logging
...
# 初始化 Loggers
logger = logging.getLogger('app.system')
audit_logger = logging.getLogger('audit')
...
class FileProcessingService:
"""
協調 ZIP 解壓與策略選取的服務類別。
"""
...
@staticmethod
def process_zip(zip_file) -> None:
"""
解析 ZIP 檔案並根據副檔名分發至對應存儲路徑。
"""
logger.info(f"開始處理 ZIP 檔案: {zip_file.name}")
with zipfile.ZipFile(zip_file, 'r') as zf:
for file_info in zf.infolist():
if file_info.is_dir():
continue
...
logger.debug(f"檔案 {filename} 副檔名為 {ext},選取策略: {strategy.__class__.__name__}")
# 確保目錄存在並寫入檔案
os.makedirs(os.path.dirname(target_path), exist_ok=True)
with zf.open(file_info) as source, open(target_path, 'wb') as target:
...
audit_logger.info(f"檔案已儲存至: {target_path}")
logger.info(f"ZIP 檔案處理完成: {zip_file.name}")
可以看到 log 確實有照著我們設定的顏色去做變化:


就算只看 log 訊息,任何人都可以對程式的執行流程有初步了解。
這對於需要接手維護程式的工程師來說,是很重要的指引。
五、常見(FAQ)
Q1:為什麼在 Django 中需要自訂 Trace ID?
在高併發環境下,多個請求會交織在一起。透過 Trace ID,開發者可以過濾出特定請求的所有 Log 片段,解決錯誤追蹤的斷層問題。
Q2:TimedRotatingFileHandler 的好處是什麼?
它可以自動按時間分割日誌,並透過 backupCount 自動清理舊檔案,防止伺服器硬碟空間被無限增長的日誌檔撐爆。
六、總結
在這篇文章,我們詳細介紹了關於 log 的結構、Django 的 log 配置,以及結合 Trace ID 與 colorlog ,讓不同層級的訊息可以得到更醒目的提示,將系統的執行情況一覽無遺。
如果想更加深入了解 Django 的 log 設置,可以參考官方文件 的說明。
![]()