T-CREATOR

Gemini CLI のコスト監視ダッシュボード:呼び出し数・トークン・失敗率の可視化

Gemini CLI のコスト監視ダッシュボード:呼び出し数・トークン・失敗率の可視化

Gemini API を CLI で利用する際、コストが予想外に膨らんでしまった経験はありませんか。API の呼び出し数やトークン消費量、失敗率を可視化することで、コストを正確に把握し、最適化の手がかりが得られます。本記事では、Gemini CLI のコスト監視ダッシュボードを構築する方法を、初心者にもわかりやすく解説していきますね。

背景

Gemini API は強力な AI モデルを提供していますが、利用料金はトークン数や API 呼び出し回数に応じて課金されます。CLI から Gemini API を利用する場合、以下のような課題が発生しやすいです。

  • API 呼び出しのたびにトークン数が変動する
  • バッチ処理で大量のリクエストを送信すると、予想外のコストが発生する
  • エラーやタイムアウトによる失敗が隠れたコストになる

これらの情報をリアルタイムで可視化することで、コスト管理が容易になります。

以下の図は、Gemini CLI からダッシュボードへのデータフローを示しています。

mermaidflowchart LR
  cli["Gemini CLI"] -->|API 呼び出し| gemini["Gemini API"]
  cli -->|ログ記録| logger["ログ収集"]
  logger -->|メトリクス集計| aggregator["集計処理"]
  aggregator -->|データ保存| db[("データベース")]
  db -->|可視化| dashboard["ダッシュボード"]

API 呼び出しごとにログを記録し、集計処理を経てダッシュボードに反映する流れです。

課題

Gemini CLI を使った開発や運用において、コスト監視には以下の課題があります。

トークン消費量の把握が困難

API レスポンスにはトークン数が含まれますが、CLI で確認するには手動でログを追う必要があります。リアルタイムで確認できないため、コストが膨らんでいることに気づきにくいですね。

失敗率の影響が見えにくい

API 呼び出しが失敗した場合、リトライ処理が発生します。このリトライが積み重なると、無駄なコストが発生してしまいます。失敗率を可視化することで、問題のある処理を特定できるでしょう。

呼び出し数の集計が手作業

複数のスクリプトや処理で API を利用している場合、全体の呼び出し数を把握するには、ログファイルを手動で集計する必要があります。これは非効率で、見落としも発生しやすいです。

以下の図は、課題となるポイントを整理したものです。

mermaidflowchart TB
  issue1["トークン消費量の<br/>把握が困難"]
  issue2["失敗率の<br/>影響が見えにくい"]
  issue3["呼び出し数の<br/>集計が手作業"]

  issue1 --> result["コスト超過"]
  issue2 --> result
  issue3 --> result

これらの課題が重なることで、コスト超過のリスクが高まります。

解決策

上記の課題を解決するため、以下の 3 つのステップでコスト監視ダッシュボードを構築します。

ログ収集の自動化

API 呼び出しごとに、以下の情報を自動的にログファイルに記録します。

  • タイムスタンプ
  • リクエスト内容(プロンプト、モデル名)
  • レスポンス内容(トークン数、成功/失敗)
  • エラーメッセージ(失敗時)

メトリクスの集計処理

ログファイルを定期的に読み込み、以下のメトリクスを集計します。

  • 総呼び出し数
  • 総トークン数(入力・出力別)
  • 失敗率(失敗数 / 総呼び出し数)
  • 時間帯別の呼び出し傾向

ダッシュボードの構築

集計したメトリクスを Web ダッシュボードで可視化します。グラフやテーブルを使い、直感的に理解できる形で表示しましょう。

以下の図は、解決策の全体構成を示しています。

mermaidflowchart TB
  step1["ログ収集の<br/>自動化"]
  step2["メトリクスの<br/>集計処理"]
  step3["ダッシュボードの<br/>構築"]

  step1 -->|ログファイル| step2
  step2 -->|集計データ| step3
  step3 -->|可視化| user["運用者"]

段階的に実装することで、確実にコスト監視体制を整えられます。

具体例

ここからは、実際のコードを使ってダッシュボードを構築していきます。TypeScript と Node.js を使用し、Gemini CLI からのログ収集、集計、可視化までを実装しますね。

プロジェクトのセットアップ

まず、プロジェクトを初期化し、必要なパッケージをインストールします。

bashyarn init -y
yarn add @google/generative-ai express sqlite3
yarn add -D typescript @types/node @types/express ts-node

TypeScript 設定ファイルの作成

TypeScript の設定ファイルを用意します。

typescript{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

ログ収集機能の実装

Gemini API を呼び出す際に、自動的にログを記録する関数を作成します。この関数は API 呼び出しをラップし、結果を JSON 形式でファイルに保存します。

typescript// src/logger.ts

import fs from 'fs';
import path from 'path';

// ログエントリの型定義
interface LogEntry {
  timestamp: string;
  model: string;
  prompt: string;
  inputTokens: number;
  outputTokens: number;
  success: boolean;
  errorMessage?: string;
}

// ログファイルのパス
const LOG_FILE = path.join(
  __dirname,
  '../logs/api-calls.jsonl'
);

上記のコードでは、ログエントリの型と保存先のパスを定義しています。

typescript// src/logger.ts (続き)

// ログディレクトリの作成
export function ensureLogDirectory(): void {
  const logDir = path.dirname(LOG_FILE);
  if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
  }
}

ログファイルを保存するディレクトリが存在しない場合は、自動的に作成します。

typescript// src/logger.ts (続き)

// ログエントリの記録
export function logApiCall(entry: LogEntry): void {
  ensureLogDirectory();
  const logLine = JSON.stringify(entry) + '\n';
  fs.appendFileSync(LOG_FILE, logLine, 'utf-8');
}

各 API 呼び出しの情報を JSON Lines 形式(1 行 1 JSON)でファイルに追記します。この形式は、後で行単位で読み込みやすく、集計処理に適していますね。

Gemini API 呼び出しのラッパー実装

Gemini API を呼び出し、自動的にログを記録するラッパー関数を作成します。

typescript// src/gemini-client.ts

import { GoogleGenerativeAI } from '@google/generative-ai';
import { logApiCall } from './logger';

// Gemini API クライアントの初期化
const genAI = new GoogleGenerativeAI(
  process.env.GEMINI_API_KEY || ''
);

環境変数から API キーを読み込み、クライアントを初期化します。

typescript// src/gemini-client.ts (続き)

// プロンプトの型定義
interface GenerateOptions {
  model: string;
  prompt: string;
}

// レスポンスの型定義
interface GenerateResponse {
  text: string;
  inputTokens: number;
  outputTokens: number;
}

API 呼び出しのオプションとレスポンスの型を定義しています。

typescript// src/gemini-client.ts (続き)

// Gemini API 呼び出しとログ記録
export async function generateWithLogging(
  options: GenerateOptions
): Promise<GenerateResponse> {
  const startTime = new Date().toISOString();

  try {
    // モデルの取得
    const model = genAI.getGenerativeModel({ model: options.model });

    // API 呼び出し
    const result = await model.generateContent(options.prompt);
    const response = await result.response;
    const text = response.text();

モデルを取得し、プロンプトを送信します。レスポンスからテキストを抽出しますね。

typescript// src/gemini-client.ts (続き)

// トークン数の取得(レスポンスメタデータから)
const usageMetadata = response.usageMetadata;
const inputTokens = usageMetadata?.promptTokenCount || 0;
const outputTokens =
  usageMetadata?.candidatesTokenCount || 0;

// 成功ログの記録
logApiCall({
  timestamp: startTime,
  model: options.model,
  prompt: options.prompt,
  inputTokens,
  outputTokens,
  success: true,
});

レスポンスメタデータからトークン数を取得し、成功ログを記録します。

typescript// src/gemini-client.ts (続き)

    return {
      text,
      inputTokens,
      outputTokens,
    };
  } catch (error) {
    // 失敗ログの記録
    logApiCall({
      timestamp: startTime,
      model: options.model,
      prompt: options.prompt,
      inputTokens: 0,
      outputTokens: 0,
      success: false,
      errorMessage: error instanceof Error ? error.message : 'Unknown error',
    });

    throw error;
  }
}

エラーが発生した場合は、失敗ログを記録してエラーを再スローします。これにより、呼び出し元でエラーハンドリングができますね。

集計処理の実装

ログファイルを読み込み、メトリクスを集計する関数を作成します。

typescript// src/aggregator.ts

import fs from 'fs';
import path from 'path';

const LOG_FILE = path.join(
  __dirname,
  '../logs/api-calls.jsonl'
);

// 集計結果の型定義
interface Metrics {
  totalCalls: number;
  successfulCalls: number;
  failedCalls: number;
  failureRate: number;
  totalInputTokens: number;
  totalOutputTokens: number;
  totalTokens: number;
  callsByHour: Record<string, number>;
}

集計結果の型を定義しています。呼び出し数、トークン数、失敗率、時間帯別の呼び出し数を含みます。

typescript// src/aggregator.ts (続き)

// ログファイルからメトリクスを集計
export function aggregateMetrics(): Metrics {
  // 初期値の設定
  const metrics: Metrics = {
    totalCalls: 0,
    successfulCalls: 0,
    failedCalls: 0,
    failureRate: 0,
    totalInputTokens: 0,
    totalOutputTokens: 0,
    totalTokens: 0,
    callsByHour: {},
  };

集計結果を格納するオブジェクトを初期化します。

typescript// src/aggregator.ts (続き)

// ログファイルの存在確認
if (!fs.existsSync(LOG_FILE)) {
  return metrics;
}

// ログファイルの読み込み
const logContent = fs.readFileSync(LOG_FILE, 'utf-8');
const lines = logContent
  .trim()
  .split('\n')
  .filter((line) => line.length > 0);

ログファイルを読み込み、行ごとに分割します。空行は除外しますね。

typescript// src/aggregator.ts (続き)

  // 各ログエントリの集計
  for (const line of lines) {
    try {
      const entry = JSON.parse(line);

      // 総呼び出し数のカウント
      metrics.totalCalls++;

      // 成功・失敗のカウント
      if (entry.success) {
        metrics.successfulCalls++;
        metrics.totalInputTokens += entry.inputTokens || 0;
        metrics.totalOutputTokens += entry.outputTokens || 0;
      } else {
        metrics.failedCalls++;
      }

各ログエントリを解析し、成功・失敗をカウントします。成功した場合のみトークン数を集計しますね。

typescript// src/aggregator.ts (続き)

      // 時間帯別の呼び出し数を集計
      const timestamp = new Date(entry.timestamp);
      const hour = timestamp.getHours().toString().padStart(2, '0');
      metrics.callsByHour[hour] = (metrics.callsByHour[hour] || 0) + 1;

    } catch (error) {
      // JSON パースエラーは無視
      console.error('Failed to parse log entry:', line);
    }
  }

タイムスタンプから時刻を抽出し、時間帯別の呼び出し数を集計します。パースエラーは無視して処理を継続しますね。

typescript// src/aggregator.ts (続き)

  // 失敗率と総トークン数の計算
  metrics.failureRate = metrics.totalCalls > 0
    ? (metrics.failedCalls / metrics.totalCalls) * 100
    : 0;
  metrics.totalTokens = metrics.totalInputTokens + metrics.totalOutputTokens;

  return metrics;
}

失敗率と総トークン数を計算して返します。

ダッシュボード API の実装

集計したメトリクスを JSON で返す API サーバーを構築します。

typescript// src/server.ts

import express from 'express';
import { aggregateMetrics } from './aggregator';

const app = express();
const PORT = process.env.PORT || 3000;

// CORS の設定(必要に応じて)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header(
    'Access-Control-Allow-Headers',
    'Content-Type'
  );
  next();
});

Express サーバーを初期化し、CORS ヘッダーを設定します。

typescript// src/server.ts (続き)

// メトリクス取得エンドポイント
app.get('/api/metrics', (req, res) => {
  try {
    const metrics = aggregateMetrics();
    res.json(metrics);
  } catch (error) {
    console.error('Error aggregating metrics:', error);
    res
      .status(500)
      .json({ error: 'Failed to aggregate metrics' });
  }
});

​/​api​/​metrics エンドポイントで集計結果を JSON 形式で返します。エラーが発生した場合は 500 エラーを返しますね。

typescript// src/server.ts (続き)

// 静的ファイルの配信(ダッシュボード HTML)
app.use(express.static('public'));

// サーバーの起動
app.listen(PORT, () => {
  console.log(
    `Dashboard server running at http://localhost:${PORT}`
  );
});

静的ファイル(HTML、CSS、JavaScript)を配信し、サーバーを起動します。

ダッシュボード HTML の作成

メトリクスを可視化するシンプルな HTML ページを作成します。

html<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Gemini CLI コスト監視ダッシュボード</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
      background-color: #f5f5f5;
    }
    h1 {
      color: #333;
      text-align: center;
    }

基本的な HTML 構造とスタイルを定義しています。

html    .metrics-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      gap: 20px;
      margin-bottom: 30px;
    }
    .metric-card {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    .metric-value {
      font-size: 32px;
      font-weight: bold;
      color: #1976d2;
    }
    .metric-label {
      font-size: 14px;
      color: #666;
      margin-top: 5px;
    }
  </style>
</head>

グリッドレイアウトとカードスタイルを定義しています。レスポンシブデザインで、画面サイズに応じてカードが並びますね。

html<body>
  <h1>Gemini CLI コスト監視ダッシュボード</h1>

  <div class="metrics-grid" id="metricsGrid">
    <!-- JavaScript で動的に生成 -->
  </div>

  <div class="metric-card">
    <h2>時間帯別呼び出し数</h2>
    <canvas id="hourlyChart"></canvas>
  </div>
</body>

メトリクスカードとグラフ用のキャンバスを配置します。

html  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  <script>
    // メトリクスの取得と表示
    async function loadMetrics() {
      try {
        const response = await fetch('/api/metrics');
        const metrics = await response.json();

        // メトリクスカードの生成
        displayMetrics(metrics);

        // グラフの描画
        drawHourlyChart(metrics.callsByHour);
      } catch (error) {
        console.error('Failed to load metrics:', error);
      }
    }

API からメトリクスを取得し、表示する関数です。Chart.js を CDN から読み込んでいますね。

html// メトリクスカードの表示 function displayMetrics(metrics) {
const grid = document.getElementById('metricsGrid'); const
cards = [ { label: '総呼び出し数', value: metrics.totalCalls
}, { label: '成功呼び出し数', value: metrics.successfulCalls
}, { label: '失敗呼び出し数', value: metrics.failedCalls },
{ label: '失敗率', value: metrics.failureRate.toFixed(2) +
'%' }, { label: '総入力トークン', value:
metrics.totalInputTokens.toLocaleString() }, { label:
'総出力トークン', value:
metrics.totalOutputTokens.toLocaleString() }, { label:
'総トークン数', value: metrics.totalTokens.toLocaleString()
}, ];

表示するメトリクスの配列を定義しています。失敗率は小数点第 2 位まで表示し、トークン数は桁区切りで読みやすくしますね。

htmlgrid.innerHTML = cards.map(card => `
<div class="metric-card">
  <div class="metric-value">${card.value}</div>
  <div class="metric-label">${card.label}</div>
</div>
`).join(''); }

メトリクスカードを動的に生成して DOM に挿入します。

html// 時間帯別グラフの描画 function
drawHourlyChart(callsByHour) { const hours = Array.from({
length: 24 }, (_, i) => i.toString().padStart(2, '0') );
const data = hours.map(hour => callsByHour[hour] || 0);
const ctx =
document.getElementById('hourlyChart').getContext('2d'); new
Chart(ctx, { type: 'bar', data: { labels: hours.map(h => h +
':00'), datasets: [{ label: '呼び出し数', data: data,
backgroundColor: 'rgba(25, 118, 210, 0.5)', borderColor:
'rgba(25, 118, 210, 1)', borderWidth: 1 }] }, options: {
responsive: true, scales: { y: { beginAtZero: true } } } });
}

Chart.js を使って、時間帯別の呼び出し数を棒グラフで表示します。0 時から 23 時までの 24 時間分のデータを可視化しますね。

html    // ページ読み込み時とリロード時にメトリクスを取得
    loadMetrics();
    setInterval(loadMetrics, 30000); // 30 秒ごとに更新
  </script>
</body>
</html>

ページ読み込み時に一度メトリクスを取得し、その後 30 秒ごとに自動更新します。

実行スクリプトの作成

CLI から簡単に実行できるように、サンプルスクリプトを用意します。

typescript// src/example.ts

import { generateWithLogging } from './gemini-client';

async function main() {
  try {
    // Gemini API の呼び出し例
    const prompts = [
      'TypeScript の型安全性について簡潔に説明してください。',
      'Next.js の SSR と SSG の違いを教えてください。',
      'Docker コンテナのメリットを3つ挙げてください。',
    ];

    for (const prompt of prompts) {
      console.log(`\nPrompt: ${prompt}`);

      const response = await generateWithLogging({
        model: 'gemini-pro',
        prompt,
      });

      console.log(`Response: ${response.text}`);
      console.log(`Tokens: ${response.inputTokens} in, ${response.outputTokens} out`);
    }

複数のプロンプトを順番に送信し、レスポンスとトークン数を表示します。

typescript// src/example.ts (続き)

  } catch (error) {
    console.error('Error:', error);
  }
}

main();

エラーハンドリングを含めて、スクリプトを実行します。

package.json にスクリプトを追加

実行コマンドを簡単にするため、package.json にスクリプトを追加します。

json{
  "scripts": {
    "start:server": "ts-node src/server.ts",
    "start:example": "ts-node src/example.ts",
    "build": "tsc"
  }
}

サーバーとサンプルスクリプトを簡単に起動できるようにしていますね。

環境変数の設定

.env ファイルを作成し、Gemini API キーを設定します。

bashGEMINI_API_KEY=your_api_key_here
PORT=3000

API キーは環境変数で管理し、コードに直接埋め込まないようにします。セキュリティ上重要ですね。

ダッシュボードの起動と確認

以下のコマンドでサーバーを起動します。

bashyarn start:server

別のターミナルでサンプルスクリプトを実行し、ログを記録します。

bashyarn start:example

ブラウザで http:​/​​/​localhost:3000 にアクセスすると、ダッシュボードが表示されます。メトリクスがリアルタイムで更新され、API 呼び出しの状況を一目で把握できますね。

以下の表は、ダッシュボードで確認できる主要メトリクスの一覧です。

#メトリクス名説明用途
1総呼び出し数API の総リクエスト数全体の利用状況を把握
2成功呼び出し数正常に完了したリクエスト数成功率の確認
3失敗呼び出し数エラーが発生したリクエスト数問題の検出
4失敗率失敗数 ÷ 総呼び出し数 × 100システムの安定性評価
5総入力トークンプロンプトのトークン総数入力コストの把握
6総出力トークンレスポンスのトークン総数出力コストの把握
7総トークン数入力 + 出力の合計総コストの推定
8時間帯別呼び出し数各時刻の呼び出し分布ピーク時間の特定

コスト最適化のポイント

ダッシュボードで可視化したメトリクスを元に、以下の最適化を行いましょう。

失敗率が高い場合

  • プロンプトの見直し(長すぎる、不適切な内容)
  • リトライロジックの改善
  • API キーやネットワーク設定の確認

トークン消費が多い場合

  • プロンプトの簡潔化
  • 不要な出力の削減
  • モデルの変更(軽量モデルへの切り替え)

ピーク時間への対処

  • バッチ処理の時間帯をずらす
  • レート制限の設定
  • キャッシュの活用

以下の図は、最適化のサイクルを示しています。

mermaidflowchart LR
  monitor["ダッシュボードで<br/>監視"]
  analyze["メトリクスを<br/>分析"]
  optimize["最適化を<br/>実施"]
  verify["効果を<br/>検証"]

  monitor --> analyze
  analyze --> optimize
  optimize --> verify
  verify --> monitor

継続的に監視・分析・最適化を繰り返すことで、コスト効率が向上します。

まとめ

本記事では、Gemini CLI のコスト監視ダッシュボードを構築する方法を解説しました。ログ収集の自動化、メトリクスの集計、Web ダッシュボードでの可視化という 3 つのステップで、API 呼び出し数、トークン消費量、失敗率をリアルタイムに把握できるようになりましたね。

このダッシュボードを活用することで、以下のメリットが得られます。

  • コストの透明性が向上し、予算管理が容易になる
  • 失敗率を監視することで、問題を早期に発見できる
  • 時間帯別の傾向を把握し、処理タイミングを最適化できる

今後は、コスト予測機能やアラート機能を追加することで、さらに高度な監視体制を構築できるでしょう。ぜひ実際に試してみて、Gemini API のコスト管理を効率化してくださいね。

関連リンク