iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0
生成式 AI

打造基於 MCP 協議與 n8n 工作流的會議處理 Agent系列 第 29

Day 29 Docker 容器可觀測性 — 使用 Grafana Loki 監控日誌

  • 分享至 

  • xImage
  •  

昨天我成功最佳化了 M2A Agent 的 Docker 映像檔,但是在容器化應用程式運行的過程中,如果系統出現問題時,我需要逐一使用 docker logs 檢查每個容器的日誌,這樣分散的日誌管理方式既耗時又難以追蹤問題。

因此今天的目標是建立一套集中式日誌監控系統,透過 Grafana Loki 與 Promtail 這套流行的技術棧,讓我能在統一的視覺化介面中,即時監控 m2a-agent-app 與 n8n 的運作狀態與日誌資訊。

今天的目標與挑戰

  • 學習使用 docker stats 指令監控容器的資源使用狀況
  • 了解 Grafana Loki 日誌監控技術棧的架構與運作原理
  • 在現有的 docker-compose.yml 中整合 Loki、Promtail 與 Grafana 服務
  • 設定 Promtail 收集容器日誌並傳送至 Loki
  • 在 Grafana 中建立資料來源與日誌儀表板
  • 使用 LogQL 查詢語言篩選與分析日誌

Step 1:使用 docker stats 監控容器資源

在導入完整的日誌監控系統之前,我需要先了解目前容器的資源使用狀況。

1-1 基本的 docker stats 指令

docker stats 指令可以即時顯示容器的資源使用情況,包括 CPU、記憶體、網路 I/O 與磁碟 I/O。

# 檢視所有運作中容器的資源使用狀況
docker stats

# 檢視特定容器的資源使用狀況
docker stats m2a-agent-app m2a-n8n

# 只顯示當前狀態(不持續更新)
docker stats --no-stream

1-2 輸出欄位說明

執行 docker stats 後,會看到以下欄位:

  • CONTAINER ID:容器的唯一識別碼
  • NAME:容器名稱
  • CPU %:容器使用的主機 CPU 百分比
  • MEM USAGE / LIMIT:容器使用的記憶體總量與允許使用的記憶體上限
  • MEM %:容器所使用的記憶體百分比
  • NET I/O:容器透過網路介面接收與傳送的資料量
  • BLOCK I/O:容器從主機的區塊裝置讀取與寫入的資料量
  • PIDS:容器建立的行程或執行緒數量

1-3 格式化輸出結果

為了更清楚地檢視特定資訊,我可以使用 --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

透過這些指令,我可以快速了解容器的資源消耗狀況,但這僅能提供當下的快照,無法追蹤歷史趨勢或集中管理日誌。


Step 2:了解 Grafana Loki 技術棧架構

在實作集中式日誌監控之前,我需要先理解這套技術棧的運作原理。

2-1 Grafana Loki 架構組成

Grafana Loki 日誌監控系統由三個核心元件組成

  • Promtail — 日誌收集器

    • 職責:負責收集、處理日誌並將之傳送至 Loki
    • 特點:專為 Loki 打造的 Log Agent,使用與 Prometheus 相似的設定檔與標籤概念
    • 功能:透過 Docker Socket 監控容器的 stdout 與 stderr,自動收集日誌資料
  • Loki — 日誌儲存後端

    • 職責:負責日誌的儲存、索引與查詢處理
    • 特點:不對日誌內容進行全文索引,而是為日誌流設定標籤,具有較佳的成本效益
    • 優勢:可水平擴展、高度可用且支援多個使用者
  • Grafana — 視覺化工具

    • 職責:提供統一的查詢與視覺化介面
    • 特點:支援多種資料來源(Prometheus、Loki、InfluxDB 等)
    • 功能:透過 LogQL 查詢語言篩選日誌、建立儀表板、設定告警條件

2-2 資料流向說明

整個日誌監控系統的資料流向如下

  1. 應用程式輸出日誌:m2a-agent-app 與 n8n 將日誌輸出至 stdout/stderr
  2. Promtail 收集日誌:透過 Docker Socket 監控容器,擷取日誌並加上標籤
  3. 傳送至 Loki:Promtail 將處理後的日誌推送至 Loki 的 API 端點
  4. Loki 儲存索引:Loki 根據標籤建立索引並儲存日誌資料
  5. Grafana 查詢展示:使用者透過 Grafana 介面使用 LogQL 查詢日誌並視覺化

Step 3:在 docker-compose.yml 中整合監控服務

現在要在現有的 docker-compose.yml 中加入 Loki、Promtail 與 Grafana 三個服務。

3-1 建立設定檔目錄結構

首先建立用於存放設定檔的目錄

# 在專案根目錄建立 config 資料夾
mkdir config

3-2 更新 docker-compose.yml

修改 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:

3-3 設定檔說明

在這個設定中,我設定了以下幾項設定

  • 加入 labels:為 n8n 與 m2a-agent 加上 logging: "promtail" 標籤,讓 Promtail 能識別要收集哪些容器的日誌
  • service_name 標籤:用於在 Grafana 中區分不同服務的日誌來源
  • 掛載 Docker Socket:讓 Promtail 可以與 Docker Daemon 溝通,取得容器清單與日誌
  • 設定檔掛載:將 ./config 目錄掛載至容器,供 Loki 與 Promtail 使用設定檔

Step 4:建立 Loki 設定檔

接下來要建立 Loki 的設定檔,定義日誌的儲存與索引規則。

4-1 建立 loki-config.yaml

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  # 最大並行查詢數量

4-2 設定檔關鍵欄位說明

這個設定檔中有幾個重要的設計考量

  • auth_enabled: false:關閉權限驗證,簡化本地測試流程,生產環境應啟用
  • store: tsdb:使用時間序列資料庫,是 Grafana 官方推薦的儲存方案
  • object_store: filesystem:將日誌儲存在本機檔案系統,適合本地測試;生產環境可改用 S3 或 GCS
  • from 欄位:必填欄位,定義 schema 的生效時間,方便未來版本升級時使用不同的 schema
  • retention_period: 0s:永久保留日誌,生產環境應設定合理的保留期限以控制儲存成本

Step 5:建立 Promtail 設定檔

現在要建立 Promtail 的設定檔,定義如何收集容器日誌。

5-1 建立 promtail-config.yaml

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'

5-2 設定檔關鍵欄位說明

這個設定檔的核心機制說明

  • docker_sd_configs:使用 Docker 的服務發現機制,自動獲取正在運作的容器資訊
  • filters:只收集帶有 logging=promtail 標籤的容器,避免收集不必要的日誌
  • relabel_configs:將 Docker 提供的中繼標籤(__meta_*)轉換為 Loki 可用的標籤
  • regex: '/(.*)':使用正規表示式移除容器名稱前的斜線字元
  • positions.yaml:記錄每個日誌檔案的讀取位置,確保重新啟動後不會重複收集日誌

Step 6:啟動監控服務並驗證

所有設定檔都已準備完畢,現在要啟動服務並驗證運作狀態。

6-1 啟動所有服務

# 停止並移除舊的容器(保留資料)
docker-compose down

# 重新建構並啟動所有服務
docker-compose up --build -d

# 檢視容器狀態
docker-compose ps

6-2 驗證服務運行狀況

檢查各個服務是否正常啟動:

# 檢視 Loki 的日誌
docker logs m2a-loki

# 檢視 Promtail 的日誌
docker logs m2a-promtail

# 檢視 Grafana 的日誌
docker logs m2a-grafana

6-3 驗證服務可訪問性

確認各個服務的網頁介面都能正常訪問

  • Grafana:訪問 http://localhost:3000,預設帳號密碼皆為 admin
  • Loki API:訪問 http://localhost:3100/ready,應顯示 ready
  • Promtail:訪問 http://localhost:9080/targets,應顯示發現的容器清單

Step 7:在 Grafana 中設定 Loki 資料來源

登入 Grafana 後,需要先新增 Loki 作為資料來源。

7-1 新增 Loki 資料來源

  1. 訪問 http://localhost:3000 並使用 admin/admin 登入
  2. 首次登入會要求變更密碼,可選擇跳過(僅限本地測試)
  3. 展開左側選單 ☰ (Menu),選擇 ConnectionsData sources
  4. 點擊中間的 Add data source 按鈕
  5. 在搜尋框中輸入 Loki,點擊進入

7-2 設定 Loki 連線資訊

在資料來源設定頁面中,輸入以下資訊:

  • NameLoki(可自訂)
  • URLhttp://loki:3100(使用 Docker 網路內的服務名稱)
  • Access:選擇 Server (default)

完成後點擊下方的 Save & test 按鈕,會看到綠色的成功訊息:「Data source successfully connected」。

Loki Settings


Step 8:建立日誌儀表板

現在要在 Grafana 中建立儀表板,將分散的日誌整合在統一的介面中。

8-1 建立新的 Dashboard

  1. 展開左側選單 ☰ (Menu),選擇 Dashboards
  2. 點擊中間的 + Create Dashboard 按鈕
  3. 點擊 + Add visualization 按鈕
  4. Select data source 頁面,選擇剛剛建立的 Loki 資料來源

8-2 建立 M2A Agent 日誌面板

在編輯面板的介面中,進行以下設定:

設定視覺化類型

在右上方的下拉式選單中的 visualization 選擇 Logs
Add log

撰寫 LogQL 查詢

在下方的 Query 區域中切換至 Code 後,輸入以下查詢語法,接著再點擊 Run Query

{service_name="m2a-agent"}

這個查詢會篩選出所有來自 m2a-agent 服務的日誌。

8-3 建立 n8n 日誌面板

在上方點擊 Add 按鈕並選擇 Visualization,重複上述步驟,但使用不同的查詢語法

{service_name="n8n"}

Add Visualization

8-4 建立錯誤日誌監控面板(待修改)

為了快速辨識錯誤,可以建立一個專門顯示錯誤日誌的面板:

{service_name=~"m2a-agent|n8n"} |~ "(?i)(error|warn|failed)"

這個查詢會同時篩選 m2a-agent 與 n8n 兩個服務中??的記錄。

8-5 儲存 Dashboard

完成所有面板的設定後,點擊中間的儲存圖案的 Save dashboard 按鈕
Save Dashboard

  • Dashboard nameM2A Agent 日誌監控
  • Folder:選擇預設的 Dashboards 或也可以建立新資料夾

點擊 Save 按鈕完成儲存後,可以看到面板的名稱已改變

All Dashboard


Step 9:使用 LogQL 進階查詢

LogQL 是 Loki 的查詢語言,類似 Prometheus 的 PromQL,可以進行強大的日誌篩選與分析。

9-1 基本查詢語法

LogQL 查詢由兩個部分組成:

{log_stream_selector} | pipeline_1 | pipeline_2 ...
  • Log stream selector:使用 {} 包覆,以 key=value 組合描述來源
  • Log pipeline:使用 | 串接多個處理步驟,對查詢結果進行處理

9-2 常用的篩選範例

以下是幾個實用的查詢範例:

# 查詢特定服務的所有日誌
{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})"

9-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 資源使用狀況
  • 深入了解 Grafana Loki 技術棧的架構,包括 Promtail、Loki 與 Grafana 的角色與資料流向
  • docker-compose.yml 中成功整合 Loki、Promtail 與 Grafana 三個監控服務
  • 建立了 loki-config.yamlpromtail-config.yaml 兩個設定檔,定義日誌的收集、儲存與索引規則
  • 在 Grafana 中設定 Loki 資料來源,並建立了「M2A Agent 日誌監控」儀表板
  • 學習了 LogQL 查詢語言的基本語法,能夠篩選特定服務、日誌等級與關鍵字
  • 成功將原本分散在各容器的日誌,整合至統一的 Grafana 介面中,大幅提升了除錯效率

心得

今天的實作讓我從「分散的日誌檢視」升級到「集中式日誌監控」,之前當系統出現問題時,我需要每個執行 docker logs 指令或在 Docker desktop 檢查每個容器,這這樣做不夠有效率。而透過 Grafana Loki 這套技術棧,我現在可以在同一個視覺化介面中,即時監控所有容器的日誌,甚至能快速篩選出錯誤日誌,定位問題所在。

Promtail 的服務發現機制它不只能自動偵測帶有特定標籤的容器,並且可以即時收集日誌,完全不需要手動設定每個容器的日誌來源。而 Loki 的標籤式索引設計,相較於傳統的全文索引,不僅節省儲存空間,查詢效能也更加優異。

LogQL 查詢語言的學習一旦掌握了基本語法,就能發揮強大的日誌分析能力,從簡單的關鍵字篩選,到複雜的聚合統計,都能透過幾行查詢語法輕鬆實現,這讓我體會到,可觀測性(Observability) 不只是「能看到日誌」,更重要的是「能快速從日誌中提取有價值的資訊」。

🎯 明天計畫

總結三十天專案開發歷程,撰寫完整技術報告與開發心得。


上一篇
Day 28 Docker 映像檔最佳化 — 實現輕量與敏捷
下一篇
Day 30 M2A Agent 開發總結 — 我的 AI 會議助理誕生記!
系列文
打造基於 MCP 協議與 n8n 工作流的會議處理 Agent30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言