Ollama: モニタリングとロギング
モニタリングはOllamaの健康診断——リアルタイムの心拍、指標トレンド、異常アラート、事故の未然防止。
💡 ヒント: Prometheus + Grafanaが最も主流のモニタリングコンビ——Prometheusが定期的な指標収集・保存を担当し、Grafanaが可視化とアラートを担当。Ollamaには組み込みのPrometheusエンドポイントがないため、node_exporter(システム指標)+ DCGMエクスポーター(GPU指標)+ カスタムミドルウェア(ビジネス指標)を組み合わせる必要がある。
📋 前提条件: まず以下を習得していること
- レッスン19: パフォーマンスチューニング
1. 学べること
- Ollamaログ設定と閲覧
- 主要モニタリング指標の収集
- Prometheus + Grafanaモニタリングスタックの構築
- アラートルール設計
- スロークエリ診断とログ分析
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) モニタリングアーキテクチャ
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トークン未満)。
📖 まとめ
- Ollamaログはjournalctl/docker logsで閲覧;OLLAMA_DEBUGで詳細ログを有効化
- 4つのモニタリングカテゴリ:GPU(VRAM/温度)、推論(レイテンシ/速度)、システム(CPU/RAM)、アプリケーション(エラー率)
- カスタムエクスポーターがOllama API指標をPrometheus形式で公開
- Prometheus + Grafanaが標準モニタリングスタック、15分でデプロイ可能
- アラートルールは重要度別に分類:criticalは即時通知、warningはログ後に通知
- スロークエリ診断:ログフィルタリング + GPU状態確認 + リソース分析
📝 練習問題
- 基本(難易度 ⭐):Ollamaのログを設定し、journalctlでOllamaサービスログを閲覧し、エラーメッセージをフィルタリングする。
- 中級(難易度 ⭐⭐):Prometheus + Grafanaモニタリングスタックをデプロイし、Node Exporterダッシュボードをインポートし、システム指標が表示されることを確認する。
- 上級(難易度 ⭐⭐⭐):カスタムOllamaエクスポーターを実装し、GrafanaでOllama専用ダッシュボード(レイテンシ、Tokens/s、エラー率)を作成し、2つ以上のアラートルールを設定する。