Ollama: モニタリングとロギング

モニタリングはOllamaの健康診断——リアルタイムの心拍、指標トレンド、異常アラート、事故の未然防止。

💡 ヒント: Prometheus + Grafanaが最も主流のモニタリングコンビ——Prometheusが定期的な指標収集・保存を担当し、Grafanaが可視化とアラートを担当。Ollamaには組み込みのPrometheusエンドポイントがないため、node_exporter(システム指標)+ DCGMエクスポーター(GPU指標)+ カスタムミドルウェア(ビジネス指標)を組み合わせる必要がある。

📋 前提条件: まず以下を習得していること

1. 学べること


2. SaaS起業家のリアルな事例

💡 ヒント: GPU OOMはOllama本番環境で最も一般的な障害。GrafanaでVRAM使用率 > 90%の早期警告アラートを設定し、問題を事前に検出。nvidia-smiのGPU温度と消費電力もモニタリング——過熱もパフォーマンス急低下の原因。

ℹ️ 情報: Ollamaには組み込みのPrometheus指標エンドポイント(/metrics)がない。node_exporter(システム指標)+ DCGMエクスポーター(GPU指標)+ カスタムFastAPIミドルウェア(ビジネス指標)を組み合わせて完全なモニタリングを実装する必要がある。

(1) ペインポイント:Ollamaが突然遅くなり原因不明

AliceのSupportBotの応答時間がある日2秒から15秒に急増したが、モデルのロードが遅いのか、GPU OOMなのか、同時実行が多すぎるのか不明。モニタリングデータがないため推測するしかなかった。

(2) ソリューション:Prometheus + Grafanaリアルタイムモニタリング

モニタリングスタックをデプロイ後、GrafanaダッシュボードでGPU使用率、リクエストレイテンシ、メモリ使用量がリアルタイム表示。一目で別プロセスがGPUメモリを消費していることが判明:

BASH
# Prometheus scrapes Ollama metrics
# Grafana visualizes real-time dashboards
# Alertmanager sends notifications when anomalies detected

3. Ollamaログ設定

⚠️ : Ollamaのログはデフォルトでリクエストとレスポンスの完全な内容を記録し、ユーザー入力の機密情報(名前、注文番号など)が含まれる可能性がある。本番ではリバースプロキシ層でログをサニタイズしてから記録するか、ログレベルを制限してリクエストボディの記録を回避すること。

(1) ログソースと閲覧方法

プラットフォーム ログコマンド 説明
Linux systemd journalctl -u ollama -f リアルタイム追跡
Docker docker logs -f ollama コンテナログ
macOS cat ~/.ollama/logs/server.log ログファイル
Windows イベントビューアー イベントログ

(2) デバッグログの有効化

環境変数 目的 出力内容
OLLAMA_DEBUG 詳細デバッグ情報 GPU検出、モデルロード詳細
OLLAMA_FLASH_ATTENTION Flash Attentionログ アテンションメカニズム状態

(3) ▶ サンプル:ログ閲覧と分析

BASH
# Linux: real-time Ollama logs
sudo journalctl -u ollama -f --no-pager

# Filter for errors
sudo journalctl -u ollama | grep -i "error\|fail\|oom\|panic"

# Docker: real-time container logs
docker logs -f ollama --tail 100

# Enable debug logging
sudo systemctl edit ollama
[Service]
Environment="OLLAMA_DEBUG=1"
sudo systemctl restart ollama

# Check for GPU issues in logs
sudo journalctl -u ollama --since "1 hour ago" | grep -i "cuda\|gpu\|memory"

出力:

TEXT
# Ollama command executed successfully

4. 主要モニタリング指標

(1) 4つの指標カテゴリ

カテゴリ 指標 収集方法 アラート閾値
GPU 使用率、VRAM使用量、温度 nvidia-smi / DCGM 使用率 > 95%
推論 レイテンシ、Tokens/s、リクエスト率 カスタムエクスポーター レイテンシ > 10秒
システム CPU、RAM、ディスクIO node_exporter RAM > 90%
アプリケーション エラー率、モデルロード時間 ログ分析 エラー率 > 5%

(2) Ollamaカスタム指標

指標 収集方法 意味
request_latency API計時 リクエストレイテンシ
tokens_per_second レスポンス統計 生成トークン数
model_load_time ロード計時 モデルロード時間
active_models APIクエリ 現在ロード中のモデル数

(3) ▶ サンプル:カスタム指標収集

PYTHON
import ollama
import time
import json
from datetime import datetime
from pathlib import Path

class OllamaMetrics:
    def __init__(self, log_file: str = "ollama_metrics.jsonl"):
        self.log_file = Path(log_file)

    def record_request(self, model: str, prompt: str) -> dict:
        start = time.time()
        try:
            response = ollama.chat(
                model=model,
                messages=[{"role": "user", "content": prompt}],
                stream=False
            )
            duration = time.time() - start
            content = response["message"]["content"]
            metrics = {
                "timestamp": datetime.now().isoformat(),
                "model": model,
                "duration_s": round(duration, 3),
                "response_length": len(content),
                "estimated_tokens": len(content) // 4,
                "tok_per_s": round(len(content) / 4 / duration, 1),
                "status": "success"
            }
        except Exception as e:
            duration = time.time() - start
            metrics = {
                "timestamp": datetime.now().isoformat(),
                "model": model,
                "duration_s": round(duration, 3),
                "status": "error",
                "error": str(e)
            }

        with open(self.log_file, "a") as f:
            f.write(json.dumps(metrics) + "\n")
        return metrics

    def get_stats(self, minutes: int = 30) -> dict:
        """Calculate stats from recent metrics."""
        cutoff = datetime.now().timestamp() - minutes * 60
        durations = []
        errors = 0
        total = 0

        if not self.log_file.exists():
            return {}

        with open(self.log_file) as f:
            for line in f:
                data = json.loads(line)
                ts = datetime.fromisoformat(data["timestamp"]).timestamp()
                if ts >= cutoff:
                    total += 1
                    durations.append(data.get("duration_s", 0))
                    if data.get("status") == "error":
                        errors += 1

        if total == 0:
            return {}

        return {
            "total_requests": total,
            "error_rate": round(errors / total * 100, 1),
            "avg_duration_s": round(sum(durations) / len(durations), 2),
            "max_duration_s": round(max(durations), 2),
            "p95_duration_s": round(sorted(durations)[int(len(durations) * 0.95)], 2)
        }

# Usage
metrics = OllamaMetrics()
metrics.record_request("qwen2.5", "Hello")
print(metrics.get_stats())

出力:

TEXT
# Function defined successfully

5. Prometheus + Grafanaモニタリングスタック

(1) モニタリングアーキテクチャ

100%
flowchart LR
    A[Ollama<br/>+ カスタムエクスポーター] --> B[Prometheus<br/>指標ストア]
    C[node_exporter<br/>システム指標] --> B
    D[DCGMエクスポーター<br/>GPU指標] --> B
    B --> E[Grafana<br/>ダッシュボード]
    E --> F[Alertmanager<br/>通知]
    F --> G[Email/Slack]

(2) Docker Composeモニタリングスタック

(3) ▶ サンプル:モニタリングスタックデプロイ

YAML
# docker-compose.monitoring.yml
version: "3.8"

services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    ports: ["9090:9090"]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    restart: unless-stopped

  grafana:
    image: grafana/grafana
    container_name: grafana
    ports: ["3001:3000"]
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on: [prometheus]
    restart: unless-stopped

  node-exporter:
    image: prom/node-exporter
    container_name: node-exporter
    ports: ["9100:9100"]
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
    restart: unless-stopped

volumes:
  prometheus_data:
  grafana_data:
YAML
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: "node"
    static_configs:
      - targets: ["node-exporter:9100"]

  - job_name: "ollama_custom"
    static_configs:
      - targets: ["host.docker.internal:8000"]  # Custom exporter

出力:

TEXT
Monitoring stack deployed successfully, Prometheus/Grafana services running normally

6. アラートルール

(1) 主要アラートルール

アラート 条件 重要度 通知
GPU OOM VRAM > 95% が2分間継続 Critical 即時通知
高レイテンシ P95 > 10秒 が5分間継続 Warning メール
サービス利用不可 ヘルスチェック3回連続失敗 Critical 即時通知
エラー率急増 エラー率 > 10% が3分間継続 Warning メール
ディスク容量不足 空き容量 < 10% Warning メール

(2) ▶ サンプル:Prometheusアラートルール

YAML
# alert_rules.yml
groups:
  - name: ollama_alerts
    rules:
      - alert: OllamaHighLatency
        expr: histogram_quantile(0.95, rate(ollama_request_duration_seconds_bucket[5m])) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Ollama P95 latency above 10s"
          description: "Current P95: {{ $value }}s"

      - alert: OllamaServiceDown
        expr: up{job="ollama_custom"} == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Ollama service is down"
          description: "No response from Ollama for 2 minutes"

      - alert: HighGPUMemory
        expr: DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL > 0.95
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "GPU memory usage above 95%"

出力:

TEXT
# Alert rules loaded successfully
kubectl exec -it prometheus-0 -- wget -qO- http://localhost:9090/api/v1/rules | head
# {"status":"success","data":{"groups":[{"name":"ollama_alerts","rules":[...]}]}}

(3) ▶ サンプル:スロークエリ診断

BASH
#!/bin/bash
# Slow query diagnosis script

LOG="ollama_metrics.jsonl"

echo "=== Slow Query Report (last 60 minutes) ==="
echo ""

if [ -f "$LOG" ]; then
    # Find requests with duration > 5 seconds
    python3 -c "
import json, sys
from datetime import datetime, timedelta

cutoff = datetime.now() - timedelta(hours=1)
slow = []
with open('$LOG') as f:
    for line in f:
        data = json.loads(line)
        ts = datetime.fromisoformat(data['timestamp'])
        if ts >= cutoff and data.get('duration_s', 0) > 5:
            slow.append(data)

if slow:
    print(f'Found {len(slow)} slow requests (>5s):')
    for s in slow[:10]:
        print(f\"  {s['timestamp']}: {s['duration_s']}s, model={s.get('model','?')}, status={s.get('status','?')}\")
else:
    print('No slow requests found in the last hour.')
"
else
    echo "No metrics log found. Enable OllamaMetrics first."
fi

echo ""
echo "=== Current GPU Status ==="
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv 2>/dev/null || echo "No GPU detected"

出力:

TEXT
NAME                    ID              SIZE    
llama3.2:latest        a80...          2.0 GB  
mistral:latest         61...           4.1 GB

7. 総合サンプル:モニタリングダッシュボード設定

PYTHON
# ============================================
# Comprehensive: Monitoring dashboard config
# Prometheus + Grafana + Custom exporter
# ============================================

import json
from http.server import HTTPServer, BaseHTTPRequestHandler
import ollama
import time
import threading

# Custom Prometheus exporter for Ollama
class OllamaExporter(BaseHTTPRequestHandler):
    metrics = {
        "ollama_request_duration_seconds": [],
        "ollama_requests_total": 0,
        "ollama_errors_total": 0,
        "ollama_active_models": 0,
    }

    def do_GET(self):
        if self.path == "/metrics":
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()

            output = []
            output.append(f"# HELP ollama_requests_total Total requests")
            output.append(f"# TYPE ollama_requests_total counter")
            output.append(f"ollama_requests_total {self.metrics['ollama_requests_total']}")

            output.append(f"# HELP ollama_errors_total Total errors")
            output.append(f"# TYPE ollama_errors_total counter")
            output.append(f"ollama_errors_total {self.metrics['ollama_errors_total']}")

            output.append(f"# HELP ollama_request_duration_seconds Request duration")
            output.append(f"# TYPE ollama_request_duration_seconds histogram")
            if self.metrics["ollama_request_duration_seconds"]:
                durations = self.metrics["ollama_request_duration_seconds"][-100:]
                avg = sum(durations) / len(durations)
                output.append(f'ollama_request_duration_seconds_avg {avg:.3f}')
                output.append(f'ollama_request_duration_seconds_max {max(durations):.3f}')

            # Active models
            try:
                models = ollama.list()
                active = len(models.get("models", []))
                output.append(f"# HELP ollama_active_models Number of loaded models")
                output.append(f"# TYPE ollama_active_models gauge")
                output.append(f"ollama_active_models {active}")
            except Exception:
                pass

            self.wfile.write("\n".join(output).encode())
        elif self.path == "/health":
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b"OK")
        else:
            self.send_response(404)
            self.end_headers()

    @classmethod
    def record_request(cls, duration: float, is_error: bool = False):
        cls.metrics["ollama_requests_total"] += 1
        cls.metrics["ollama_request_duration_seconds"].append(duration)
        if is_error:
            cls.metrics["ollama_errors_total"] += 1

def run_exporter(port: int = 8000):
    server = HTTPServer(("0.0.0.0", port), OllamaExporter)
    print(f"Ollama exporter running on :{port}/metrics")
    server.serve_forever()

# Start exporter in background thread
# threading.Thread(target=run_exporter, daemon=True).start()
# Then configure Prometheus to scrape http://localhost:8000/metrics

❓ よくある質問

Q Ollamaに組み込みのPrometheusエンドポイントはある?
A Ollama自体はPrometheus指標を公開しない。独自のエクスポーター(このレッスンのサンプルなど)を構築するか、Ollama APIを使用して指標を収集しPrometheusにプッシュする必要がある。
Q Grafanaダッシュボードを素早く設定するには?
A Grafanaには既製のNode ExporterとDCGMダッシュボードテンプレートがある(IDでインポート:1860がNode、12239がDCGM)。Ollamaカスタム指標は独自のダッシュボード構築が必要。
Q ログ量が大きすぎる場合は?
A ログローテーションを設定(systemdではlogrotate)またはLokiをファイルログの代わりに使用。エラーとスロークエリのみログに記録し、通常リクエストはメトリクスで代替。
Q 頻繁すぎるアラートをどう調整する?
A forの期間を延長(1m→5m)、閾値を引き上げ(レイテンシ5s→10s)、重要度で分類(criticalは即時通知、warningはログのみ)。
Q Docker内のOllamaをモニタリングするには?
A Docker statsでCPU/メモリ、nvidia-smiでGPU。またはcAdvisorでコンテナ指標を収集してPrometheusにプッシュ。
Q モニタリングスタックはパフォーマンスに影響する?
A Prometheusの15秒間隔スクレイピングはOllamaに実質的に影響なし。カスタムエクスポーターのヘルスチェックリクエストは非常に小さい(10トークン未満)。

📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐):Ollamaのログを設定し、journalctlでOllamaサービスログを閲覧し、エラーメッセージをフィルタリングする。
  2. 中級(難易度 ⭐⭐):Prometheus + Grafanaモニタリングスタックをデプロイし、Node Exporterダッシュボードをインポートし、システム指標が表示されることを確認する。
  3. 上級(難易度 ⭐⭐⭐):カスタムOllamaエクスポーターを実装し、GrafanaでOllama専用ダッシュボード(レイテンシ、Tokens/s、エラー率)を作成し、2つ以上のアラートルールを設定する。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%