はじめに
現代のマイクロサービス環境において、システムの可観測性(Observability)は必要不可欠な要素となっています。 特にKubernetes環境では、分散したコンポーネント間の依存関係や障害の原因特定が複雑になりがちです。
弊社のGREE BizCenシステムでは、Google Kubernetes Engine(GKE)上でマイクロサービスアーキテクチャを採用しており、 OpenTelemetryとGoogle Cloud Observabilityを組み合わせた包括的な可観測性基盤を構築しています。
本記事では、トレース、ログ、メトリクスの三本柱からなる可観測性の実装方法と運用事例について、 実際のコードや設定を交えながら詳しく解説します。
システム構成概要
GREE BizCenシステムの可観測性基盤は以下のように構成されています。
アーキテクチャ概要
コンポーネント | 役割 | 実装技術 |
---|---|---|
アプリケーション | メトリクス・トレース・ログの生成 | Go, OpenTelemetry SDK |
OpenTelemetry Collector | テレメトリデータの収集・処理・転送 | Google公式設定 |
Google Cloud Managed Prometheus | メトリクス保存・可視化 | GCP Managed Service |
Google Cloud Trace | 分散トレーシング | GCP Managed Service |
Google Cloud Logging | ログ集約・検索 | GCP Managed Service |
トレース実装
ここまでで前提となる対象システムや利用する技術について説明しました。 ここから本題のトレース、ログ、メトリクスについて説明していきたいと思います。 まずは、GCP標準のAPM機能であるトレースについて説明します。
GREE BizCenでは、OpenTelemetryを活用した包括的な分散トレーシングシステムを構築しています。 アプリケーション層からデータベース層まで、すべてのコンポーネントでトレース情報を収集し、 マイクロサービス間の依存関係とパフォーマンスを可視化しています。
初期化
アプリケーション起動時の初期化から見ていきます。
// exporterの作成
exporter, err = otlptracegrpc.New(ctxTimeout, otlptracegrpc.WithGRPCConn(conn))
// TracerProviderの作成
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(cfg.TraceSamplingRatio)),
sdktrace.WithResource(res),
sdktrace.WithBatcher(exporter),
)
// グローバルTracerProviderとして設定
otel.SetTracerProvider(tp)
// Propagatorの設定
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
gcppropagator.CloudTraceOneWayPropagator{},
propagation.TraceContext{},
propagation.Baggage{},
))
アプリケーションは、トレースデータを OpenTelemetry Collector へ送信します。 まず、データ送信クライアントとして動作する Exporter を作成します。 次に、アプリケーション内部で計測を行う TraceProvider を作成します。 最後に、複数のマイクロサービスで一貫したトレースを提供するため、 Propagator を設定します。
主要なトレーシングポイント
トレースの内部は、スパンという単位で分割されます。 GREE BizCenでは、Clean Architectureを採用しており、以下の構成となっています。
- Controller: リクエストを受け、必要なUsecaseを呼び出し、レスポンスを構築する。
- Usecase: ビジネスロジック実装。必要に応じてRepositoryを呼び出す。
- Repository: ストレージや外部API呼び出しを行う。
各層でスパンを作ることにより、それぞれの処理時間を把握できるようになります。 また、属性を付与することで、より詳細な分析が可能となります。
Controller: スパンを作り、リクエスト及びレスポンス情報を格納しています。
ctx, span := tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(attribute.String("component", "controller")),
)
defer span.End()
span.SetAttributes(attribute.String("method", "GET"))
span.SetAttributes(attribute.String("path", "/api/v1/users"))
span.SetAttributes(attribute.String("status", 200))
Usecase: ユースケースでは、デバックに有用な情報を格納しています。
ctx, span := tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindInternal),
trace.WithAttributes(attribute.String("component", "usecase")),
)
defer span.End()
span.SetAttributes(attribute.String("id", id))
span.SetAttributes(attribute.String("unit_price", unit_price))
span.SetAttributes(attribute.String("amount", amount))
Repository: Repositoryでは、ストレージや外部リクエスト関連情報を格納しています。
ctx, span := tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(attribute.String("component", "repository")),
)
defer span.End()
span.SetAttributes(attribute.String("db.statement", stmt.SQL))
エラーハンドリング
エラー記録: span.RecordError()を使うことで、エラーとして記録されます。
if err != nil {
span.RecordError(err)
}
トレース実装まとめ
この実装により、GREE BizCenではボトルネックの特定と最適化による応答時間の改善を行いました。 応答速度に問題が発生してとき、トレースを確認することで即座に原因を特定することができます。
ログ実装
次にロギングについて説明します。 GKEでは標準出力に書き出されたログは、暗黙的にCloud Loggingに転送されます。 Cloud Loggingではログを構造化ログと呼ばれるフォーマットで出力することにより、高速な検索を可能とします。 GREE BizCenでは、標準出力に書き出すログを構造化ログに対応させることにより、活用しやすいログ基盤を構築しています。 具体的には、Goのslogパッケージをベースとし、Cloud Loggingの特別なフィールド形式に対応した構造化ログシステムを構築しました。
主要な実装ポイント
属性変換の自動化: Cloud Loggingが期待するフィールド名への自動変換を実装しています。ログレベルをseverity
に、タイムスタンプをtimestamp
に、ソースコード情報をlogging.googleapis.com/sourceLocation
形式に変換することで、Cloud Loggingの機能を最大限活用しています。
OpenTelemetryトレース統合: ログハンドラーがコンテキストからOpenTelemetryのトレース情報を自動抽出し、logging.googleapis.com/trace
とlogging.googleapis.com/spanId
フィールドに設定します。これにより、ログエントリがGoogle Cloud Traceのスパンと自動的に関連付けられます。
HTTPリクエスト専用フィールド: Cloud LoggingのhttpRequest
フィールドに対応した構造化ログを実装しています。リクエストメソッド、URL、ステータスコード、レスポンスサイズ、レイテンシーなどの情報を標準形式で記録し、Cloud LoggingのHTTPリクエスト分析機能を活用しています。
初期化
HandlerOptionsで、属性変換処理を行います。 また、Handlerを使って属性追加処理を行います。 これをslog初期化時に渡すことで、ログ出力時に暗黙的にこれらの変換を行うことができます。
opts := &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Cloud Logging形式への変換
return cloudLoggingReplaceAttr(groups, a)
},
}
// Cloud Logging用ハンドラーの作成
handler := NewCloudLoggingHandler(opts, config.ProjectID)
logger := slog.New(handler)
属性変換関数の実装
ここが実装の核心部分です。 Cloud Loggingの期待するフィールド形式に自動変換する関数を実装しています。
// Cloud Logging形式に属性キーを変換する関数
func cloudLoggingReplaceAttr(groups []string, a slog.Attr) slog.Attr {
switch a.Key {
case "msg", "message":
a.Key = "message"
case "level":
// levelの値をCloud Loggingのseverityに変換
if level, ok := a.Value.Any().(slog.Level); ok {
var severity string
switch {
case level < slog.LevelInfo:
severity = "DEBUG"
case level < slog.LevelWarn:
severity = "INFO"
case level < slog.LevelError:
severity = "WARNING"
default:
severity = "ERROR"
}
a.Key = "severity"
a.Value = slog.StringValue(severity)
}
case "time":
if t, ok := a.Value.Any().(time.Time); ok {
a.Key = "timestamp"
a.Value = slog.StringValue(t.Format(time.RFC3339Nano))
}
case "source":
// ソースコード情報をCloud Logging形式に変換
if source, ok := a.Value.Any().(string); ok {
parts := strings.Split(source, ":")
sourceLocation := make(map[string]string)
if len(parts) >= 1 {
sourceLocation["file"] = parts[0]
}
if len(parts) >= 2 {
sourceLocation["line"] = parts[1]
}
if len(parts) >= 3 {
sourceLocation["function"] = parts[2]
}
a.Key = "logging.googleapis.com/sourceLocation"
a.Value = slog.AnyValue(sourceLocation)
}
}
return a
}
コードは長いですが、行なっている変換処理は次の3点です。
- level: keyの名前をlevelからseverityに変換します。
- time: フォーマットをRFC3339Nanoの形式に変換します。
- source: backtraceで書き出される”:“区切りの情報の変換。
トレース統合
ログエントリとトレーススパンを自動的に関連付ける実装です。
// Handle implements slog.Handler
func (h *cloudLoggingHandler) Handle(ctx context.Context, record slog.Record) error {
// OpenTelemetryからトレース情報を取得
span := trace.SpanFromContext(ctx)
if span.SpanContext().IsValid() {
traceID := span.SpanContext().TraceID().String()
spanID := span.SpanContext().SpanID().String()
// ProjectIDが設定されていて、トレースIDが有効な場合
if h.projectID != "" && traceID != "" {
// Cloud Logging形式のトレース値を構築
gcpTrace := fmt.Sprintf("projects/%s/traces/%s", h.projectID, traceID)
record.AddAttrs(slog.String("logging.googleapis.com/trace", gcpTrace))
}
// SpanIDをログに追加
if spanID != "" {
record.AddAttrs(slog.String("logging.googleapis.com/spanId", spanID))
}
}
return h.handler.Handle(ctx, record)
}
この実装により、Cloud LoggingでトレースIDを使ったログの絞り込みや、Google Cloud Traceからの直接ログアクセスが可能になります。
使用例
slog.InfoContext(ctx, "User operation completed",
"userID", "12345",
"operation", "purchase",
"amount", 10000)
運用における効果
この構造化ログ実装により、GREE BizCenでは以下の効果を実現しています。
- トレースIDによるログとトレースの自動相関により、根本原因特定時間を短縮
- 構造化検索により、特定条件でのログ絞り込みが高速化
メトリクス実装
最後にメトリクス実装について説明します。 ここはまだ十分活用できていない部分です。 そのため、現状と今後の展望について説明します。
実装アプローチ
メトリクス初期化では、GCP環境に最適化されたエクスポーター設定を行います。 GCP環境では認証情報を自動取得し、30秒間隔での定期的なメトリクス送信を設定しています。
func InitMeter(ctx context.Context, cfg Config) (*MeterProvider, error) {
...
exporter, err = otlpmetricgrpc.New(ctxTimeout, otlpmetricgrpc.WithGRPCConn(conn))
// Create meter provider with the exporter
mp := metric.NewMeterProvider(
metric.WithResource(res),
metric.WithReader(
metric.NewPeriodicReader(
exporter,
metric.WithInterval(30*time.Second), // Report metrics every 30 seconds
),
),
)
// Set the global meter provider
otel.SetMeterProvider(mp)
}
メトリクス種別は以下の通り使い分ける必要があります。
- Counter: HTTPリクエスト数、エラー発生数など累積値の記録
- Histogram: レスポンス時間、処理時間の分布測定
- UpDownCounter: アクティブ接続数、メモリ使用量など増減する値の追跡
HTTPミドルウェア統合により、全てのHTTPリクエストを自動的に計測します。 リクエストメソッド、パス、ステータスコードをラベルとして付与し、レスポンス時間とエラー率を同時に記録しています。
レスポンスライターのラッパー実装により、標準のhttp.ResponseWriter
では取得できないステータスコード情報を記録可能にしています。
これにより、エラー率の正確な測定を目指しています。
ビジネス固有のメトリクス
システムメトリクスに加えて、ビジネス価値に直結するメトリクスを送信したいと考えています。
- 契約プラン
- 最終ログイン時刻
これらのメトリクスは、マーケティング効果測定やA/Bテストの効果検証に活用する予定で、技術指標とビジネス指標の両面からシステムの健全性を監視したいと考えています。
まとめ
GREE BizCenでの可観測性システム導入により、システムの内部状態を詳細に把握可能となりました。 しかし、ここがゴールとは考えていません。 具体的には、以下の成果を目指しています。
- 障害検知時間: 5分以内
- 根本原因特定時間: 30分以内
- システム可用性: 99.99%
- 運用コスト: 適切な最適化により予算内で運用
- ビジネス価値: KPIのリアルタイム更新
可観測性は単なる監視ツールではなく、ビジネス価値向上のための重要な投資です。 まだ実現途中ですが、システムの信頼性とビジネスの成長の両立を目指しています。 この記事が皆さんのお役に立てれば幸いです。