メインコンテンツにスキップ

統一されたエラーハンドリング戦略 - Lambda関数で一貫したエラー処理パターン

背景

「エラーが発生したとき、どこで何が起きているのかわからない」— これは多くのサーバーレス開発チームが直面する課題です。

11個のLambda関数が独立して動作する分散システムでは、エラーの発生場所の特定根本原因の分析適切な復旧処理が非常に困難になります。

私たちのプロジェクトでも、初期は「関数ごとに異なるエラー処理」、「不一致なエラーメッセージ**、ログの分散と不統一により、障害時の対応に数時間を要していました。しかし、統一されたエラーハンドリング戦略共通ハンドラーへの統合により、エラー対応時間を85%短縮し、システム全体の信頼性を大幅に向上させることができました。

この記事では、統一エラーハンドリングの設計思想実装パターン運用での効果を、3ヶ月間の実運用データと具体的なインシデント対応事例を交えて詳しく解説します。

## 用語解説

Lambda関数

AWS(Amazon Web Services)が提供するサーバーレスコンピューティングサービスで、コードを実行するためにサーバーをプロビジョニングしたり管理したりする必要がないのが特徴です。イベントに応じて自動的にコードが実行され、使った分だけ料金が発生します。

分散システム

複数の独立したコンピューター(この場合はLambda関数)が連携して一つの大きなタスクを処理するシステムのことです。それぞれのLambda関数が特定の役割を担い、互いに連携しながら動作します。

課題:分散したエラー処理の混乱

改善前のエラー処理問題

// 悪い例:各Lambda関数で異なるエラー処理
// この例では、エラー処理が一貫しておらず、問題発生時の調査や対応が困難になる典型的なパターンを示しています。
// cmd/login/main.go(改善前)
func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // 問題1: エラーログのフォーマットが統一されていない
    // log.Printlnやlog.PrintfはGo言語の標準的なログ出力関数です。
    // しかし、ここではログの形式や出力レベル(INFO, WARN, ERRORなど)が統一されていません。
    log.Println("Login attempt")
    
    var loginRequest LoginRequest
    // json.Unmarshalは、受け取ったJSON形式のデータをGo言語の構造体(ここではloginRequest)に変換する関数です。
    if err := json.Unmarshal([]byte(request.Body), &loginRequest); err != nil {
        log.Printf("JSON parse error: %v", err)  // 問題:ログレベル不統一
        return events.APIGatewayProxyResponse{
            StatusCode: 400, // HTTPステータスコード400は「Bad Request(不正なリクエスト)」を意味し、クライアント側の問題を示します。
            Body:       `{"error": "Bad request"}`,  // 問題:エラー形式不統一
        }, nil
    }

    if err := validateLogin(&loginRequest); err != nil {
        log.Printf("Validation failed: %s", err.Error())
        return events.APIGatewayProxyResponse{
            StatusCode: 400,
            Body:       fmt.Sprintf(`{"message": "Validation error: %s"}`, err.Error()),  // 問題:機密情報漏洩リスク
            // ここでerr.Error()をそのまま返すことで、内部的なエラーメッセージ(スタックトレースなど)がユーザーに公開され、
            // セキュリティ上のリスク(情報漏洩)につながる可能性があります。
        }, nil
    }

    // 問題2: Cognitoエラーの処理が不適切
    // CognitoはAWSが提供するユーザー認証・認可サービスです。
    // ここではCognitoからのエラーが適切に処理されていません。
    result, err := cognitoClient.InitiateAuth(ctx, authInput)
    if err != nil {
        log.Printf("Cognito error: %v", err)  // 問題:詳細すぎるログ
        return events.APIGatewayProxyResponse{
            StatusCode: 500,  // 問題:認証失敗なのに500エラー
            // HTTPステータスコード500は「Internal Server Error(サーバー内部エラー)」を意味し、サーバー側の問題を示します。
            // 認証失敗は通常、クライアント側の問題なので、4xx系のステータスコード(例: 401 Unauthorized)を返すのが適切です。
            Body:       `{"error": "Server error"}`,  // 問題:ユーザーに不親切
        }, nil
    }

    return events.APIGatewayProxyResponse{StatusCode: 200, Body: `{"status": "ok"}`}, nil
}

// cmd/register/main.go(改善前)
func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // 問題3: 同じ種類のエラーでも処理が異なる
    var registerRequest RegisterRequest
    if err := json.Unmarshal([]byte(request.Body), &registerRequest); err != nil {
        fmt.Printf("Parse error in register: %v\n", err)  // 問題:ログ出力方法が異なる
        return events.APIGatewayProxyResponse{
            StatusCode: 422,  // 問題:login関数と異なるHTTPステータス
            // HTTPステータスコード422は「Unprocessable Entity(処理できないエンティティ)」を意味し、
            // リクエストの構文は正しいが、意味的に誤りがある場合に使われます。
            Body:       `{"msg": "Invalid JSON"}`,  // 問題:レスポンス形式が異なる
        }, nil
    }

    // 問題4: パニック回復なし
    // Go言語における「パニック(panic)」とは、プログラムの実行を停止させる回復不能なエラーのことです。
    // ここでは、予期しないエラー(例: nilポインタ参照)が発生した場合に、Lambda関数全体がクラッシュし、
    // ユーザーへの適切なエラーレスポンスが返せなくなるリスクがあります。
    // 「パニック回復」とは、このパニックを捕捉し、プログラムの異常終了を防ぐ仕組みです。

    return events.APIGatewayProxyResponse{StatusCode: 201, Body: `{"result": "created"}`}, nil
}

// このような不統一な実装の問題点:
// - エラーの種類と原因の特定困難
// - ログ分析の複雑化
// - ユーザー向けエラーメッセージの不一致
// - 障害対応時の混乱
// - セキュリティリスク(情報漏洩)
)

実際に発生した運用問題

  • エラー対応で発生した問題事例:

  • 問題事例1: データベース接続エラー

    • 症状: DynamoDBアクセスでタイムアウト
    • 影響範囲: 複数Lambda関数で同時発生
    • 調査時間: 3時間
    • 原因特定困難理由:
      • ログフォーマットが関数ごとに異なる
      • エラーメッセージの統一性なし
      • 関連するエラー情報の分散
  • 問題事例2: 認証エラーの誤分類

    • 症状: ユーザーから「サーバーエラーで使えない」報告
    • 実際の原因: 期限切れJWTトークン
    • 調査時間: 1.5時間
    • 問題:
      • 認証エラーが500で返される
      • エラーメッセージが不適切
      • 根本原因の特定に時間要
  • 問題事例3: パニッククラッシュ

    • 症状: Lambda関数が予期せず終了
    • 原因: nil ポインター参照
    • 影響: ユーザーリクエストの完全失敗
    • 問題:
      • パニック回復機構なし
      • クラッシュ前の状態情報なし
      • 再現困難なエラー
  • 総障害対応時間: 月平均15時間

  • 主要問題: エラー処理の統一性欠如

  • ユーザー満足度影響: 大(不適切なエラーメッセージ)

統一エラーハンドリング設計

エラー分類体系

// pkg/errors/error_types.go
// このファイルでは、アプリケーション全体で利用する統一されたエラーの型と、そのエラーを生成するための関数を定義しています。
// これにより、エラーの種類を明確にし、一貫したエラー処理を可能にします。

package errors

import (
    "fmt"      // 文字列のフォーマット(整形)を行うためのパッケージです。
    "net/http" // HTTPステータスコードなどの定数を定義しているパッケージです。
)

// エラーカテゴリの定義
// エラーを種類ごとに分類するための型です。
type ErrorCategory string

const (
    ErrorCategoryValidation    ErrorCategory = "VALIDATION"       // 入力値の検証エラー
    ErrorCategoryAuth          ErrorCategory = "AUTHENTICATION"   // 認証エラー
    ErrorCategoryAuthorization ErrorCategory = "AUTHORIZATION"  // 認可エラー(アクセス権限がない場合)
    ErrorCategoryNotFound      ErrorCategory = "NOT_FOUND"        // リソースが見つからないエラー
    ErrorCategoryConflict      ErrorCategory = "CONFLICT"         // リソースの競合エラー
    ErrorCategoryExternal      ErrorCategory = "EXTERNAL_SERVICE" // 外部サービス連携時のエラー
    ErrorCategoryInternal      ErrorCategory = "INTERNAL"         // サーバー内部の予期せぬエラー
    ErrorCategoryRateLimit     ErrorCategory = "RATE_LIMIT"       // リクエストレート制限によるエラー
)

// アプリケーションエラー構造
// アプリケーション内で発生するエラーを表現するためのカスタム構造体です。
// `json:"..."` はJSONタグと呼ばれ、この構造体がJSON形式に変換される際に、
// フィールド名がどのようにマッピングされるかを指定します。
type AppError struct {
    Category     ErrorCategory `json:"category"`      // エラーのカテゴリ(上記で定義)
    Code         string        `json:"code"`          // エラーを一意に識別するコード(例: "INVALID_JSON")
    Message      string        `json:"message"`       // 開発者向けの詳細なエラーメッセージ
    UserMessage  string        `json:"user_message"`  // ユーザーに表示する、分かりやすいエラーメッセージ
    HTTPStatus   int           `json:"http_status"`   // HTTPレスポンスのステータスコード(例: 400, 500)
    Internal     error         `json:"-"`             // 内部で発生した元のエラー(ログ出力用で、JSONには含めない)
    Context      map[string]interface{} `json:"context,omitempty"` // エラー発生時の追加情報(例: どのフィールドでエラーが起きたか)
}

// Error() メソッドは、Go言語のerrorインターフェースを満たすために必要です。
// このメソッドを実装することで、AppError型が標準のエラーとして扱えるようになります。
func (e *AppError) Error() string {
    return fmt.Sprintf("[%s:%s] %s", e.Category, e.Code, e.Message)
}

// 標準エラーコンストラクタ
// 特定のエラーカテゴリのAppErrorインスタンスを生成するためのヘルパー関数(コンストラクタ)です。
func NewValidationError(code, message, userMessage string) *AppError {
    return &AppError{
        Category:    ErrorCategoryValidation,
        Code:        code,
        Message:     message,
        UserMessage: userMessage,
        HTTPStatus:  http.StatusBadRequest,
        Context:     make(map[string]interface{}),
    }
}

func NewAuthenticationError(code, message string) *AppError {
    return &AppError{
        Category:    ErrorCategoryAuth,
        Code:        code,
        Message:     message,
        UserMessage: "認証に失敗しました。ログイン情報を確認してください。",
        HTTPStatus:  http.StatusUnauthorized,
        Context:     make(map[string]interface{}),
    }
}

func NewInternalError(code, message string, internal error) *AppError {
    return &AppError{
        Category:    ErrorCategoryInternal,
        Code:        code,
        Message:     message,
        UserMessage: "一時的なエラーが発生しました。しばらく待ってから再度お試しください。",
        HTTPStatus:  http.StatusInternalServerError,
        Internal:    internal,
        Context:     make(map[string]interface{}),
    }
}

// エラーにコンテキスト情報を追加
// エラー発生時の状況をより詳細に記録するために、追加の情報をAppErrorに追加するメソッドです。
func (e *AppError) WithContext(key string, value interface{}) *AppError {
    e.Context[key] = value
    return e
}

// 内部エラーを追加
// 発生した元のGo言語のエラーをAppErrorに紐付けるメソッドです。
// これにより、ログには元のエラーの詳細を残しつつ、ユーザーには抽象化されたエラーを返せます。
func (e *AppError) WithInternal(internal error) *AppError {
    e.Internal = internal
    return e
}

// 事前定義された共通エラー
// アプリケーション内で頻繁に利用される特定のエラーを、あらかじめ定義しておくことで、
// コードの重複を減らし、一貫性を保ちます。
var (
    ErrInvalidJSON = NewValidationError("INVALID_JSON",
        "Request body contains invalid JSON",
        "リクエストの形式が正しくありません")

    ErrMissingRequiredField = NewValidationError("MISSING_FIELD",
        "Required field is missing",
        "必須項目が入力されていません")

    ErrInvalidEmail = NewValidationError("INVALID_EMAIL",
        "Invalid email format",
        "メールアドレスの形式が正しくありません")

    ErrUserNotFound = NewAuthenticationError("USER_NOT_FOUND",
        "User does not exist")

    ErrInvalidCredentials = NewAuthenticationError("INVALID_CREDENTIALS",
        "Email or password is incorrect")

    ErrExpiredToken = NewAuthenticationError("EXPIRED_TOKEN",
        "JWT token has expired")

    ErrDatabaseUnavailable = NewInternalError("DB_UNAVAILABLE",
        "Database service is temporarily unavailable", nil)

    ErrExternalServiceTimeout = NewInternalError("EXTERNAL_TIMEOUT",
        "External service timeout", nil)
)

エラーハンドリングミドルウェア

// pkg/middleware/error_handler.go
// このファイルは、アプリケーション全体でエラーを統一的に処理するための「ミドルウェア」を定義しています。
// ミドルウェアとは、HTTPリクエストが実際のビジネスロジックに到達する前や、レスポンスが返される前に、
// 共通の処理(ここではエラーハンドリング)を実行するためのプログラムの層のことです。

package middleware

import (
    "context"
    "encoding/json"
    "fmt"
    "runtime/debug"
    "strings"
    "time" // timeパッケージのインポートを追加

    "github.com/aws/aws-lambda-go/events"

    "poc-cognite/pkg/errors"
    "poc-cognite/pkg/logger"
    "poc-cognite/pkg/utils"
    "os" // osパッケージのインポートを追加
)

type ErrorHandler struct {
    logger        *logger.Logger
    metricsClient *MetricsClient
}

func NewErrorHandler(log *logger.Logger, metrics *MetricsClient) *ErrorHandler {
    return &ErrorHandler{
        logger:        log,
        metricsClient: metrics,
    }
}

// パニック回復ミドルウェア
// Go言語で発生する「パニック(panic)」を捕捉し、Lambda関数がクラッシュするのを防ぎます。
// これにより、予期せぬエラーが発生しても、適切にエラーレスポンスを返すことができます。
func (eh *ErrorHandler) RecoverFromPanic() func() {
    return func() {
        if r := recover(); r != nil {
            stack := debug.Stack()

            eh.logger.Error("Lambda function panicked", map[string]interface{}{
                "panic":       r,
                "stack_trace": string(stack),
            })

            // パニックメトリクス
            // パニック発生回数を計測し、監視システムで異常を検知できるようにします。
            if eh.metricsClient != nil {
                eh.metricsClient.RecordBusinessMetric("Error.Panic", 1, map[string]string{
                    "Type": "UnexpectedPanic",
                })
            }
        }
    }
}

// エラーレスポンス生成
// 発生したエラーをAppError型に変換し、統一された形式のHTTPレスポンスを生成します。
func (eh *ErrorHandler) HandleError(err error) events.APIGatewayProxyResponse {
    // AppErrorにキャスト
    // 渡されたエラーがAppError型であればそのまま利用し、そうでなければ予期しない内部エラーとして扱います。
    appErr, ok := err.(*errors.AppError)
    if !ok {
        // 予期しないエラーの場合
        appErr = errors.NewInternalError("UNEXPECTED_ERROR",
            "An unexpected error occurred", err)
    }

    // 構造化エラーログ
    // エラー情報を構造化された形式(JSONなど)でログに出力します。
    // これにより、ログ分析ツールでの検索や集計が容易になります。
    eh.logError(appErr)

    // エラーメトリクス
    // エラーの種類や発生回数などをメトリクスとして記録し、ダッシュボードでの可視化やアラート設定に利用します。
    eh.recordErrorMetrics(appErr)

    // レスポンス形式の統一
    // ユーザーに返すエラーレスポンスの形式を統一します。
    // AWS LambdaのAPI Gatewayプロキシ統合では、events.APIGatewayProxyResponse型でレスポンスを返します。
    response := eh.createErrorResponse(appErr)

    return response
}

func (eh *ErrorHandler) logError(appErr *errors.AppError) {
    // エラーカテゴリに基づいてログレベル(ERROR, WARN, INFO)を決定します。
    logLevel := eh.determineLogLevel(appErr.Category)

    logData := map[string]interface{}{
        "error_category":  string(appErr.Category),
        "error_code":      appErr.Code,
        "error_message":   appErr.Message,
        "http_status":     appErr.HTTPStatus,
    }

    // コンテキスト情報を追加
    // エラーに付随する追加情報(例: ユーザーID、リクエストIDなど)をログに含めます。
    for key, value := range appErr.Context {
        logData[fmt.Sprintf("ctx_%s", key)] = value
    }

    // 内部エラーがある場合は追加
    // 元のGo言語のエラー情報をログに含めることで、デバッグ時に役立ちます。
    if appErr.Internal != nil {
        logData["internal_error"] = appErr.Internal.Error()
    }

    switch logLevel {
    case "ERROR":
        eh.logger.Error("Application error occurred", logData)
    case "WARN":
        eh.logger.Warn("Application warning", logData)
    case "INFO":
        eh.logger.Info("Application event", logData)
    }
}

func (eh *ErrorHandler) determineLogLevel(category errors.ErrorCategory) string {
    switch category {
    case errors.ErrorCategoryInternal:
        return "ERROR" // 内部エラーは重大なのでERRORレベル
    case errors.ErrorCategoryExternal:
        return "WARN" // 外部サービスエラーはWARNレベル
    case errors.ErrorCategoryAuth, errors.ErrorCategoryAuthorization:
        return "WARN"  // セキュリティ関連は警告レベル
    case errors.ErrorCategoryValidation, errors.ErrorCategoryNotFound:
        return "INFO" // バリデーションエラーやリソースが見つからない場合はINFOレベル
    default:
        return "WARN"
    }
}

func (eh *ErrorHandler) recordErrorMetrics(appErr *errors.AppError) {
    if eh.metricsClient == nil {
        return
    }

    // エラーカテゴリ別メトリクス
    // エラーのカテゴリごとに発生回数を記録します。
    eh.metricsClient.RecordBusinessMetric("Error.Occurred", 1, map[string]string{
        "Category":   string(appErr.Category),
        "Code":       appErr.Code,
        "HTTPStatus": fmt.Sprintf("%d", appErr.HTTPStatus),
    })

    // 特定エラーの詳細トラッキング
    // 認証失敗や内部エラーなど、特定の重要なエラーについては、さらに詳細なメトリクスを記録します。
    switch appErr.Category {
    case errors.ErrorCategoryAuth:
        eh.metricsClient.RecordBusinessMetric("Security.AuthFailure", 1, map[string]string{
            "Reason": appErr.Code,
        })
    case errors.ErrorCategoryInternal:
        eh.metricsClient.RecordBusinessMetric("System.InternalError", 1, map[string]string{
            "Service": eh.getAffectedService(appErr),
        })
    case errors.ErrorCategoryExternal:
        eh.metricsClient.RecordBusinessMetric("External.ServiceError", 1, map[string]string{
            "Service": eh.getExternalService(appErr),
        })
    }
}

func (eh *ErrorHandler) createErrorResponse(appErr *errors.AppError) events.APIGatewayProxyResponse {
    // 統一されたエラーレスポンス形式
    // ユーザーに返すエラーレスポンスのJSON形式を統一します。
    errorResponse := map[string]interface{}{
        "error": map[string]interface{}{
            "code":    appErr.Code,
            "message": appErr.UserMessage,
        },
        "timestamp": time.Now().UTC().Format(time.RFC3339),
        "status":    appErr.HTTPStatus,
    }

    // デバッグ環境では詳細情報を含める
    // 環境変数"STAGE"が"dev"(開発環境)の場合のみ、開発者向けのデバッグ情報をレスポンスに含めます。
    // これにより、本番環境で機密情報が漏洩するのを防ぎます。
    if os.Getenv("STAGE") == "dev" {
        errorResponse["debug"] = map[string]interface{}{
            "category": string(appErr.Category),
            "internal_message": appErr.Message,
            "context": appErr.Context,
        }
    }

    responseBody, _ := json.Marshal(errorResponse)

    return events.APIGatewayProxyResponse{
        StatusCode: appErr.HTTPStatus,
        Headers: map[string]string{
            "Content-Type": "application/json",
        },
        Body: string(responseBody),
    }
}

AWS固有エラーハンドラー

// pkg/middleware/aws_error_handler.go
// AWS サービス固有のエラー変換

package middleware

import (
    "strings"

    "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
    dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
    
    "poc-cognite/pkg/errors"
)

type AWSErrorHandler struct{}

func NewAWSErrorHandler() *AWSErrorHandler {
    return &AWSErrorHandler{}
}

// Cognitoエラーの変換
func (h *AWSErrorHandler) ConvertCognitoError(err error) *errors.AppError {
    if err == nil {
        return nil
    }

    errStr := err.Error()

    // Cognitoエラーのパターンマッチング
    switch {
    case strings.Contains(errStr, "UserNotFoundException"):
        return errors.ErrUserNotFound.WithInternal(err)
    
    case strings.Contains(errStr, "NotAuthorizedException"):
        if strings.Contains(errStr, "Password attempts exceeded") {
            return errors.NewAuthenticationError("PASSWORD_ATTEMPTS_EXCEEDED", 
                "Too many failed password attempts").WithInternal(err)
        }
        return errors.ErrInvalidCredentials.WithInternal(err)
    
    case strings.Contains(errStr, "UserNotConfirmedException"):
        return errors.NewAuthenticationError("USER_NOT_CONFIRMED", 
            "User account is not confirmed").WithInternal(err)
    
    case strings.Contains(errStr, "TooManyRequestsException"):
        return errors.NewInternalError("RATE_LIMITED", 
            "Too many requests to authentication service", err)
    
    case strings.Contains(errStr, "InvalidPasswordException"):
        return errors.NewValidationError("INVALID_PASSWORD", 
            "Password does not meet requirements", 
            "パスワードは8文字以上で、大文字・小文字・数字を含む必要があります").WithInternal(err)
    
    case strings.Contains(errStr, "UsernameExistsException"):
        return errors.NewValidationError("USER_EXISTS", 
            "User with this email already exists", 
            "このメールアドレスは既に登録されています").WithInternal(err)

    default:
        return errors.NewInternalError("COGNITO_ERROR", 
            "Authentication service error", err)
    }
}

// DynamoDBエラーの変換
func (h *AWSErrorHandler) ConvertDynamoDBError(err error) *errors.AppError {
    if err == nil {
        return nil
    }

    errStr := err.Error()

    switch {
    case strings.Contains(errStr, "ConditionalCheckFailedException"):
        return errors.NewValidationError("CONDITION_FAILED", 
            "Data condition check failed", 
            "データの整合性チェックに失敗しました").WithInternal(err)
    
    case strings.Contains(errStr, "ProvisionedThroughputExceededException"):
        return errors.NewInternalError("DATABASE_THROTTLED", 
            "Database capacity exceeded", err)
    
    case strings.Contains(errStr, "ResourceNotFoundException"):
        return errors.NewInternalError("DATABASE_RESOURCE_NOT_FOUND", 
            "Database resource not found", err)
    
    case strings.Contains(errStr, "ValidationException"):
        return errors.NewValidationError("DATABASE_VALIDATION", 
            "Invalid database operation", 
            "データベース操作でエラーが発生しました").WithInternal(err)
    
    case strings.Contains(errStr, "ServiceUnavailableException"):
        return errors.ErrDatabaseUnavailable.WithInternal(err)

    default:
        return errors.NewInternalError("DATABASE_ERROR", 
            "Database service error", err)
    }
}

// 汎用AWSエラー変換
func (h *AWSErrorHandler) ConvertAWSError(err error, service string) *errors.AppError {
    if err == nil {
        return nil
    }

    errStr := err.Error()

    switch {
    case strings.Contains(errStr, "timeout"):
        return errors.NewInternalError(fmt.Sprintf("%s_TIMEOUT", strings.ToUpper(service)), 
            fmt.Sprintf("%s service timeout", service), err)
    
    case strings.Contains(errStr, "AccessDenied"):
        return errors.NewInternalError(fmt.Sprintf("%s_ACCESS_DENIED", strings.ToUpper(service)), 
            fmt.Sprintf("Access denied to %s service", service), err)
    
    case strings.Contains(errStr, "ServiceUnavailable"):
        return errors.NewInternalError(fmt.Sprintf("%s_UNAVAILABLE", strings.ToUpper(service)), 
            fmt.Sprintf("%s service is temporarily unavailable", service), err)

    default:
        return errors.NewInternalError(fmt.Sprintf("%s_ERROR", strings.ToUpper(service)), 
            fmt.Sprintf("%s service error", service), err)
    }
}

共通ハンドラーへの統合

エラーハンドリング統合ハンドラー

// pkg/lambda/error_aware_handler.go
// このファイルは、各Lambda関数のビジネスロジックをラップし、
// 統一されたエラーハンドリング、ロギング、メトリクス収集などを適用する共通ハンドラーです。
// これにより、各Lambda関数は純粋なビジネスロジックに集中でき、共通処理はここで一元管理されます。

func (h *CommonHandlerWrapper) WrapHandlerWithErrorHandling(
    // businessHandlerは、各Lambda関数が実装する実際のビジネスロジックです。
    // この共通ハンドラーによってラップされ、エラー処理などが自動的に適用されます。
    businessHandler func(context.Context, events.APIGatewayProxyRequest, *logger.Logger, *middleware.MetricsClient) (events.APIGatewayProxyResponse, error),
) func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

    return func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
        // エラーハンドラーの初期化
        errorHandler := middleware.NewErrorHandler(h.logger, h.metricsClient)

        // パニック回復の設定
        // deferキーワードにより、この関数が終了する直前にerrorHandler.RecoverFromPanic()が実行されます。
        // これにより、ビジネスロジック内でパニックが発生しても、Lambda関数がクラッシュせず、
        // 適切にエラーハンドリングミドルウェアがエラーを処理できるようになります。
        defer errorHandler.RecoverFromPanic()

        startTime := time.Now()

        // リクエストログ
        h.logger.LogRequest(request)

        // CORS プリフライト処理
        // CORS (Cross-Origin Resource Sharing) は、Webブラウザが異なるオリジン(ドメイン、プロトコル、ポート)の
        // リソースにアクセスする際のセキュリティメカニズムです。
        // プリフライトリクエスト(HTTP OPTIONSメソッド)は、実際のHTTPリクエストを送信する前に、
        // サーバーがそのリクエストを許可するかどうかを確認するために送信されます。
        if request.HTTPMethod == "OPTIONS" {
            return utils.CreateCORSResponse(), nil
        }

        // ビジネスハンドラー実行(エラー捕捉付き)
        response, err := h.executeBusinessHandlerSafely(ctx, request, businessHandler, errorHandler)

        // 実行時間記録
        // リクエストの処理にかかった時間を計測し、パフォーマンスメトリクスとして記録します。
        duration := time.Since(startTime)
        if h.metricsClient != nil {
            h.metricsClient.RecordDuration("Request.Duration", duration) // リクエストの処理時間を記録
        }

        // CORS ヘッダー追加
        if response.Headers == nil {
            response.Headers = make(map[string]string)
        }
        response.Headers = h.corsMiddleware.AddCORSHeaders(response.Headers)

        // レスポンスログ(エラー情報含む)
        h.logger.LogResponse(response)

        return response, nil
    }
}

func (h *CommonHandlerWrapper) executeBusinessHandlerSafely(
    ctx context.Context,
    request events.APIGatewayProxyRequest,
    businessHandler func(context.Context, events.APIGatewayProxyRequest, *logger.Logger, *middleware.MetricsClient) (events.APIGatewayProxyResponse, error),
    errorHandler *middleware.ErrorHandler,
) (events.APIGatewayProxyResponse, error) {

    // ビジネスロジック実行
    response, err := businessHandler(ctx, request, h.logger, h.metricsClient)

    if err != nil {
        // エラーを統一的に処理
        // ビジネスロジックからエラーが返された場合、ErrorHandlerのHandleErrorメソッドを呼び出し、
        // 統一されたエラーレスポンスを生成します。
        return errorHandler.HandleError(err), nil
    }

    // 成功メトリクス
    // リクエストが成功した場合に、成功回数をメトリクスとして記録します。
    if h.metricsClient != nil {
        h.metricsClient.RecordBusinessMetric("Request.Success", 1, map[string]string{
            "Path":   request.Path,
            "Method": request.HTTPMethod,
        })
    }

    return response, nil
}

Lambda関数での統一エラー使用例

// cmd/login/main.go
// このファイルは、統一エラーハンドリングを実際にLambda関数でどのように使用するかを示す例です。
// 各ビジネスロジック内でエラーが発生した場合、定義済みのAppError型を返すことで、
// 共通ハンドラーがそのエラーを適切に処理し、統一されたレスポンスを生成します。

func businessHandler(ctx context.Context, request events.APIGatewayProxyRequest,
    log *logger.Logger, metricsClient *middleware.MetricsClient) (events.APIGatewayProxyResponse, error) {

    // リクエスト解析(統一エラー使用)
    var loginRequest models.LoginRequest
    // JSONのパースに失敗した場合、ErrInvalidJSONという事前定義されたエラーを返します。
    // WithContextで追加情報(どのボディでエラーが起きたか)を付与し、WithInternalで元のエラーを保持します。
    if err := json.Unmarshal([]byte(request.Body), &loginRequest); err != nil {
        return nil, errors.ErrInvalidJSON.WithContext("body", request.Body).WithInternal(err)
    }

    // バリデーション(統一エラー使用)
    // 入力値のバリデーションに失敗した場合、ErrMissingRequiredFieldやErrInvalidEmailといった
    // 事前定義されたバリデーションエラーを返します。
    if loginRequest.Email == "" {
        return nil, errors.ErrMissingRequiredField.WithContext("field", "email")
    }

    if !isValidEmail(loginRequest.Email) {
        return nil, errors.ErrInvalidEmail.WithContext("email", loginRequest.Email)
    }

    if loginRequest.Password == "" {
        return nil, errors.ErrMissingRequiredField.WithContext("field", "password")
    }

    // Cognito認証(AWS固有エラーハンドリング)
    // di.GetCognitoClient()は、Cognitoサービスと連携するためのクライアントを取得します。
    // (DI: Dependency Injection - 依存性注入という設計パターンで、必要なオブジェクトを外部から渡すことで、
    // コードのテスト容易性や再利用性を高めます。)
    cognitoClient := di.GetCognitoClient()
    authResult, err := cognitoClient.InitiateAuth(ctx, &loginRequest)
    if err != nil {
        // Cognitoから返されたエラーをAWSErrorHandlerでAppError型に変換します。
        awsErrorHandler := middleware.NewAWSErrorHandler()
        return nil, awsErrorHandler.ConvertCognitoError(err).WithContext("email", loginRequest.Email)
    }

    // DynamoDBアクセス(AWS固有エラーハンドリング)
    // di.GetUserService()は、ユーザーデータにアクセスするためのサービスを取得します。
    userService := di.GetUserService()
    userData, err := userService.GetUserByUserSub(ctx, authResult.UserSub);
    if err != nil {
        // DynamoDBから返されたエラーも同様にAWSErrorHandlerでAppError型に変換します。
        awsErrorHandler := middleware.NewAWSErrorHandler()
        return nil, awsErrorHandler.ConvertDynamoDBError(err).WithContext("user_sub", authResult.UserSub)
    }

    // 成功レスポンス
    loginResponse := models.LoginResponse{
        AccessToken: authResult.AccessToken,
        ExpiresIn:   authResult.ExpiresIn,
        User:        *userData,
    }

    return utils.CreateSuccessResponse(loginResponse), nil
}

func main() {
    config := lambdaCommon.DefaultConfig("login")
    commonHandler := lambdaCommon.NewCommonHandler(config)

    // エラーハンドリング統合ハンドラーを使用
    // ここで、ビジネスロジック(businessHandler)を共通ハンドラーでラップします。
    // これにより、エラー処理、ロギング、メトリクス収集などの共通機能が自動的に適用されます。
    wrappedHandler := commonHandler.WrapHandlerWithErrorHandling(businessHandler)

    // lambda.Startは、Go言語で書かれたLambda関数のエントリポイントです。
    // ここでラップされたハンドラーをAWS Lambdaランタイムに登録し、イベントの処理を開始します。
    lambda.Start(wrappedHandler)
}

エラー監視・分析システム

CloudWatch Insights クエリ

-- CloudWatch Insights クエリ
-- CloudWatch Insightsは、AWSのログ分析サービスで、ログデータに対してSQLのようなクエリを実行し、
-- 問題の診断、運用上のパフォーマンスの監視、ログの傾向分析を行うことができます。

-- エラー種別別集計クエリ
-- 発生したエラーをカテゴリとコード別に集計し、どの種類のエラーが多いかを把握します。
fields @timestamp, error_category, error_code, error_message, ctx_user_id
| filter @message like /Application error occurred/ -- "Application error occurred"というメッセージを含むログをフィルタリングします。
| stats count() by error_category, error_code      -- error_categoryとerror_codeごとにログの数をカウントします。
| sort count desc                                  -- カウントが多い順に結果をソートします。

-- 認証エラーの詳細分析
-- 認証エラー(AUTHENTICATIONカテゴリ)に絞り込み、エラーコードと送信元IPアドレス別に集計します。
-- これにより、特定の認証エラーがどこから多く発生しているかなどを分析できます。
fields @timestamp, error_code, ctx_email, ctx_source_ip
| filter error_category = "AUTHENTICATION"
| stats count() by error_code, ctx_source_ip
| sort count desc

-- 内部エラーのトレンド分析
-- 内部エラー(INTERNALカテゴリ)の発生トレンドを時系列で分析します。
fields @timestamp, error_code, internal_error
| filter error_category = "INTERNAL"
| bin(5m)                                          -- ログを5分間隔でグループ化(ビン化)します。
| stats count() by bin(@timestamp)                 -- 各5分間隔でのエラー数をカウントします。
| sort @timestamp                                  -- タイムスタンプ順に結果をソートします。

-- パフォーマンスに影響するエラー
-- 外部サービス連携時のエラー(EXTERNALカテゴリ)に絞り込み、そのエラーが発生したリクエストの平均処理時間と最大処理時間を分析します。
-- これにより、どの外部サービスのエラーがシステム全体のパフォーマンスに影響を与えているかを特定できます。
fields @timestamp, duration, error_category, error_code
| filter @type = "END" and error_category = "EXTERNAL"
| stats avg(duration), max(duration), count() by error_code
| sort avg desc

エラーアラート設定

# CloudWatch アラーム設定
# CloudWatchアラームは、特定のメトリクスが設定したしきい値を超えた場合に、
# 自動的に通知(SNSトピックへの発行など)やアクション(Auto Scalingなど)を実行する機能です。
# これにより、システムの問題を早期に検知し、対応することができます。

Resources:
  # 高頻度エラーアラーム
  # 5分間に発生したエラーの合計が10回を超えた場合にアラートを発します。
  HighErrorRateAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${AWS::StackName}-high-error-rate" # アラームの名前。!SubはCloudFormationの組み込み関数で、変数を埋め込みます。
      AlarmDescription: "Error rate exceeds threshold"     # アラームの説明
      MetricName: Error.Occurred                          # 監視するメトリクスの名前
      Namespace: PocCognite/Lambda                        # メトリクスが属する名前空間
      Statistic: Sum                                      # 統計方法(合計)
      Period: 300  # 5分間 (300秒)                       # メトリクスを評価する期間
      EvaluationPeriods: 2                                # しきい値を超えた状態が続く期間(2期間連続でしきい値を超えたらアラート)
      Threshold: 10  # 5分間で10エラー以上                # しきい値
      ComparisonOperator: GreaterThanThreshold            # 比較演算子(しきい値より大きい場合)
      AlarmActions:
        - !Ref CriticalAlertTopic # アラームが発動した際に通知を送るSNSトピックへの参照。
                                  # !RefもCloudFormationの組み込み関数で、他のリソースを参照します。

  # 内部エラー急増アラーム
  # 5分間に発生した内部エラーの合計が3回を超えた場合にアラートを発します。
  InternalErrorSpikeAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${AWS::StackName}-internal-error-spike"
      MetricName: System.InternalError
      Namespace: PocCognite/Lambda
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 3  # 5分間で3回以上の内部エラー
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref CriticalAlertTopic

  # 認証エラー急増アラーム(セキュリティ)
  # 5分間に発生した認証失敗の合計が20回を超えた場合にアラートを発します。
  # これは、ブルートフォースアタックなどのセキュリティ上の脅威を検知するために重要です。
  AuthFailureSpikeAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${AWS::StackName}-auth-failure-spike"
      MetricName: Security.AuthFailure
      Namespace: PocCognite/Lambda
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 20  # 5分間で20回以上の認証失敗
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref SecurityAlertTopic # セキュリティ関連のアラートを通知するSNSトピックへの参照。

エラー分析ダッシュボード

// tools/error-analysis-dashboard.go
// エラー分析レポート自動生成

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
)

type ErrorAnalyzer struct {
    logsClient *cloudwatchlogs.Client
    logGroups  []string
}

type ErrorSummary struct {
    Period        time.Duration                `json:"period"`
    TotalErrors   int                          `json:"total_errors"`
    ErrorsByType  map[string]int               `json:"errors_by_type"`
    TopErrors     []ErrorDetail                `json:"top_errors"`
    TrendAnalysis map[string][]TimeSeriesPoint `json:"trend_analysis"`
}

type ErrorDetail struct {
    Category    string    `json:"category"`
    Code        string    `json:"code"`
    Count       int       `json:"count"`
    FirstSeen   time.Time `json:"first_seen"`
    LastSeen    time.Time `json:"last_seen"`
    AffectedUsers int     `json:"affected_users"`
}

func (ea *ErrorAnalyzer) GenerateErrorSummary(ctx context.Context, since time.Duration) (*ErrorSummary, error) {
    startTime := time.Now().Add(-since)
    endTime := time.Now()

    // CloudWatch Insights クエリ実行
    query := `
    fields @timestamp, error_category, error_code, ctx_user_id, internal_error
    | filter @message like /Application error occurred/
    | stats count() by error_category, error_code
    | sort count desc
    `

    results, err := ea.executeInsightsQuery(ctx, query, startTime, endTime)
    if err != nil {
        return nil, err
    }

    // 結果を分析
    summary := &ErrorSummary{
        Period:        since,
        ErrorsByType:  make(map[string]int),
        TrendAnalysis: make(map[string][]TimeSeriesPoint),
    }

    for _, result := range results {
        category := result["error_category"].(string)
        code := result["error_code"].(string)
        count := int(result["count"].(float64))

        summary.ErrorsByType[category] += count
        summary.TotalErrors += count

        summary.TopErrors = append(summary.TopErrors, ErrorDetail{
            Category: category,
            Code:     code,
            Count:    count,
        })
    }

    // トレンド分析
    trendQuery := `
    fields @timestamp, error_category
    | filter @message like /Application error occurred/
    | bin(1h)
    | stats count() by bin(@timestamp), error_category
    | sort @timestamp
    `

    trendResults, err := ea.executeInsightsQuery(ctx, trendQuery, startTime, endTime)
    if err != nil {
        return nil, err
    }

    for _, result := range trendResults {
        timestamp := result["bin(@timestamp)"].(string)
        category := result["error_category"].(string)
        count := int(result["count"].(float64))

        ts, _ := time.Parse(time.RFC3339, timestamp)
        point := TimeSeriesPoint{
            Timestamp: ts,
            Value:     count,
        }

        summary.TrendAnalysis[category] = append(summary.TrendAnalysis[category], point)
    }

    return summary, nil
}

// Slack通知用のエラーサマリー
func (ea *ErrorAnalyzer) SendErrorSummaryToSlack(summary *ErrorSummary) error {
    if summary.TotalErrors == 0 {
        return nil // エラーなしの場合は通知しない
    }

    message := fmt.Sprintf(`
📊 *Error Summary Report* (Last %v)

🚨 *Total Errors:* %d

*Top Error Categories:*
%s

*Top Error Codes:*
%s

*Trend Analysis:*
%s

Dashboard: https://console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#dashboards:name=poc-cognite-errors
    `,
        summary.Period,
        summary.TotalErrors,
        ea.formatErrorsByType(summary.ErrorsByType),
        ea.formatTopErrors(summary.TopErrors),
        ea.formatTrendAnalysis(summary.TrendAnalysis),
    )

    return ea.sendSlackMessage(message)
}

type TimeSeriesPoint struct {
    Timestamp time.Time `json:"timestamp"`
    Value     int       `json:"value"`
}

実運用での効果

3ヶ月間のエラー対応効率向上

障害対応時間の劇的改善:

最適化前:
  平均対応時間: 2.5時間
  エラー原因特定時間: 1.8時間
  修復実装時間: 0.7時間
  
最適化後:
  平均対応時間: 0.4時間
  エラー原因特定時間: 0.1時間  # 統一ログで即座に特定
  修復実装時間: 0.3時間
  
改善率: 84%短縮

エラー分類・対応効率:
  自動分類率: 95%(従来は手動分析)
  重要度判定時間: 即座(従来は30分)
  影響範囲特定時間: 5分(従来は45分)
  
ユーザー体験改善:
  適切なエラーメッセージ表示率: 98%
  ユーザー問い合わせ削減: 60%
  ユーザー満足度向上: 7.2→8.4点

エラー発生パターンの可視化

3ヶ月間のエラー統計:

総エラー件数: 1,247件
  VALIDATION: 534件(42.8%)
  AUTHENTICATION: 312件(25.0%)
  INTERNAL: 89件(7.1%)
  EXTERNAL_SERVICE: 156件(12.5%)
  NOT_FOUND: 123件(9.9%)
  その他: 33件(2.6%)

修正されたエラーパターン:
  不適切なHTTPステータス: 127件→0件
  セキュリティ情報漏洩リスク: 23件→0件
  ユーザー向け不適切メッセージ: 89件→0件
  
新しく検出された問題:
  - 特定時間帯のDynamoDB接続タイムアウト
  - モバイルアプリからの不正なリクエストフォーマット
  - 外部API依存の予期しないレスポンス変更

自動復旧成功率: 78%(一時的エラーの自動リトライ)

コスト・ROI分析

エラーハンドリング統一化の投資対効果:

初期実装コスト:
  設計・実装工数: 32時間
  既存関数移行工数: 24時間
  テスト・検証工数: 16時間
  総工数: 72時間($3,600)

削減効果(月間):
  障害対応工数削減: 20時間 × $50/時間 = $1,000
  ユーザーサポート工数削減: 8時間 × $50/時間 = $400
  開発効率向上: 12時間 × $50/時間 = $600
  
  月間効果: $2,000
  年間効果: $24,000

ROI計算:
  投資: $3,600
  年間効果: $24,000
  ROI: 567%(非常に高い投資対効果)
  # ROI (Return on Investment) は「投資収益率」を意味し、投資した費用に対してどれだけの利益が得られたかを示す指標です。
  # ここでは、エラーハンドリングの統一化にかけたコストと、それによって得られた削減効果を比較しています。

無形価値:
  # 無形価値とは、数値では直接測れないものの、プロジェクトや組織にとって非常に重要な価値のことです。
  - 開発者のストレス軽減
  - システム信頼性向上
  - ユーザー満足度向上
  - 予測可能なエラー処理

まとめ:統一エラーハンドリングの価値

実現できた変革

運用効率

  • 障害対応時間: 84%短縮
  • エラー原因特定: 即座に判明
  • 自動分類・対応: 95%の自動化

品質向上

  • 一貫したエラー処理: 全11関数で統一
  • セキュリティリスク排除: 情報漏洩リスクの完全除去
  • ユーザー体験: 適切なエラーメッセージによる満足度向上

開発効率

  • 新機能開発: エラー処理の標準化による開発速度向上
  • 保守性: 統一パターンによる理解・修正の容易さ
  • テスタビリティ: 予測可能なエラー動作

なぜこの記録を残すのか

  1. 統一設計の価値: 分散システムでの一貫したエラー処理の重要性
  2. 実装パターン: 再利用可能な具体的実装方法
  3. 運用改善効果: 定量的な効果測定と投資対効果
  4. 継続改善: エラー分析による品質向上の仕組み

サーバーレス開発者の方へ

  • エラーハンドリングは「最後に追加」ではなく「最初から組み込む」設計です
  • 統一されたエラー処理は開発効率と運用効率を両方向上させます
  • 適切なエラー分類とログは障害対応を劇的に効率化します
  • エラーメトリクスは品質改善の重要な指標になります
  • ユーザー向けのエラーメッセージは体験価値に直結します

この統一エラーハンドリング戦略により、**「エラーが怖い」から「エラーも管理されている」**システムへの転換を実現できました。


実現された効果:

  • 効率化: 障害対応時間84%短縮、自動分類95%
  • 🛡️ 品質向上: 統一エラー処理、セキュリティリスク除去
  • 📊 可視化: エラー分析・監視の自動化
  • 👥 UX改善: ユーザー満足度7.2→8.4点向上
  • 💰 ROI: 567%の高い投資対効果(年間$24,000効果)