Gemini APIを個人プロジェクトから本番サービスへスケールさせるとき、多くの開発者が直面する壁があります。「APIが遅いのはモデルの問題か、ネットワークの問題か」「今月のAPI費用がなぜ急増したのか」「ユーザーからエラー報告があったが、どのリクエストで何が起きたのか」——これらの問いに即座に答えられる仕組みが、オブザーバビリティです。
ここで扱うのはGemini APIを本番運用するために必要なオブザーバビリティ基盤を、構造化ログ設計からコスト追跡ダッシュボードまで、実装コード付きで体系的に解説します。なお、APIのエラーハンドリングやリトライ戦略についてはGemini API エラーハンドリングとリトライパターン、コスト最適化の基本はGemini API コスト最適化ガイド も併せてご覧ください。
オブザーバビリティの3本柱とGemini API
オブザーバビリティは「ログ(Logs)」「メトリクス(Metrics)」「トレース(Traces)」の3本柱で構成されます。Gemini APIの本番運用においては、それぞれ以下の役割を担います。
ログ はAPIリクエストとレスポンスの詳細を記録し、問題発生時の原因調査に使います。プロンプトの内容、モデルのレスポンス、エラーメッセージなど、個別のイベントを時系列で追跡できます。
メトリクス はシステムの健全性を数値で示します。レイテンシの中央値・P95・P99、トークン消費量、エラー率、秒間リクエスト数(RPS)など、集約された統計値をモニタリングします。
トレース は1つのユーザーリクエストがシステム内部をどう流れたかを可視化します。特にRAGパイプラインやマルチエージェントシステムでは、Gemini API呼び出しがどのステップで行われ、どこがボトルネックになっているかを特定するために不可欠です。
構造化ログの設計と実装
Gemini APIのログを効果的に活用するには、構造化ログ(Structured Logging)が重要です。プレーンテキストのログではフィルタリングや集計が難しく、大量のリクエストを処理する本番環境では役に立ちません。
以下は、Python の structlog を使った Gemini API 用の構造化ログ設計です。
import structlog
import time
import uuid
from google import genai
from google.genai import types
# structlog の設定
structlog.configure(
processors = [
structlog.processors.TimeStamper( fmt = "iso" ),
structlog.processors.add_log_level,
structlog.processors.JSONRenderer()
]
)
logger = structlog.get_logger()
class GeminiObservableClient :
"""オブザーバビリティ機能付きGemini APIクライアント"""
def __init__ (self, api_key: str , default_model: str = "gemini-2.5-pro" ):
self .client = genai.Client( api_key = api_key)
self .default_model = default_model
self .metrics = MetricsCollector()
def generate (self, prompt: str , model: str = None ,
config: dict = None , trace_id: str = None ):
"""ログ・メトリクス付きでコンテンツを生成する"""
request_id = str (uuid.uuid4())[: 8 ]
trace_id = trace_id or str (uuid.uuid4())
model_name = model or self .default_model
start_time = time.monotonic()
# リクエストログ
logger.info( "gemini_request_start" ,
request_id = request_id,
trace_id = trace_id,
model = model_name,
prompt_length = len (prompt),
config = config)
try :
response = self .client.models.generate_content(
model = model_name,
contents = prompt,
config = types.GenerateContentConfig( ** (config or {}))
)
elapsed = time.monotonic() - start_time
input_tokens = response.usage_metadata.prompt_token_count
output_tokens = response.usage_metadata.candidates_token_count
# 成功ログ
logger.info( "gemini_request_success" ,
request_id = request_id,
trace_id = trace_id,
model = model_name,
latency_ms = round (elapsed * 1000 , 2 ),
input_tokens = input_tokens,
output_tokens = output_tokens,
total_tokens = input_tokens + output_tokens,
finish_reason = str (response.candidates[ 0 ].finish_reason))
# メトリクス記録
self .metrics.record_request(
model = model_name,
latency = elapsed,
input_tokens = input_tokens,
output_tokens = output_tokens,
success = True
)
return response
except Exception as e:
elapsed = time.monotonic() - start_time
# エラーログ
logger.error( "gemini_request_error" ,
request_id = request_id,
trace_id = trace_id,
model = model_name,
latency_ms = round (elapsed * 1000 , 2 ),
error_type = type (e). __name__ ,
error_message = str (e))
self .metrics.record_request(
model = model_name,
latency = elapsed,
input_tokens = 0 ,
output_tokens = 0 ,
success = False ,
error_type = type (e). __name__
)
raise
このクライアントは、すべてのAPI呼び出しに対して request_id と trace_id を自動付与します。request_id は個々のAPI呼び出しを識別し、trace_id はユーザーリクエスト全体を通して紐づけるための識別子です。
ログ出力は以下のようなJSON形式になります。
# 出力例:
# {"event": "gemini_request_success", "request_id": "a1b2c3d4",
# "trace_id": "550e8400-...", "model": "gemini-2.5-pro",
# "latency_ms": 1523.45, "input_tokens": 256,
# "output_tokens": 1024, "total_tokens": 1280,
# "finish_reason": "STOP", "timestamp": "2026-03-30T10:15:00Z",
# "level": "info"}
メトリクス収集とPrometheus連携
構造化ログだけでは、システム全体の傾向を把握するのは困難です。メトリクス収集の仕組みを構築し、時系列データとして集約することで、レイテンシの推移やトークン消費の傾向を一目で確認できるようになります。
from collections import defaultdict
from dataclasses import dataclass, field
import threading
import time
@dataclass
class RequestMetric :
timestamp: float
model: str
latency: float
input_tokens: int
output_tokens: int
success: bool
error_type: str = ""
class MetricsCollector :
"""Gemini APIメトリクス収集・集約クラス"""
def __init__ (self):
self ._metrics: list[RequestMetric] = []
self ._lock = threading.Lock()
def record_request (self, model: str , latency: float ,
input_tokens: int , output_tokens: int ,
success: bool , error_type: str = "" ):
metric = RequestMetric(
timestamp = time.time(),
model = model,
latency = latency,
input_tokens = input_tokens,
output_tokens = output_tokens,
success = success,
error_type = error_type
)
with self ._lock:
self ._metrics.append(metric)
def get_summary (self, window_minutes: int = 60 ) -> dict :
"""指定期間のメトリクスサマリーを返す"""
cutoff = time.time() - (window_minutes * 60 )
with self ._lock:
recent = [m for m in self ._metrics if m.timestamp > cutoff]
if not recent:
return { "period_minutes" : window_minutes, "total_requests" : 0 }
latencies = sorted ([m.latency for m in recent if m.success])
total_input = sum (m.input_tokens for m in recent)
total_output = sum (m.output_tokens for m in recent)
errors = [m for m in recent if not m.success]
# モデル別の集計
by_model = defaultdict( lambda : { "count" : 0 , "tokens" : 0 })
for m in recent:
by_model[m.model][ "count" ] += 1
by_model[m.model][ "tokens" ] += m.input_tokens + m.output_tokens
return {
"period_minutes" : window_minutes,
"total_requests" : len (recent),
"success_rate" : round (( len (recent) - len (errors)) / len (recent) * 100 , 2 ),
"latency" : {
"p50" : round (latencies[ len (latencies) // 2 ] * 1000 , 2 ) if latencies else 0 ,
"p95" : round (latencies[ int ( len (latencies) * 0.95 )] * 1000 , 2 ) if latencies else 0 ,
"p99" : round (latencies[ int ( len (latencies) * 0.99 )] * 1000 , 2 ) if latencies else 0 ,
},
"tokens" : {
"total_input" : total_input,
"total_output" : total_output,
"total" : total_input + total_output,
},
"errors" : {
"count" : len (errors),
"by_type" : dict (defaultdict( int , {
e.error_type: sum ( 1 for x in errors if x.error_type == e.error_type)
for e in errors
}))
},
"by_model" : dict (by_model),
}
Prometheus形式でエクスポートする場合は、prometheus_client ライブラリと組み合わせます。
from prometheus_client import Counter, Histogram, Gauge, start_http_server
# Prometheusメトリクス定義
gemini_requests_total = Counter(
"gemini_api_requests_total" ,
"Gemini APIリクエスト総数" ,
[ "model" , "status" ]
)
gemini_latency = Histogram(
"gemini_api_latency_seconds" ,
"Gemini APIレイテンシ(秒)" ,
[ "model" ],
buckets = [ 0.1 , 0.25 , 0.5 , 1.0 , 2.5 , 5.0 , 10.0 , 30.0 ]
)
gemini_tokens = Counter(
"gemini_api_tokens_total" ,
"Gemini APIトークン消費量" ,
[ "model" , "direction" ] # direction: input / output
)
gemini_estimated_cost = Counter(
"gemini_api_estimated_cost_usd" ,
"Gemini API推定コスト(USD)" ,
[ "model" ]
)
# メトリクスサーバー起動(ポート9090)
# start_http_server(9090)
コスト追跡システムの構築
Gemini APIの本番運用で最も重視されるメトリクスの一つが、コストです。モデルごとの料金体系を理解し、リアルタイムでコストを追跡する仕組みを構築しましょう。
from dataclasses import dataclass
from typing import Optional
@dataclass
class ModelPricing :
"""モデル別料金(USD / 1Mトークン)"""
input_per_million: float
output_per_million: float
context_cache_per_million: Optional[ float ] = None
# 2026年3月時点のGemini API料金(公式料金ページを定期確認すること)
PRICING = {
"gemini-2.5-pro" : ModelPricing(
input_per_million = 1.25 ,
output_per_million = 10.00 ,
context_cache_per_million = 0.3125
),
"gemini-2.5-flash" : ModelPricing(
input_per_million = 0.15 ,
output_per_million = 0.60 ,
context_cache_per_million = 0.0375
),
"gemini-2.0-flash" : ModelPricing(
input_per_million = 0.10 ,
output_per_million = 0.40
),
"gemini-2.0-flash-lite" : ModelPricing(
input_per_million = 0.075 ,
output_per_million = 0.30
),
}
class CostTracker :
"""リアルタイムコスト追跡"""
def __init__ (self):
self ._costs: list[ dict ] = []
self ._lock = threading.Lock()
def calculate_cost (self, model: str , input_tokens: int ,
output_tokens: int ,
cached_tokens: int = 0 ) -> float :
"""リクエストごとのコストを計算"""
pricing = PRICING .get(model)
if not pricing:
logger.warning( "unknown_model_pricing" , model = model)
return 0.0
input_cost = (input_tokens / 1_000_000 ) * pricing.input_per_million
output_cost = (output_tokens / 1_000_000 ) * pricing.output_per_million
# コンテキストキャッシュ利用時の割引
cache_discount = 0.0
if cached_tokens > 0 and pricing.context_cache_per_million:
full_price = (cached_tokens / 1_000_000 ) * pricing.input_per_million
cache_price = (cached_tokens / 1_000_000 ) * pricing.context_cache_per_million
cache_discount = full_price - cache_price
total = input_cost + output_cost - cache_discount
cost_entry = {
"timestamp" : time.time(),
"model" : model,
"input_tokens" : input_tokens,
"output_tokens" : output_tokens,
"cached_tokens" : cached_tokens,
"cost_usd" : round (total, 6 ),
"cache_savings_usd" : round (cache_discount, 6 )
}
with self ._lock:
self ._costs.append(cost_entry)
return total
def get_daily_summary (self) -> dict :
"""本日のコストサマリーを返す"""
import datetime
today_start = datetime.datetime.now().replace(
hour = 0 , minute = 0 , second = 0
).timestamp()
with self ._lock:
today = [c for c in self ._costs if c[ "timestamp" ] > today_start]
total_cost = sum (c[ "cost_usd" ] for c in today)
by_model = defaultdict( float )
for c in today:
by_model[c[ "model" ]] += c[ "cost_usd" ]
return {
"date" : datetime.date.today().isoformat(),
"total_cost_usd" : round (total_cost, 4 ),
"total_requests" : len (today),
"by_model" : {k: round (v, 4 ) for k, v in by_model.items()},
"total_savings_from_cache" : round (
sum (c[ "cache_savings_usd" ] for c in today), 4
)
}
レート制限との連携も重要です。メトリクスデータを使って、APIのクォータ消費状況を可視化できます。クォータ管理の詳細はGemini API レート制限とクォータ管理を参照してください。
コスト追跡で見落としがちなポイントは、コンテキストキャッシュによる節約効果 です。Gemini 2.5 Pro のコンテキストキャッシュ料金は入力トークンの約25%であるため、繰り返し同じコンテキストを使うワークロードでは大幅なコスト削減が期待できます。上記の CostTracker はキャッシュ割引も計算に含めることで、正確なコスト把握を可能にしています。
レイテンシ監視とアラート設計
レイテンシの異常を早期に検知するには、適切な閾値設定とアラートルールの設計が必要です。Gemini APIのレイテンシはモデルやプロンプト長によって大きく変動するため、静的な閾値だけでなく、移動平均に基づく動的な異常検知も組み合わせます。
from collections import deque
import statistics
class LatencyMonitor :
"""レイテンシ異常検知モニター"""
def __init__ (self, window_size: int = 100 ,
static_threshold_ms: float = 10000 ,
sigma_multiplier: float = 3.0 ):
self ._window = deque( maxlen = window_size)
self .static_threshold_ms = static_threshold_ms
self .sigma_multiplier = sigma_multiplier
def record_and_check (self, latency_ms: float , model: str ) -> dict :
"""レイテンシを記録し、異常かどうかを判定する"""
self ._window.append(latency_ms)
alert = { "is_anomaly" : False , "reason" : "" }
# 静的閾値チェック
if latency_ms > self .static_threshold_ms:
alert = {
"is_anomaly" : True ,
"reason" : f "レイテンシ { latency_ms :.0f } ms が"
f "閾値 { self .static_threshold_ms } ms を超過"
}
# 動的異常検知(σルール: 平均 + N×標準偏差を超えたら異常)
if len ( self ._window) >= 20 :
mean = statistics.mean( self ._window)
stdev = statistics.stdev( self ._window)
dynamic_threshold = mean + ( self .sigma_multiplier * stdev)
if latency_ms > dynamic_threshold and not alert[ "is_anomaly" ]:
alert = {
"is_anomaly" : True ,
"reason" : f "レイテンシ { latency_ms :.0f } ms が"
f "動的閾値 { dynamic_threshold :.0f } ms を超過"
f "(平均= { mean :.0f } , σ= { stdev :.0f } )"
}
if alert[ "is_anomaly" ]:
logger.warning( "latency_anomaly_detected" ,
model = model,
latency_ms = latency_ms,
reason = alert[ "reason" ])
return alert
本番環境では、このアラートをSlackやPagerDutyに送信する仕組みと連携させます。以下は、Slack通知の実装例です。
import json
from urllib.request import Request, urlopen
def send_slack_alert (webhook_url: str , alert_data: dict ):
"""Slackにアラートを送信する"""
message = {
"text" : ":rotating_light: Gemini API レイテンシ異常検知" ,
"blocks" : [
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : f "*モデル*: ` { alert_data.get( 'model' , 'unknown' ) } ` \n "
f "*レイテンシ*: { alert_data.get( 'latency_ms' , 0 ) :.0f } ms \n "
f "*理由*: { alert_data.get( 'reason' , '' ) } "
}
}
]
}
req = Request(webhook_url,
data = json.dumps(message).encode(),
headers = { "Content-Type" : "application/json" })
urlopen(req)
OpenTelemetryによる分散トレーシング
RAGパイプラインやマルチエージェントシステムでは、1つのユーザーリクエストが複数のGemini API呼び出しを含むことがあります。OpenTelemetry(OTel)を使った分散トレーシングにより、リクエスト全体のフローを可視化できます。
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
# OTel初期化
resource = Resource.create({ "service.name" : "gemini-api-service" })
provider = TracerProvider( resource = resource)
exporter = OTLPSpanExporter( endpoint = "http://localhost:4317" )
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer( "gemini.api" )
class TracedGeminiClient ( GeminiObservableClient ):
"""OpenTelemetryトレーシング付きGeminiクライアント"""
def generate_with_trace (self, prompt: str ,
model: str = None , config: dict = None ):
model_name = model or self .default_model
with tracer.start_as_current_span( "gemini.generate" ) as span:
span.set_attribute( "gemini.model" , model_name)
span.set_attribute( "gemini.prompt_length" , len (prompt))
try :
response = self .generate(
prompt = prompt,
model = model_name,
config = config,
trace_id = format (span.get_span_context().trace_id, "032x" )
)
span.set_attribute( "gemini.input_tokens" ,
response.usage_metadata.prompt_token_count)
span.set_attribute( "gemini.output_tokens" ,
response.usage_metadata.candidates_token_count)
span.set_status(trace.StatusCode. OK )
return response
except Exception as e:
span.set_status(trace.StatusCode. ERROR , str (e))
span.record_exception(e)
raise
def rag_pipeline (self, query: str , context_docs: list[ str ]):
"""RAGパイプラインをトレース付きで実行"""
with tracer.start_as_current_span( "rag.pipeline" ) as parent:
parent.set_attribute( "rag.query" , query)
parent.set_attribute( "rag.context_count" , len (context_docs))
# Step 1: クエリ拡張
with tracer.start_as_current_span( "rag.query_expansion" ):
expanded = self .generate_with_trace(
f "検索クエリを拡張してください: { query } " ,
model = "gemini-2.5-flash"
)
# Step 2: コンテキスト付きで回答生成
context = " \n --- \n " .join(context_docs)
with tracer.start_as_current_span( "rag.answer_generation" ):
answer = self .generate_with_trace(
f "コンテキスト: \n{ context }\n\n 質問: { query } " ,
model = "gemini-2.5-pro"
)
return answer
OpenTelemetryのトレースは、Jaeger、Zipkin、Grafana Tempo などのバックエンドで可視化できます。各スパン(span)にGemini固有の属性(モデル名、トークン数、レイテンシ)を付与することで、ボトルネックの特定が容易になります。
Grafanaダッシュボードの構築
収集したメトリクスをGrafanaで可視化する際に推奨するパネル構成は以下の通りです。
ダッシュボード「Gemini API Overview」の構成:
リクエスト数(時系列) : rate(gemini_api_requests_total[5m]) — モデル別のRPSを折れ線グラフで表示
エラー率 : rate(gemini_api_requests_total{status="error"}[5m]) / rate(gemini_api_requests_total[5m]) — 目標は1%未満
レイテンシ分布 : histogram_quantile(0.95, rate(gemini_api_latency_seconds_bucket[5m])) — P50/P95/P99の3本線
トークン消費量 : increase(gemini_api_tokens_total[1h]) — 入力/出力別の積み上げグラフ
推定コスト : increase(gemini_api_estimated_cost_usd[24h]) — 日次の累積コスト
モデル別コスト比率 : sum by (model) (increase(gemini_api_estimated_cost_usd[24h])) — 円グラフでコスト配分を確認
アラートルールとして、以下の3つを最低限設定しておくことを推奨します。
エラー率5%超過 : 5分間のエラー率が5%を超えた場合にSlack通知
P95レイテンシ異常 : P95が通常の3倍を超えた場合にSlack通知
日次コスト閾値超過 : 1日の推定コストが設定した上限を超えた場合にSlack+メール通知
本番運用のベストプラクティス
ここまでの実装を本番環境で活用するための、実践的なベストプラクティスをまとめます。
ログのライフサイクル管理 : すべてのAPIログを永続保存すると、ストレージコストが膨大になります。ホットログ(直近7日)は検索可能なElasticsearchやCloud Loggingに、コールドログ(7日〜90日)はCloud StorageやS3に、それ以降は削除するというライフサイクルポリシーを設計しましょう。
プロンプトのサニタイズ : ログにプロンプト全文を記録する場合、ユーザーの個人情報が含まれる可能性があります。PIIマスキング(メールアドレス、電話番号、クレジットカード番号等をマスク)をログ書き込み前に実行する仕組みが必要です。
import re
def sanitize_prompt (text: str ) -> str :
"""プロンプト内の個人情報をマスクする"""
# メールアドレス
text = re.sub( r ' [\w .+- ] + @ [\w - ] + \. [\w . ] + ' , '[EMAIL]' , text)
# 電話番号(日本)
text = re.sub( r '0 \d {1,4} - ? \d {1,4} - ? \d {4} ' , '[PHONE]' , text)
# クレジットカード番号
text = re.sub( r ' \b\d {4} [\s - ] ? \d {4} [\s - ] ? \d {4} [\s - ] ? \d {4} \b ' , '[CARD]' , text)
return text
フォールバック戦略との連携 : オブザーバビリティデータを使って、モデルの自動フォールバックを実装できます。たとえば、Gemini 2.5 Pro のP95レイテンシが閾値を超えた場合に自動的にGemini 2.5 Flashに切り替える、といった戦略です。
class AdaptiveModelSelector :
"""メトリクスベースの自動モデル選択"""
def __init__ (self, latency_monitor: LatencyMonitor,
primary: str = "gemini-2.5-pro" ,
fallback: str = "gemini-2.5-flash" ,
latency_threshold_ms: float = 8000 ):
self .latency_monitor = latency_monitor
self .primary = primary
self .fallback = fallback
self .threshold = latency_threshold_ms
self ._use_fallback = False
def select_model (self) -> str :
"""現在のレイテンシ状況に応じたモデルを返す"""
if self ._use_fallback:
# フォールバック中は定期的にプライマリに戻す試行
return self .primary if time.time() % 60 < 5 else self .fallback
return self .primary
def on_response (self, model: str , latency_ms: float ):
"""レスポンス受信時にフォールバック判定"""
result = self .latency_monitor.record_and_check(latency_ms, model)
if result[ "is_anomaly" ] and model == self .primary:
self ._use_fallback = True
logger.warning( "model_fallback_activated" ,
primary = self .primary,
fallback = self .fallback)
個人開発者の視点から(実体験メモ)
全体を振り返って
Gemini APIの本番運用では、オブザーバビリティが「あると便利」ではなく「なくてはならない」基盤です。構造化ログで個別リクエストを追跡し、メトリクスでシステムの健全性を監視し、コスト追跡で予算を管理する——この3つの柱を整えることで、自信を持って本番サービスを運用できるようになります。
本記事で紹介したPythonコードはすべてモジュール化されており、既存のGemini APIプロジェクトに段階的に導入できます。まずは GeminiObservableClient を既存クライアントに置き換えるところから始めてみてください。
AIアプリケーションの監視と運用についてさらに深く