T-CREATOR

gpt-oss 運用監視ダッシュボード設計:Prometheus/Grafana/OTel で可観測性強化

gpt-oss 運用監視ダッシュボード設計:Prometheus/Grafana/OTel で可観測性強化

gpt-oss の本番環境運用では、モデルの推論速度、リソース使用率、エラー率などをリアルタイムで監視することが不可欠です。せっかく高性能な LLM を構築しても、運用中のボトルネックやエラーを早期発見できなければ、ユーザー体験は大きく損なわれてしまいます。

本記事では、Prometheus、Grafana、OpenTelemetry(OTel)を組み合わせた可観測性基盤の設計方法を解説します。これらのツールを適切に組み合わせることで、メトリクス収集・可視化・アラート発行の仕組みを効率的に構築できるのです。

実際の運用現場で求められる監視項目の選定から、ダッシュボードの設計パターン、トラブルシューティング時の調査フローまで、実践的な内容をお届けします。

背景

LLM 運用における可観測性の重要性

大規模言語モデル(LLM)の運用は、従来の Web アプリケーションとは異なる特性を持ちます。推論処理は CPU/GPU の計算リソースを大量に消費し、レスポンスタイムは数百ミリ秒から数秒と幅があり、メモリ使用量も動的に変化します。

これらの特性により、LLM の運用では以下のような可観測性が求められます。

| 監視領域 | 主要メトリクス | 監視目的 | アラート閾値例 | |#|#|#|#| | 1 | パフォーマンス | レイテンシ、スループット | ユーザー体験維持 | P95 > 3 秒 | | 2 | リソース | GPU 使用率、メモリ使用量 | リソース枯渇防止 | GPU > 90% | | 3 | エラー率 | HTTP 5xx、モデルエラー | 可用性確保 | エラー率 > 5% | | 4 | モデル品質 | トークン生成速度、出力品質 | 品質維持 | 生成速度 < 10 tok/s |

従来のアプリケーション監視では捉えきれない、LLM 特有の動作パターンを可視化する必要があります。

可観測性を構成する三つの柱

現代の可観測性(Observability)は、以下の三つの柱で構成されます。

以下の図は、可観測性を構成する三つの要素とその関係性を示しています。

mermaidflowchart TB
    obs[可観測性<br/>Observability]

    obs --> metrics[メトリクス<br/>Metrics]
    obs --> logs[ログ<br/>Logs]
    obs --> traces[トレース<br/>Traces]

    metrics --> prom[Prometheus<br/>時系列データ収集]
    logs --> loki[Loki / Elasticsearch<br/>構造化ログ管理]
    traces --> jaeger[Jaeger / Tempo<br/>分散トレーシング]

    prom --> grafana[Grafana<br/>統合可視化]
    loki --> grafana
    jaeger --> grafana

    grafana --> dashboard[ダッシュボード]
    grafana --> alert[アラート]

    style obs fill:#e1f5fe
    style grafana fill:#fff9c4
    style dashboard fill:#c8e6c9
    style alert fill:#ffccbc

図で理解できる要点:

  • 可観測性は三つの異なる視点からシステムを理解する
  • それぞれ専用の収集・保存ツールが存在する
  • Grafana が統合可視化レイヤーとして機能する

メトリクス(Metrics) は、数値データの時系列変化を記録します。CPU 使用率、リクエスト数、レスポンスタイムなど、定量的な指標を扱います。

ログ(Logs) は、システムが出力するイベント記録です。エラーメッセージ、デバッグ情報、トランザクション記録などが含まれます。

トレース(Traces) は、一つのリクエストがシステム内をどう流れたかを追跡します。分散システムでのボトルネック特定に有効です。

Prometheus / Grafana / OpenTelemetry の役割分担

これら三つのツールは、それぞれ異なる役割を担いながら連携します。

typescript// 各ツールの役割を TypeScript の型定義で表現
interface ObservabilityStack {
  // Prometheus: メトリクス収集・保存
  prometheus: {
    role: 'メトリクス収集エンジン';
    capabilities: string[];
    storage: 'TSDB(時系列データベース)';
  };

  // Grafana: 可視化・ダッシュボード
  grafana: {
    role: 'データ可視化プラットフォーム';
    datasources: string[];
    outputs: string[];
  };

  // OpenTelemetry: 計装・データ収集
  opentelemetry: {
    role: '計装フレームワーク';
    signals: string[];
    exporters: string[];
  };
}

上記の型定義は、各ツールの責務を明確に示しています。

Prometheus は、定期的にアプリケーションからメトリクスを収集(Pull 型)し、時系列データベースに保存します。シンプルで信頼性の高いアーキテクチャが特徴です。

Grafana は、複数のデータソース(Prometheus、Loki、Jaeger など)からデータを取得し、美しいダッシュボードとして可視化します。アラート機能も提供します。

OpenTelemetry(OTel) は、アプリケーションコードに埋め込む計装ライブラリです。メトリクス・ログ・トレースを統一的な方法で収集し、様々なバックエンド(Prometheus、Jaeger など)にエクスポートできます。

課題

LLM 特有の監視指標の複雑性

gpt-oss の運用では、従来の Web アプリケーションとは異なる監視指標が必要になります。単純なリクエスト/レスポンスだけでなく、モデル推論の内部状態まで可視化する必要があるのです。

以下の図は、LLM アプリケーションの処理フローと各段階で監視すべき指標を示しています。

mermaidflowchart LR
    user[クライアント] -->|1.リクエスト| lb[ロードバランサ]
    lb -->|2.振り分け| api[API サーバー]
    api -->|3.前処理| preproc[トークナイズ]
    preproc -->|4.推論| model[LLM モデル]
    model -->|5.後処理| postproc[デコード]
    postproc -->|6.レスポンス| user

    lb -.監視.- m1[["接続数<br/>帯域幅"]]
    api -.監視.- m2[["リクエスト数<br/>エラー率"]]
    preproc -.監視.- m3[["トークン数<br/>処理時間"]]
    model -.監視.- m4[["GPU使用率<br/>推論時間<br/>バッチサイズ"]]
    postproc -.監視.- m5[["生成トークン数<br/>品質スコア"]]

    style model fill:#ffccbc
    style m4 fill:#fff9c4

図で理解できる要点:

  • LLM アプリケーションは複数の処理段階で構成される
  • 各段階で異なる種類のメトリクスが必要
  • モデル推論フェーズが最も複雑で重要な監視対象

特に以下の指標は LLM 運用で重要です。

python# LLM 固有のメトリクス定義例
from prometheus_client import Counter, Histogram, Gauge

# トークン処理関連
tokens_processed = Counter(
    'llm_tokens_processed_total',
    'Total number of tokens processed',
    ['model_name', 'operation']  # ラベルでモデル名と処理タイプを分類
)

上記のコードは、処理されたトークン数を累積カウントするメトリクスを定義しています。

python# 推論時間のヒストグラム
inference_duration = Histogram(
    'llm_inference_duration_seconds',
    'Time spent in model inference',
    ['model_name', 'batch_size'],
    buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0]  # レスポンスタイムのバケット
)

ヒストグラムを使うことで、レイテンシの分布(P50、P95、P99 など)を計算できるようになります。

python# GPU メモリ使用量
gpu_memory_used = Gauge(
    'llm_gpu_memory_bytes',
    'GPU memory usage in bytes',
    ['gpu_id', 'model_name']
)

これらのメトリクスを適切に定義し、収集することが LLM 監視の第一歩です。

分散システムにおけるトレーシングの困難さ

gpt-oss を本番運用する際、多くの場合は分散アーキテクチャを採用します。API ゲートウェイ、複数のモデルサーバー、キャッシュレイヤー、データベースなど、複数のコンポーネントが連携します。

この環境では、一つのユーザーリクエストが複数のサービスを横断します。レイテンシが高い場合、どのコンポーネントがボトルネックなのかを特定するのが困難になるのです。

typescript// 分散トレーシングの課題を示す型定義
interface DistributedRequest {
  traceId: string; // リクエスト全体を識別する ID
  spans: RequestSpan[]; // 各サービスでの処理単位
}

interface RequestSpan {
  spanId: string; // この処理単位の ID
  parentSpanId?: string; // 親の処理単位 ID
  serviceName: string; // サービス名
  operationName: string; // 処理名
  startTime: number; // 開始時刻
  duration: number; // 処理時間
  tags: Record<string, any>; // メタデータ
  logs: LogEntry[]; // この処理内のログ
}

上記の型定義は、分散トレーシングで必要となるデータ構造を示しています。

typescript// 問題:各サービスで独自にログを出力すると
// リクエスト全体の流れが見えにくくなる
interface ServiceLog {
  timestamp: string;
  level: string;
  message: string;
  // traceId が無いと、このログがどのリクエストのものか不明
}

上記のコードが示すように、トレース ID とスパン ID を適切に管理しないと、リクエストの全体像が見えなくなります。

ダッシュボードの情報過多とアラート疲労

監視項目が増えると、ダッシュボードに表示する情報が増え、かえって重要な情報が埋もれてしまう「情報過多」の問題が発生します。

同様に、アラート閾値を厳しく設定しすぎると、頻繁にアラートが発火し、運用担当者が麻痺してしまう「アラート疲労」も深刻な課題です。

| 問題 | 症状 | 影響 | 対策の方向性 | |#|#|#|#| | 1 | 情報過多 | ダッシュボードに 50+ のグラフ | 重要指標の見落とし | レイヤー分け、目的別分割 | | 2 | アラート疲労 | 1 日 100+ のアラート通知 | 真の障害を見逃す | 閾値の適正化、集約 | | 3 | 文脈欠如 | 数値だけ表示 | 原因特定に時間がかかる | 関連メトリクスの併記 | | 4 | 更新遅延 | データ反映に 5 分以上 | リアルタイム対応不可 | スクレイプ間隔の短縮 |

これらの課題を解決するには、設計段階から「誰が、何のために見るのか」を明確にする必要があります。

解決策

OpenTelemetry による統一的な計装設計

OpenTelemetry(OTel)を使うことで、メトリクス・ログ・トレースを統一的な方法で収集できます。コードへの埋め込み(計装)も一度で済み、バックエンドの変更にも柔軟に対応可能です。

以下は、gpt-oss の推論エンドポイントに OTel を組み込む例です。

typescriptimport {
  trace,
  metrics,
  context,
} from '@opentelemetry/api';
import {
  MeterProvider,
  PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';

// Prometheus エクスポーターの設定
const prometheusExporter = new PrometheusExporter(
  {
    port: 9464, // Prometheus がスクレイプするポート
  },
  () => {
    console.log('Prometheus exporter started on port 9464');
  }
);

上記のコードは、Prometheus にメトリクスを公開するエクスポーターを初期化しています。

typescript// メトリクスプロバイダーの設定
const meterProvider = new MeterProvider({
  readers: [
    new PeriodicExportingMetricReader({
      exporter: prometheusExporter,
      exportIntervalMillis: 1000, // 1秒ごとにメトリクスを更新
    }),
  ],
});

// グローバルに設定
metrics.setGlobalMeterProvider(meterProvider);
const meter = metrics.getMeter('gpt-oss-inference');

メトリクスプロバイダーを設定し、1 秒間隔でメトリクスを更新するようにしています。

typescript// 推論処理のメトリクス定義
const inferenceCounter = meter.createCounter(
  'llm.inference.requests',
  {
    description: 'Number of inference requests',
  }
);

const inferenceHistogram = meter.createHistogram(
  'llm.inference.duration',
  {
    description: 'Inference duration in seconds',
    unit: 'seconds',
  }
);

const tokensProcessed = meter.createCounter(
  'llm.tokens.processed',
  {
    description: 'Total tokens processed',
  }
);

ここでは、推論リクエスト数、推論時間、トークン数という三つの重要なメトリクスを定義しています。

typescript// 実際の推論処理への計装
export async function runInference(
  modelName: string,
  prompt: string
): Promise<string> {
  const startTime = Date.now();

  // トレーシングのためのスパン作成
  const tracer = trace.getTracer('gpt-oss-inference');
  const span = tracer.startSpan('inference', {
    attributes: {
      'model.name': modelName,
      'prompt.length': prompt.length,
    },
  });

  try {
    // 推論処理の実行
    const result = await executeModel(modelName, prompt);

    // メトリクスの記録
    const duration = (Date.now() - startTime) / 1000;
    inferenceCounter.add(1, {
      model: modelName,
      status: 'success',
    });
    inferenceHistogram.record(duration, {
      model: modelName,
    });
    tokensProcessed.add(result.tokenCount, {
      model: modelName,
    });

    // スパンに結果を記録
    span.setAttributes({
      'response.token_count': result.tokenCount,
      'response.duration_ms': duration * 1000,
    });
    span.setStatus({ code: 1 }); // OK

    return result.text;
  } catch (error) {
    // エラー時のメトリクス
    inferenceCounter.add(1, {
      model: modelName,
      status: 'error',
    });
    span.setStatus({ code: 2, message: error.message }); // ERROR
    span.recordException(error);
    throw error;
  } finally {
    span.end();
  }
}

この関数は、推論処理の前後でメトリクスを記録し、トレースのスパンも作成しています。エラー時にも適切に記録される仕組みです。

Prometheus によるメトリクス収集設計

OpenTelemetry が公開したメトリクスを、Prometheus が定期的に収集(スクレイプ)します。

以下は、Prometheus の設定ファイル例です。

yaml# prometheus.yml
# グローバル設定
global:
  scrape_interval: 15s # 15秒ごとにメトリクスを収集
  evaluation_interval: 15s # アラートルールの評価間隔

  # すべてのメトリクスに自動付与されるラベル
  external_labels:
    cluster: 'gpt-oss-production'
    region: 'ap-northeast-1'

グローバル設定では、収集間隔とすべてのメトリクスに付与されるラベルを定義します。

yaml# アラートマネージャーの設定
alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - 'alertmanager:9093'

アラート通知を処理する Alertmanager の接続先を設定しています。

yaml# アラートルールファイルの読み込み
rule_files:
  - '/etc/prometheus/rules/*.yml'

アラートルールは別ファイルで管理し、ここで読み込みます。

yaml# スクレイプ対象の設定
scrape_configs:
  # gpt-oss API サーバーからのメトリクス収集
  - job_name: 'gpt-oss-api'
    static_configs:
      - targets:
          - 'gpt-oss-api-1:9464'
          - 'gpt-oss-api-2:9464'
          - 'gpt-oss-api-3:9464'
    relabel_configs:
      # インスタンスラベルをホスト名に変更
      - source_labels: [__address__]
        regex: '([^:]+):.*'
        target_label: instance
        replacement: '$1'

各 API サーバーのメトリクスエンドポイントを定義しています。relabel_configs でラベルをカスタマイズできます。

yaml# GPU メトリクスの収集(nvidia-gpu-exporter 使用)
- job_name: 'gpu-metrics'
  static_configs:
    - targets:
        - 'gpu-node-1:9445'
        - 'gpu-node-2:9445'
  metric_relabel_configs:
    # 不要なメトリクスをドロップ
    - source_labels: [__name__]
      regex: 'nvidia_gpu_power_state'
      action: drop

GPU ノードからは専用の Exporter でメトリクスを収集します。不要なメトリクスはドロップして保存容量を節約します。

yaml# Node Exporter(サーバーの基本メトリクス)
- job_name: 'node-exporter'
  static_configs:
    - targets:
        - 'node-1:9100'
        - 'node-2:9100'
        - 'node-3:9100'

CPU、メモリ、ディスクなどのシステムメトリクスも収集します。

Grafana ダッシュボード設計のベストプラクティス

Grafana では、収集したメトリクスを視覚的にわかりやすく表示します。ダッシュボード設計では、「誰が見るのか」を明確にし、目的に応じて複数のダッシュボードを作成するのがベストプラクティスです。

レイヤー別ダッシュボード構成

以下の図は、推奨されるダッシュボードの階層構造を示しています。

mermaidflowchart TD
    db[ダッシュボード階層]

    db --> exec[エグゼクティブ<br/>ダッシュボード]
    db --> ops[運用<br/>ダッシュボード]
    db --> dev[開発<br/>ダッシュボード]

    exec --> exec1[["全体稼働率<br/>SLA達成率<br/>コスト推移"]]

    ops --> ops1[["リアルタイム監視<br/>アラート一覧<br/>リソース使用率"]]
    ops --> ops2[["ログ分析<br/>エラー追跡<br/>パフォーマンス"]]

    dev --> dev1[["APIレスポンス<br/>モデル精度<br/>トークン統計"]]
    dev --> dev2[["デバッグ情報<br/>トレース詳細<br/>キャッシュ効率"]]

    style exec fill:#e1f5fe
    style ops fill:#fff9c4
    style dev fill:#c8e6c9

図で理解できる要点:

  • 見る人の役割に応じてダッシュボードを分ける
  • エグゼクティブ層は高レベルな KPI のみ
  • 運用チームはリアルタイムな詳細情報
  • 開発チームは技術的な深掘り情報

パネル構成の実装例

以下は、Grafana のダッシュボード定義(JSON 形式)の一部です。実際には Grafana UI で作成することが多いですが、コードで管理することも可能です。

json{
  "dashboard": {
    "title": "gpt-oss 運用監視ダッシュボード",
    "tags": ["llm", "production", "monitoring"],
    "timezone": "Asia/Tokyo",
    "panels": [
      {
        "id": 1,
        "title": "リクエスト数(1分あたり)",
        "type": "graph",
        "datasource": "Prometheus",
        "targets": [
          {
            "expr": "rate(llm_inference_requests_total[1m])",
            "legendFormat": "{{model}} - {{status}}"
          }
        ],
        "gridPos": {
          "h": 8,
          "w": 12,
          "x": 0,
          "y": 0
        }
      }
    ]
  }
}

このパネルは、1 分あたりの推論リクエスト数を、モデル名とステータス(成功/失敗)別に表示します。

typescript// Grafana ダッシュボードを TypeScript で定義する例
// (grafonnet などのライブラリを使用)
interface GrafanaPanel {
  title: string;
  type: 'graph' | 'singlestat' | 'table' | 'heatmap';
  queries: PrometheusQuery[];
  layout: { x: number; y: number; w: number; h: number };
}

interface PrometheusQuery {
  expr: string; // PromQL クエリ
  legend: string; // 凡例のフォーマット
  interval?: string; // データポイントの間隔
}

上記は、ダッシュボードをコードで管理するための型定義です。

typescript// リクエストレイテンシのパネル定義
const latencyPanel: GrafanaPanel = {
  title: 'P50/P95/P99 レイテンシ',
  type: 'graph',
  queries: [
    {
      expr: 'histogram_quantile(0.50, rate(llm_inference_duration_bucket[5m]))',
      legend: 'P50',
    },
    {
      expr: 'histogram_quantile(0.95, rate(llm_inference_duration_bucket[5m]))',
      legend: 'P95',
    },
    {
      expr: 'histogram_quantile(0.99, rate(llm_inference_duration_bucket[5m]))',
      legend: 'P99',
    },
  ],
  layout: { x: 12, y: 0, w: 12, h: 8 },
};

P50、P95、P99 のパーセンタイルレイテンシを表示するパネル定義です。ヒストグラムメトリクスから計算しています。

typescript// GPU 使用率のパネル
const gpuPanel: GrafanaPanel = {
  title: 'GPU 使用率',
  type: 'graph',
  queries: [
    {
      expr: 'nvidia_gpu_utilization{job="gpu-metrics"}',
      legend: 'GPU {{gpu_id}} - {{instance}}',
      interval: '30s',
    },
  ],
  layout: { x: 0, y: 8, w: 12, h: 8 },
};

各 GPU の使用率を時系列グラフで表示します。

アラートルールの設計

Prometheus では、メトリクスに基づいてアラートを定義できます。

yaml# /etc/prometheus/rules/llm-alerts.yml
groups:
  - name: llm_inference_alerts
    interval: 30s # 30秒ごとにルールを評価

    rules:
      # 高レイテンシアラート
      - alert: HighInferenceLatency
        expr: |
          histogram_quantile(0.95,
            rate(llm_inference_duration_bucket[5m])
          ) > 3
        for: 2m # 2分間継続したら発火
        labels:
          severity: warning
          component: inference
        annotations:
          summary: '推論レイテンシが高い状態が継続しています'
          description: 'P95 レイテンシが {{ $value }} 秒です(閾値: 3秒)'

P95 レイテンシが 3 秒を超える状態が 2 分間続いたら警告を発します。

yaml# エラー率アラート
- alert: HighErrorRate
  expr: |
    (
      rate(llm_inference_requests_total{status="error"}[5m])
      /
      rate(llm_inference_requests_total[5m])
    ) > 0.05
  for: 1m
  labels:
    severity: critical
    component: inference
  annotations:
    summary: '推論エラー率が異常に高くなっています'
    description: 'エラー率: {{ $value | humanizePercentage }}(閾値: 5%)'

エラー率が 5% を超えたら、より深刻な critical レベルのアラートとして発火します。

yaml# GPU メモリ不足アラート
- alert: GPUMemoryHigh
  expr: |
    nvidia_gpu_memory_used_bytes / nvidia_gpu_memory_total_bytes > 0.9
  for: 5m
  labels:
    severity: warning
    component: gpu
  annotations:
    summary: 'GPU メモリ使用率が 90% を超えています'
    description: 'GPU {{ $labels.gpu_id }} on {{ $labels.instance }}: {{ $value | humanizePercentage }}'

GPU メモリ使用率が 90% を超えたら警告します。OOM(Out of Memory)を未然に防ぐためのアラートです。

具体例

Docker Compose による監視スタック構築

ここでは、gpt-oss の監視環境を Docker Compose で構築する具体的な手順を示します。

ディレクトリ構成

bashgpt-oss-monitoring/
├── docker-compose.yml           # 全サービスの定義
├── prometheus/
│   ├── prometheus.yml           # Prometheus 設定
│   └── rules/
│       └── llm-alerts.yml       # アラートルール
├── grafana/
│   ├── provisioning/
│   │   ├── datasources/
│   │   │   └── prometheus.yml   # データソース自動設定
│   │   └── dashboards/
│   │       ├── dashboard.yml    # ダッシュボード自動読み込み
│   │       └── llm-dashboard.json  # ダッシュボード定義
│   └── grafana.ini              # Grafana 設定
└── otel-collector/
    └── config.yml               # OpenTelemetry Collector 設定

Docker Compose 設定

以下は、Prometheus、Grafana、OpenTelemetry Collector を起動する設定です。

yaml# docker-compose.yml
version: '3.8'

services:
  # Prometheus サービス
  prometheus:
    image: prom/prometheus:v2.45.0
    container_name: prometheus
    ports:
      - '9090:9090'
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus/rules:/etc/prometheus/rules
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/usr/share/prometheus/console_libraries'
      - '--web.console.templates=/usr/share/prometheus/consoles'
    restart: unless-stopped

Prometheus コンテナの設定です。設定ファイルとアラートルールをマウントしています。

yaml# Grafana サービス
grafana:
  image: grafana/grafana:10.0.3
  container_name: grafana
  ports:
    - '3000:3000'
  volumes:
    - ./grafana/provisioning:/etc/grafana/provisioning
    - ./grafana/grafana.ini:/etc/grafana/grafana.ini
    - grafana-data:/var/lib/grafana
  environment:
    - GF_SECURITY_ADMIN_USER=admin
    - GF_SECURITY_ADMIN_PASSWORD=admin123
    - GF_USERS_ALLOW_SIGN_UP=false
  depends_on:
    - prometheus
  restart: unless-stopped

Grafana コンテナの設定です。初期管理者の認証情報を環境変数で設定しています。

yaml# OpenTelemetry Collector
otel-collector:
  image: otel/opentelemetry-collector:0.82.0
  container_name: otel-collector
  ports:
    - '4317:4317' # OTLP gRPC receiver
    - '4318:4318' # OTLP HTTP receiver
    - '8888:8888' # Prometheus metrics from collector itself
  volumes:
    - ./otel-collector/config.yml:/etc/otel/config.yml
  command: ['--config=/etc/otel/config.yml']
  depends_on:
    - prometheus
  restart: unless-stopped

OpenTelemetry Collector は、アプリケーションからメトリクス・トレースを受け取り、Prometheus などにエクスポートします。

yaml  # Node Exporter(ホストメトリクス収集)
  node-exporter:
    image: prom/node-exporter:v1.6.1
    container_name: node-exporter
    ports:
      - "9100:9100"
    command:
      - '--path.rootfs=/host'
    volumes:
      - '/:/host:ro,rslave'
    restart: unless-stopped

volumes:
  prometheus-data:
  grafana-data:

Node Exporter は、CPU、メモリ、ディスクなどのシステムメトリクスを収集します。

OpenTelemetry Collector 設定

以下は、OTel Collector の設定ファイルです。

yaml# otel-collector/config.yml
receivers:
  # OTLP 受信(gRPC と HTTP)
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  # Prometheus メトリクス受信
  prometheus:
    config:
      scrape_configs:
        - job_name: 'otel-collector'
          scrape_interval: 10s
          static_configs:
            - targets: ['0.0.0.0:8888']

アプリケーションから OTLP プロトコルでデータを受信する設定です。

yamlprocessors:
  # バッチ処理(効率化のため)
  batch:
    timeout: 10s
    send_batch_size: 1024

  # メモリ制限
  memory_limiter:
    check_interval: 1s
    limit_mib: 512

受信したデータをバッチ処理し、メモリ使用量を制限します。

yamlexporters:
  # Prometheus へのエクスポート
  prometheus:
    endpoint: '0.0.0.0:8889'
    namespace: 'otel'

  # ログ出力(デバッグ用)
  logging:
    loglevel: info

メトリクスを Prometheus 形式で公開します。

yamlservice:
  pipelines:
    # メトリクスパイプライン
    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, batch]
      exporters: [prometheus, logging]

    # トレースパイプライン
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [logging]

受信したデータをどう処理してどこにエクスポートするかを定義しています。

gpt-oss アプリケーションへの計装実装

ここでは、実際の gpt-oss API サーバーに OpenTelemetry を組み込む例を示します。

Node.js / Express での実装

typescript// src/instrumentation.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

計装に必要な各種エクスポーターをインポートします。

typescript// Prometheus メトリクスエクスポーターの設定
const prometheusExporter = new PrometheusExporter({
  port: 9464,
});

// トレースエクスポーターの設定(OTel Collector へ送信)
const traceExporter = new OTLPTraceExporter({
  url: 'http://otel-collector:4318/v1/traces',
});

Prometheus へのメトリクス公開と、OTel Collector へのトレース送信を設定します。

typescript// OpenTelemetry SDK の初期化
const sdk = new NodeSDK({
  // 自動計装(Express、HTTP、etc.)
  instrumentations: [getNodeAutoInstrumentations()],

  // メトリクスリーダーの設定
  metricReader: new PeriodicExportingMetricReader({
    exporter: prometheusExporter,
    exportIntervalMillis: 1000,
  }),

  // トレースエクスポーターの設定
  traceExporter: traceExporter,

  // サービス情報
  serviceName: 'gpt-oss-api',
});

// SDK の開始
sdk.start();

SDK を初期化し、自動計装を有効化します。

typescript// プロセス終了時のクリーンアップ
process.on('SIGTERM', () => {
  sdk
    .shutdown()
    .then(() =>
      console.log(
        'OpenTelemetry SDK shut down successfully'
      )
    )
    .catch((error) =>
      console.error('Error shutting down SDK', error)
    )
    .finally(() => process.exit(0));
});

export default sdk;

プロセス終了時に適切にシャットダウンします。

カスタムメトリクスの追加

自動計装では捉えられない LLM 固有のメトリクスを追加します。

typescript// src/metrics/llm-metrics.ts
import { metrics } from '@opentelemetry/api';

const meter = metrics.getMeter('gpt-oss-custom-metrics');

// カスタムメトリクスの定義
export const llmMetrics = {
  // トークン処理数
  tokensProcessed: meter.createCounter(
    'llm.tokens.processed',
    {
      description: 'Total tokens processed by the model',
      unit: 'tokens',
    }
  ),

  // プロンプトトークン数
  promptTokens: meter.createHistogram('llm.tokens.prompt', {
    description: 'Number of tokens in prompts',
    unit: 'tokens',
  }),

  // 完了トークン数
  completionTokens: meter.createHistogram(
    'llm.tokens.completion',
    {
      description: 'Number of tokens in completions',
      unit: 'tokens',
    }
  ),

  // モデル読み込み時間
  modelLoadTime: meter.createHistogram(
    'llm.model.load_time',
    {
      description: 'Time to load model into memory',
      unit: 'seconds',
    }
  ),

  // 同時実行中のリクエスト数
  activeRequests: meter.createUpDownCounter(
    'llm.requests.active',
    {
      description:
        'Number of requests currently being processed',
    }
  ),
};

LLM 運用に必要なカスタムメトリクスを定義します。

API エンドポイントへの計装

typescript// src/routes/inference.ts
import express from 'express';
import { trace, context } from '@opentelemetry/api';
import { llmMetrics } from '../metrics/llm-metrics';

const router = express.Router();
const tracer = trace.getTracer('gpt-oss-api');

// 推論エンドポイント
router.post('/v1/inference', async (req, res) => {
  const { model, prompt, max_tokens = 100 } = req.body;

  // アクティブリクエスト数をインクリメント
  llmMetrics.activeRequests.add(1, { model });

  // トレーシング用のスパン作成
  const span = tracer.startSpan('inference_request', {
    attributes: {
      'model.name': model,
      'request.max_tokens': max_tokens,
      'prompt.length': prompt.length,
    },
  });

  const startTime = Date.now();

推論エンドポイントに計装を追加します。リクエスト開始時にメトリクスとスパンを記録開始します。

typescript  try {
    // プロンプトのトークナイズ
    const tokenizeSpan = tracer.startSpan('tokenize', {
      parent: span,
    });
    const promptTokens = await tokenizePrompt(prompt);
    llmMetrics.promptTokens.record(promptTokens.length, { model });
    tokenizeSpan.end();

    // モデル推論
    const inferenceSpan = tracer.startSpan('model_inference', {
      parent: span,
    });
    const completion = await runModelInference(model, promptTokens, max_tokens);
    inferenceSpan.end();

    // トークン数の記録
    const totalTokens = promptTokens.length + completion.tokens.length;
    llmMetrics.tokensProcessed.add(totalTokens, { model });
    llmMetrics.completionTokens.record(completion.tokens.length, { model });

処理の各段階でメトリクスを記録し、スパンで追跡します。

typescript    // レスポンス
    res.json({
      text: completion.text,
      usage: {
        prompt_tokens: promptTokens.length,
        completion_tokens: completion.tokens.length,
        total_tokens: totalTokens,
      },
      model: model,
    });

    span.setStatus({ code: 1 });  // OK

  } catch (error) {
    span.recordException(error);
    span.setStatus({ code: 2, message: error.message });
    res.status(500).json({ error: error.message });

  } finally {
    // アクティブリクエスト数をデクリメント
    llmMetrics.activeRequests.add(-1, { model });
    span.end();
  }
});

export default router;

エラー時にも適切にメトリクスを記録し、finally ブロックでリソースをクリーンアップします。

ダッシュボードでの可視化例

最後に、Grafana でどのように可視化するかを示します。

メインダッシュボードの構成

以下のようなパネル構成を推奨します。

| パネル名 | 種類 | 表示内容 | PromQL クエリ例 | |#|#|#|#| | 1 | リクエスト数 | Graph | rate(http_requests_total[5m]) | | 2 | エラー率 | Singlestat | rate(http_requests_total{status=~"5.."}[5m]) ​/​ rate(http_requests_total[5m]) | | 3 | レイテンシ | Graph | histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) | | 4 | トークン処理数 | Counter | increase(llm_tokens_processed[1h]) | | 5 | GPU 使用率 | Graph | nvidia_gpu_utilization | | 6 | メモリ使用量 | Graph | process_resident_memory_bytes ​/​ 1024 ​/​ 1024 |

PromQL クエリ実例

以下は、実際に使えるクエリ例です。

promql# 過去 5 分間のリクエスト成功率
(
  rate(llm_inference_requests_total{status="success"}[5m])
  /
  rate(llm_inference_requests_total[5m])
) * 100

成功リクエストの割合をパーセントで表示します。

promql# モデル別の平均トークン生成速度(tokens/second)
rate(llm_tokens_completion[5m])
/
rate(llm_inference_requests_total{status="success"}[5m])

1 リクエストあたりの平均トークン生成数を計算します。

promql# GPU メモリ使用率
(
  nvidia_gpu_memory_used_bytes
  /
  nvidia_gpu_memory_total_bytes
) * 100

GPU メモリ使用率をパーセントで表示します。

promql# P95 推論レイテンシ(モデル別)
histogram_quantile(0.95,
  sum(rate(llm_inference_duration_bucket[5m])) by (le, model)
)

各モデルの P95 レイテンシを計算します。

アラート通知の設定

Grafana のアラート機能で、Slack や Email に通知を送ることができます。

json{
  "alerting": {
    "contactPoints": [
      {
        "name": "Slack - Engineering",
        "type": "slack",
        "settings": {
          "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
          "text": "{{ .CommonAnnotations.summary }}\n{{ .CommonAnnotations.description }}"
        }
      }
    ],
    "policies": [
      {
        "receiver": "Slack - Engineering",
        "group_by": ["alertname", "severity"],
        "group_wait": "30s",
        "group_interval": "5m",
        "repeat_interval": "12h"
      }
    ]
  }
}

Slack への通知設定例です。アラートが発火すると、指定した Webhook URL にメッセージが送信されます。

まとめ

本記事では、gpt-oss の運用監視ダッシュボードを Prometheus、Grafana、OpenTelemetry で構築する方法を解説しました。

可観測性の三つの柱(メトリクス・ログ・トレース)を理解し、それぞれに適したツールを組み合わせることで、LLM アプリケーションの運用品質を大きく向上させることができます。

特に重要なポイントは以下の通りです。

OpenTelemetry による統一的な計装で、コードの変更を最小限に抑えながらメトリクスとトレースを収集できます。一度計装すれば、バックエンドを変更しても対応できる柔軟性があります。

Prometheus の Pull 型アーキテクチャにより、シンプルで信頼性の高いメトリクス収集が実現できます。設定もシンプルで、スケールアウトも容易です。

Grafana のダッシュボード設計では、見る人の役割に応じて複数のダッシュボードを用意することが推奨されます。エグゼクティブ、運用チーム、開発チームで必要な情報は異なるためです。

アラートルールの適切な設計により、真に重要な障害だけを通知し、アラート疲労を防ぐことができます。閾値は運用しながら調整していくことが重要でしょう。

LLM 特有の監視指標(トークン処理数、GPU 使用率、推論レイテンシなど)を適切に収集し、可視化することで、パフォーマンスの問題を早期発見し、ユーザー体験を継続的に改善できます。

監視基盤は一度構築して終わりではありません。運用を続けながら、必要なメトリクスを追加し、ダッシュボードを改善し、アラート閾値を調整していくことが重要です。

関連リンク