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 のコスト管理を効率化してくださいね。
関連リンク
articleGemini CLI のコスト監視ダッシュボード:呼び出し数・トークン・失敗率の可視化
articleGemini CLI を中核にした“AI パイプライン”設計:前処理 → 推論 → 後処理の標準化
articleGemini CLI コマンド&フラグ早見表:入出力・温度・安全設定の定番セット
articleGemini CLI を macOS で最短導入:Homebrew・PATH・シェル補完のベストプラクティス
articleGemini CLI と curl + REST の生産性比較:開発速度・再現性・保守性を計測
articleGemini CLI で JSON が壊れる問題を撲滅:出力拘束・スキーマ・再試行の実務
articleGemini CLI のコスト監視ダッシュボード:呼び出し数・トークン・失敗率の可視化
articleGrok アカウント作成から初回設定まで:5 分で完了するスターターガイド
articleFFmpeg コーデック地図 2025:H.264/H.265/AV1/VP9/ProRes/DNxHR の使いどころ
articleESLint の内部構造を覗く:Parser・Scope・Rule・Fixer の連携を図解
articlegpt-oss の量子化別ベンチ比較:INT8/FP16/FP8 の速度・品質トレードオフ
articleDify で実現する RAG 以外の戦略:ツール実行・関数呼び出し・自律エージェントの全体像
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来