🚀

ここから始める Powertools for AWS Lambda (Python) - Metrics 編 -

に公開

はじめに

こんにちは。
WHERE プロダクト開発部で主にインフラを担当している えんどぅ です。
みなさん元気に Lambda Life を送っていますか?

Lambda 関数に限らず、システム運用ではアプリケーションのパフォーマンスやビジネスメトリクスを可視化することが重要です。
しかし、CloudWatch にカスタムメトリクスを送信するには、PutMetricData API を使った同期的な処理が必要で、Lambda 関数の実行時間とコストに影響を与えます。
また、メトリクスの定義ミス (単位, 値, ディメンションの上限など) を事前に検証する仕組みもありません。

そんな課題を解決してくれるのが、Powertools for AWS Lambda の Metrics 機能です。
今回はこの機能について、実際の使用イメージと共に紹介します。

なお、今後も Powertools for AWS Lambda の各種機能について継続的にブログを書いていく予定なので、興味のある方は楽しみにしていただけると嬉しいです。

※ 過去のブログ記事はコチラ👇

Powertools for AWS Lambda とは

Powertools for AWS Lambda は、サーバーレスベストプラクティスの実装と開発者の生産性向上を支援する開発者ツールキットです。

段階的に導入できる柔軟な設計により、必要な機能から順次採用することができます。

Python, TypeScript, Java, .NET で提供されており、AWS Well-Architected Serverless Lens に基づく推奨事項を簡単に実装できます。

Metrics 機能

Metrics 機能 は、Amazon CloudWatch Embedded Metric Format (EMF) に従ってメトリクスを標準出力にログ出力することで、カスタムメトリクスを非同期的に作成するユーティリティです。

これらのメトリクスは Amazon CloudWatch コンソールで可視化できます。

主な特徴

  • 単一の CloudWatch EMF オブジェクト (大きな JSON blob) を使用して最大 100 個のメトリクスを集約
  • 一般的なメトリクス定義のミス (メトリクス単位, 値, 最大ディメンション数, 最大メトリクス数など) に対するバリデーション
  • CloudWatch サービスによってメトリクスが非同期的に作成されるため、カスタムスタックは不要
  • 異なるディメンションを持つ単一メトリクスを作成するためのコンテキストマネージャー

実践: Lambda 関数の構築・実装

ここからは、実際に Lambda 関数を構築・実装するイメージを紹介します。

※ プロジェクトの完全なコードは コチラ

使用ツール バージョン
aws-cdk-cli 2.1021.0
aws-cdk-lib 2.199.0
aws-cli 2.13.32
node 22.13.1
python 3.11.8
uv 0.6.14
jq 1.7

0. プロジェクトの初期化

Terminal
# プロジェクトディレクトリの作成
mkdir metrics && \
cd metrics
Terminal
# AWS CDK プロジェクトの初期化
cdk init app --language python
Terminal
# プロジェクトのセットアップ
source .venv/bin/activate && \
uv init && \
uv add --group infra -r requirements.txt && \
uv add --dev -r requirements-dev.txt && \
mkdir lambda && \
touch lambda/function.py && \
rm requirements.txt requirements-dev.txt main.py

主なプロジェクト構成は以下のようになります:

metrics/
├── metrics/
│   └── metrics_stack.py          # AWS CDK スタック定義
├── lambda/                       # Lambda 関数のソースコード
│   └── function.py               # Metrics 機能使用版
├── app.py                        # AWS CDK アプリケーションのエントリーポイント
├── cdk.json                      # AWS CDK 設定ファイル
├── pyproject.toml                # 依存関係管理ファイル
└── uv.lock                       # 依存関係管理ファイル

1. 依存関係の設定

Terminal
# Powertools for AWS Lambda の追加
uv add --group lambda aws-lambda-powertools

2. Lambda 関数の実装

lambda/function.py
import json
import os
from decimal import Decimal
from typing import Any

from aws_lambda_powertools import Metrics
from aws_lambda_powertools.metrics import MetricResolution, MetricUnit
from aws_lambda_powertools.utilities.typing import LambdaContext

# 環境変数から取得
STAGE = os.getenv("STAGE", "dev")

# Metrics のインスタンス化
metrics = Metrics(service="ecommerce-api")

# デフォルトディメンションの設定
metrics.set_default_dimensions(environment=STAGE)


def validate_order(order_data: dict[str, Any]) -> bool:
    """注文データのバリデーション"""
    required_fields = ["order_id", "product_id", "quantity", "price"]

    # バリデーションの成功/失敗をメトリクスとして記録
    if not all(field in order_data for field in required_fields):
        metrics.add_metric(
            name="OrderValidationFailure", unit=MetricUnit.Count, value=1
        )
        return False

    if order_data["quantity"] <= 0 or order_data["price"] <= 0:
        metrics.add_metric(
            name="OrderValidationFailure", unit=MetricUnit.Count, value=1
        )
        return False

    # バリデーション成功
    metrics.add_metric(name="OrderValidationSuccess", unit=MetricUnit.Count, value=1)
    return True


def calculate_total_amount(price: Decimal, quantity: int) -> Decimal:
    """合計金額の計算"""
    total = price * quantity

    # 高解像度メトリクスで注文金額を記録 (1秒の粒度)
    metrics.add_metric(
        name="OrderAmount",
        unit=MetricUnit.None_,
        value=float(total),
        resolution=MetricResolution.High,
    )

    return total


def process_order(order_data: dict[str, Any]) -> dict[str, Any]:
    """注文処理"""
    order_id = order_data["order_id"]
    product_id = order_data["product_id"]
    quantity = order_data["quantity"]
    price = Decimal(str(order_data["price"]))

    # ディメンションを追加して特定の商品の注文数を追跡
    metrics.add_dimension(name="product_id", value=product_id)

    # 商品ごとの注文数をメトリクスとして記録
    metrics.add_metric(name="OrdersPerProduct", unit=MetricUnit.Count, value=1)

    # 合計金額の計算
    total_amount = calculate_total_amount(price, quantity)

    # メタデータとして高カーディナリティデータを追加
    # (メトリクスの可視化では利用できないが、ログでの検索に有用)
    metrics.add_metadata(
        key="order_details",
        value={
            "order_id": order_id,
            "product_id": product_id,
            "quantity": quantity,
            "total_amount": str(total_amount),
        },
    )

    # 処理成功メトリクス
    metrics.add_metric(name="OrderProcessed", unit=MetricUnit.Count, value=1)

    return {
        "order_id": order_id,
        "status": "processed",
        "total_amount": str(total_amount),
    }


@metrics.log_metrics(capture_cold_start_metric=True)
def lambda_handler(event: dict[str, Any], context: LambdaContext) -> dict[str, Any]:
    """
    E コマース注文処理の Lambda ハンドラー

    Metrics の log_metrics デコレーターにより:
    - すべてのメトリクスが関数終了時に自動的にフラッシュされる
    - メトリクスのバリデーションが実行される
    - capture_cold_start_metric=True によりコールドスタートメトリクスが別途記録される
    """
    try:
        # イベントから注文データを取得
        order_data = event.get("order", {})

        # バリデーション
        if not validate_order(order_data):
            return {
                "statusCode": 400,
                "body": json.dumps({"error": "Invalid order data"}),
            }

        # 注文処理
        result = process_order(order_data)

        # API レスポンスの成功メトリクス
        metrics.add_metric(name="APISuccess", unit=MetricUnit.Count, value=1)

        return {"statusCode": 200, "body": json.dumps(result)}

    except Exception:
        # エラーメトリクス
        metrics.add_metric(name="APIError", unit=MetricUnit.Count, value=1)

        return {
            "statusCode": 500,
            "body": json.dumps({"error": "Internal server error"}),
        }

3. AWS CDK スタックの定義

metrics/metrics_stack.py
from aws_cdk import CfnOutput, Duration, Stack
from aws_cdk import aws_lambda as _lambda
from aws_cdk import aws_logs as logs
from constructs import Construct


class MetricsStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Lambda Layer: Powertools for AWS Lambda (Python)
        layer = _lambda.LayerVersion.from_layer_version_arn(
            self,
            "PowertoolsLayer",
            layer_version_arn="arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-x86_64:20",
        )

        # Lambda 関数
        function = _lambda.Function(
            self,
            "Function",
            runtime=_lambda.Runtime.PYTHON_3_11,
            handler="function.lambda_handler",
            code=_lambda.Code.from_asset("lambda"),
            layers=[layer],
            timeout=Duration.seconds(30),
            environment={
                "STAGE": "dev",
                "POWERTOOLS_SERVICE_NAME": "ecommerce-api",
                "POWERTOOLS_METRICS_NAMESPACE": "EcommerceApp",
                # オプション: 関数名をカスタマイズ (ColdStart メトリクス用)
                "POWERTOOLS_METRICS_FUNCTION_NAME": "order-processor",
            },
            log_retention=logs.RetentionDays.ONE_WEEK,
        )

        # 出力
        CfnOutput(
            self,
            "FunctionName",
            value=function.function_name,
            export_name="FunctionName",
        )

4. デプロイ・動作確認

Terminal
# デプロイの実行
cdk deploy
Terminal
# テスト用のペイロード作成
cat > test_payload.json << 'EOF'
{
  "order": {
    "order_id": "ORD-12345",
    "product_id": "PROD-67890",
    "quantity": 3,
    "price": 1500
  }
}
EOF
Terminal
# Lambda 関数のテスト
aws lambda invoke \
  --function-name <関数名> \
  --cli-binary-format raw-in-base64-out \
  --payload file://test_payload.json \
  response.json && \
cat response.json | jq

CloudWatch Logs に出力される EMF の例

{
    "_aws": {
        "Timestamp": 1763175442681,
        "CloudWatchMetrics": [
            {
                "Namespace": "EcommerceApp",
                "Dimensions": [
                    [
                        "environment",
                        "product_id",
                        "service"
                    ]
                ],
                "Metrics": [
                    {
                        "Name": "OrderValidationSuccess",
                        "Unit": "Count"
                    },
                    {
                        "Name": "OrdersPerProduct",
                        "Unit": "Count"
                    },
                    {
                        "Name": "APIError",
                        "Unit": "Count"
                    }
                ]
            }
        ]
    },
    "environment": "dev",
    "product_id": "PROD-67890",
    "service": "ecommerce-api",
    "OrderValidationSuccess": [
        1
    ],
    "OrdersPerProduct": [
        1
    ],
    "APIError": [
        1
    ]
}

CloudWatch コンソールでメトリクスを確認

デプロイ後、CloudWatch コンソールで以下の情報を確認できます:

  • カスタムメトリクス: EcommerceApp ネームスペースの下に、定義したすべてのメトリクスが表示される
  • ディメンションでのフィルタリング: environment, service, product_id などのディメンションでメトリクスを絞り込み

5. リソースのクリーンアップ

Terminal
cdk destroy -f

Metrics 機能を使用するメリット

1. 非同期メトリクス作成によるパフォーマンス向上

CloudWatch EMF を使用することで、メトリクスは CloudWatch Logs 経由で非同期的に作成されます。
これにより、PutMetricData API を使った同期的な処理と比較して、Lambda 関数の実行時間とコストを削減できます。

2. 自動バリデーションによるミス防止

log_metrics デコレーターを使用することで、以下のバリデーションが自動的に実行されます:

  • 最大 29 個のユーザー定義ディメンション
  • ネームスペースが設定されていること
  • メトリクス単位が CloudWatch でサポートされていること

これにより、メトリクス定義のミスを事前に検出できます。

3. 最大 100 個のメトリクスを効率的に集約

単一の EMF オブジェクトで最大 100 個のメトリクスを集約できます。
100 個目のメトリクスを追加すると、Metrics ユーティリティが自動的にすべてのメトリクスをフラッシュし、101 個目以降は新しい EMF オブジェクトに集約されます。

4. デフォルトディメンションによる一貫性の確保

set_default_dimensions メソッドや log_metrics デコレーターの default_dimensions パラメーターを使用することで、Lambda 呼び出し間でディメンションを永続化できます。
これにより、すべてのメトリクスに一貫したディメンションを設定できます。

5. コールドスタートメトリクスの自動キャプチャ

capture_cold_start_metric=True を設定することで、コールドスタート時に専用のメトリクスが自動的に作成されます。
このメトリクスはアプリケーションメトリクスとは別の EMF オブジェクトとして記録されるため、関係のないディメンションによるデータ汚染を防ぎます。

6. 高解像度メトリクスのサポート

MetricResolution.High を指定することで、1 秒の粒度を持つ高解像度メトリクスを作成できます。これは、テレメトリ、時系列データ、リアルタイムインシデント管理などに非常に有用です。

7. メタデータによる高カーディナリティデータの追加

add_metadata メソッドを使用することで、メトリクスログの一部として高カーディナリティデータを追加できます。
これは、メトリクスと共に高度にコンテキスト情報を検索したい場合に便利です (ただし、メトリクス可視化では利用できません)。

まとめ

本記事では、Powertools for AWS Lambda (Python) の Metrics 機能について紹介しました。

特に、ビジネスメトリクスやアプリケーションのパフォーマンス指標を可視化することは、プロダクトの成長やトラブルシューティングに不可欠です。
Metrics 機能を活用することで、これらのベストプラクティスを簡単かつ効率的に実装できます。

Powertools for AWS Lambda に興味のある方、Lambda 関数のメトリクス収集に課題を感じている方の参考になれば嬉しいです。

今後も Powertools for AWS Lambda の各種機能についてブログを書いていく予定ですので、お楽しみに。

参考資料

最後に

株式会社 WHERE (旧: 株式会社 Penetrator) は、シリーズ A ラウンドにおいて総額 5.5 億円の資金調達を実施し、不動産テック業界における更なる成長を目指して採用活動を一層強化しています。

エンジニア, デザイナー, カスタマーサクセス, BizDev, 営業, マーケティングなど、事業拡大を支える多様なポジションで共に挑戦していただける方を待っています!!

▽ 会社のカルチャーを知りたい方はこちら
https://www.wantedly.com/companies/company_9924832

▽ 募集職種を知りたい方はこちら
https://www.wantedly.com/companies/company_9924832/projects

Discussion