昨天我成功最佳化了 M2A Agent 的 Docker 映像檔,但是在容器化應用程式運行的過程中,如果系統出現問題時,我需要逐一使用 docker logs 檢查每個容器的日誌,這樣分散的日誌管理方式既耗時又難以追蹤問題。
因此今天的目標是建立一套集中式日誌監控系統,透過 Grafana Loki 與 Promtail 這套流行的技術棧,讓我能在統一的視覺化介面中,即時監控 m2a-agent-app 與 n8n 的運作狀態與日誌資訊。
docker stats 指令監控容器的資源使用狀況docker-compose.yml 中整合 Loki、Promtail 與 Grafana 服務在導入完整的日誌監控系統之前,我需要先了解目前容器的資源使用狀況。
docker stats 指令可以即時顯示容器的資源使用情況,包括 CPU、記憶體、網路 I/O 與磁碟 I/O。
# 檢視所有運作中容器的資源使用狀況
docker stats
# 檢視特定容器的資源使用狀況
docker stats m2a-agent-app m2a-n8n
# 只顯示當前狀態(不持續更新)
docker stats --no-stream
執行 docker stats 後,會看到以下欄位:
為了更清楚地檢視特定資訊,我可以使用 --format 選項自訂輸出格式:
# 以表格格式顯示容器名稱、CPU 與記憶體使用量
docker stats --format "table {{.Container}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
# 只顯示 M2A Agent 相關容器的資源使用狀況
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" m2a-agent-app m2a-n8n
透過這些指令,我可以快速了解容器的資源消耗狀況,但這僅能提供當下的快照,無法追蹤歷史趨勢或集中管理日誌。
在實作集中式日誌監控之前,我需要先理解這套技術棧的運作原理。
Grafana Loki 日誌監控系統由三個核心元件組成
Promtail — 日誌收集器
Loki — 日誌儲存後端
Grafana — 視覺化工具
整個日誌監控系統的資料流向如下
現在要在現有的 docker-compose.yml 中加入 Loki、Promtail 與 Grafana 三個服務。
首先建立用於存放設定檔的目錄
# 在專案根目錄建立 config 資料夾
mkdir config
修改 docker-compose.yml,加入監控相關的服務:
services:
# ===================================
# 核心應用服務
# ===================================
n8n:
image: n8nio/n8n:latest
container_name: m2a-n8n
restart: unless-stopped
ports:
- "5678:5678"
volumes:
- ./n8n_data:/home/node/.n8n
environment:
- N8N_HOST=0.0.0.0
- N8N_PORT=5678
- N8N_PROTOCOL=http
- WEBHOOK_URL=http://n8n:5678/
- GENERIC_TIMEZONE=Asia/Taipei
- N8N_SECURE_COOKIE=false
- N8N_DIAGNOSTICS_ENABLED=false
# 加入 labels 供 Promtail 識別
labels:
logging: "promtail"
service_name: "n8n"
networks:
- m2a_network
m2a-agent:
build: .
container_name: m2a-agent-app
restart: unless-stopped
ports:
- "7860:7860"
volumes:
- ./app.py:/app/app.py
- ./src:/app/src
- ./data:/app/data
- ./recording:/app/recording
- ./.gradio:/app/.gradio
env_file:
- .env
depends_on:
- n8n
# 加入 labels 供 Promtail 識別
labels:
logging: "promtail"
service_name: "m2a-agent"
networks:
- m2a_network
# 健康檢查設定,與 Dockerfile 中的 HEALTHCHECK 指令對應
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:7860/" ]
interval: 30s
timeout: 30s
retries: 3
start_period: 300s
# ===================================
# 日誌監控服務
# ===================================
loki:
image: grafana/loki:3.4.1
container_name: m2a-loki
restart: unless-stopped
ports:
- "3100:3100"
volumes:
- ./config:/mnt/config
command: -config.file=/mnt/config/loki-config.yaml
networks:
- m2a_network
promtail:
image: grafana/promtail:3.4.1
container_name: m2a-promtail
restart: unless-stopped
ports:
- "9080:9080" # 現在出於方便除錯驗證,後面驗證完成後記得回來移除埠號映射以提升安全性
volumes:
- /var/run/docker.sock:/var/run/docker.sock # 掛載 Docker Socket,讓 Promtail 可以讀取容器資訊
- ./config:/mnt/config
command: -config.file=/mnt/config/promtail-config.yaml
depends_on:
- loki
networks:
- m2a_network
grafana:
image: grafana/grafana:10.2.3
container_name: m2a-grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin # 預設管理者帳號
- GF_SECURITY_ADMIN_PASSWORD=admin # 預設管理者密碼
depends_on:
- loki
networks:
- m2a_network
networks:
m2a_network:
driver: bridge
volumes:
n8n_data:
在這個設定中,我設定了以下幾項設定
logging: "promtail" 標籤,讓 Promtail 能識別要收集哪些容器的日誌./config 目錄掛載至容器,供 Loki 與 Promtail 使用設定檔接下來要建立 Loki 的設定檔,定義日誌的儲存與索引規則。
在 config 目錄中建立 loki-config.yaml 檔案:
# 關閉權限驗證,適合本地測試環境使用
auth_enabled: false
# 伺服器設定
server:
http_listen_port: 3100 # Loki HTTP API 的監聽埠號
grpc_listen_port: 9096 # Loki gRPC API 的監聽埠號
# 通用設定
common:
path_prefix: /tmp/loki # 資料儲存的根目錄
storage:
filesystem:
chunks_directory: /tmp/loki/chunks # 日誌區塊的儲存位置
rules_directory: /tmp/loki/rules # 告警規則的儲存位置
replication_factor: 1 # 資料副本數量,單節點設為 1
ring:
kvstore:
store: inmemory # 使用記憶體儲存 ring 狀態資訊
# Schema 設定
schema_config:
configs:
- from: 2025-10-01 # Schema 生效日期(必填欄位)
store: tsdb # 使用時間序列資料庫儲存索引
object_store: filesystem # 使用本機檔案系統儲存日誌區塊
schema: v13 # 使用 Grafana 官方推薦的 schema 版本
index:
prefix: index_ # 索引檔案的前綴
period: 24h # 每 24 小時產生一個新的索引檔案
# 限制設定
limits_config:
reject_old_samples: true # 拒絕接收時間戳記過舊的日誌
reject_old_samples_max_age: 168h # 超過 7 天的日誌會被拒絕
ingestion_rate_mb: 16 # 每秒最大接收 16MB 的日誌資料
ingestion_burst_size_mb: 32 # 突發流量時最大接收 32MB
retention_period: 168h # 保留日誌 7 天(本地測試用)
# 查詢器設定
querier:
max_concurrent: 4 # 最大並行查詢數量
這個設定檔中有幾個重要的設計考量
現在要建立 Promtail 的設定檔,定義如何收集容器日誌。
在 config 目錄中建立 promtail-config.yaml 檔案:
# 伺服器設定
server:
http_listen_port: 9080 # Promtail HTTP API 的監聽埠號
grpc_listen_port: 0 # 不啟用 gRPC
# 位置檔案設定
positions:
filename: /tmp/positions.yaml # 記錄上次讀取到的日誌位置,避免重複擷取
# Loki 客戶端設定
clients:
- url: http://loki:3100/loki/api/v1/push # Loki 的 API 端點
# 日誌收集設定
scrape_configs:
- job_name: docker # Job 名稱
# Docker 服務發現設定
docker_sd_configs:
- host: unix:///var/run/docker.sock # Docker Socket 的位置
refresh_interval: 5s # 每 5 秒重新掃描容器清單
filters:
- name: label # 根據 label 篩選容器
values: ["logging=promtail"] # 只收集帶有 logging=promtail 標籤的容器
# 標籤重新配置
relabel_configs:
# 提取容器名稱作為 job 標籤
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)' # 移除容器名稱前的斜線
target_label: 'job'
# 提取 service_name 標籤
- source_labels: ['__meta_docker_container_label_service_name']
target_label: 'service_name'
# 提取容器 ID
- source_labels: ['__meta_docker_container_id']
target_label: 'container_id'
# 提取日誌串流類型(stdout 或 stderr)
- source_labels: ['__meta_docker_container_log_stream']
target_label: 'logstream'
這個設定檔的核心機制說明
logging=promtail 標籤的容器,避免收集不必要的日誌__meta_*)轉換為 Loki 可用的標籤所有設定檔都已準備完畢,現在要啟動服務並驗證運作狀態。
# 停止並移除舊的容器(保留資料)
docker-compose down
# 重新建構並啟動所有服務
docker-compose up --build -d
# 檢視容器狀態
docker-compose ps
檢查各個服務是否正常啟動:
# 檢視 Loki 的日誌
docker logs m2a-loki
# 檢視 Promtail 的日誌
docker logs m2a-promtail
# 檢視 Grafana 的日誌
docker logs m2a-grafana
確認各個服務的網頁介面都能正常訪問
http://localhost:3000,預設帳號密碼皆為 admin
http://localhost:3100/ready,應顯示 ready
http://localhost:9080/targets,應顯示發現的容器清單登入 Grafana 後,需要先新增 Loki 作為資料來源。
http://localhost:3000 並使用 admin/admin 登入在資料來源設定頁面中,輸入以下資訊:
Loki(可自訂)http://loki:3100(使用 Docker 網路內的服務名稱)Server (default)
完成後點擊下方的 Save & test 按鈕,會看到綠色的成功訊息:「Data source successfully connected」。

現在要在 Grafana 中建立儀表板,將分散的日誌整合在統一的介面中。
在編輯面板的介面中,進行以下設定:
在右上方的下拉式選單中的 visualization 選擇 Logs。
在下方的 Query 區域中切換至 Code 後,輸入以下查詢語法,接著再點擊 Run Query
{service_name="m2a-agent"}
這個查詢會篩選出所有來自 m2a-agent 服務的日誌。
在上方點擊 Add 按鈕並選擇 Visualization,重複上述步驟,但使用不同的查詢語法
{service_name="n8n"}

為了快速辨識錯誤,可以建立一個專門顯示錯誤日誌的面板:
{service_name=~"m2a-agent|n8n"} |~ "(?i)(error|warn|failed)"
這個查詢會同時篩選 m2a-agent 與 n8n 兩個服務中??的記錄。
完成所有面板的設定後,點擊中間的儲存圖案的 Save dashboard 按鈕
M2A Agent 日誌監控
點擊 Save 按鈕完成儲存後,可以看到面板的名稱已改變

LogQL 是 Loki 的查詢語言,類似 Prometheus 的 PromQL,可以進行強大的日誌篩選與分析。
LogQL 查詢由兩個部分組成:
{log_stream_selector} | pipeline_1 | pipeline_2 ...
{} 包覆,以 key=value 組合描述來源| 串接多個處理步驟,對查詢結果進行處理以下是幾個實用的查詢範例:
# 查詢特定服務的所有日誌
{service_name="m2a-agent"}
# 查詢多個服務的日誌
{service_name=~"m2a-agent|n8n"}
# 查詢包含特定關鍵字的日誌
{service_name="m2a-agent"} |= "error"
# 查詢不包含特定關鍵字的日誌
{service_name="m2a-agent"} != "debug"
# 查詢並解析 JSON 格式的日誌 (若 Log 為 JSON 格式)
{service_name="m2a-agent"} | json
# 查詢 JSON 日誌中特定欄位的值
{service_name="m2a-agent"} | json | level="error"
# 使用正規表示式篩選
{service_name="m2a-agent"} | regexp "(?P<status_code>\\d{3})"
LogQL 也支援聚合函式,可以統計日誌出現的次數:
# 統計過去 5 分鐘內錯誤日誌的數量
sum(count_over_time({service_name="m2a-agent"} |~ "(?i)(error|warn|failed|exception)" [5m]))
# 統計每個服務的日誌數量
sum(count_over_time({service_name=~"m2a-agent|n8n"} [1m])) by (service_name)
# 計算錯誤率(每秒的錯誤日誌出現次數)
sum(rate({service_name="m2a-agent"} |~ "(?i)(error|warn|failed|exception)" [5m]))
✅ 完成項目
docker stats 指令監控容器的 CPU、記憶體與網路 I/O 資源使用狀況docker-compose.yml 中成功整合 Loki、Promtail 與 Grafana 三個監控服務loki-config.yaml 與 promtail-config.yaml 兩個設定檔,定義日誌的收集、儲存與索引規則今天的實作讓我從「分散的日誌檢視」升級到「集中式日誌監控」,之前當系統出現問題時,我需要每個執行 docker logs 指令或在 Docker desktop 檢查每個容器,這這樣做不夠有效率。而透過 Grafana Loki 這套技術棧,我現在可以在同一個視覺化介面中,即時監控所有容器的日誌,甚至能快速篩選出錯誤日誌,定位問題所在。
Promtail 的服務發現機制它不只能自動偵測帶有特定標籤的容器,並且可以即時收集日誌,完全不需要手動設定每個容器的日誌來源。而 Loki 的標籤式索引設計,相較於傳統的全文索引,不僅節省儲存空間,查詢效能也更加優異。
LogQL 查詢語言的學習一旦掌握了基本語法,就能發揮強大的日誌分析能力,從簡單的關鍵字篩選,到複雜的聚合統計,都能透過幾行查詢語法輕鬆實現,這讓我體會到,可觀測性(Observability) 不只是「能看到日誌」,更重要的是「能快速從日誌中提取有價值的資訊」。
🎯 明天計畫
總結三十天專案開發歷程,撰寫完整技術報告與開發心得。