Lambda Web AdapterでrequestIdをログ出力する

素の Python Lambda だと logging モジュールを利用することで簡単に requestId をログに出力することができます。requestId があると、CloudWatch Logs Insights で requestId でフィルターできるためエラーが発生した際のログ調査を迅速に行うことができて便利です。

import logging

logger = logging.getLogger()
logger.setLevel("INFO")
  
def lambda_handler(event, context):
    logger.info("test")

しかし、Lambda Web Adapter だとそうはいきません。FastAPI を利用して同じように実装しても requestId は出力されません。

import logging

from fastapi import FastAPI


logger = logging.getLogger("uvicorn")
logger.setLevel("INFO")

app = FastAPI()


@app.get("/")
def read_root():
    logger.info("test")
    return {"Hello": "World"}

今回は Lambda Web Adapter で requestId を出力してみます。FastAPI での例ですが、他の言語・フレームワークでも似たようなロジックで実装可能ですので適宜読み替えてもらえればと思います。

どのようにして requestId を取得するか

Lambda Web Adapter の README を見ると以下の記載があります。リクエストヘッダーの "x-amzn-lambda-context" に何かしらの情報があるように読み取れます。

Lambda Web Adapter forwards this information to the web application in a Http Header named "x-amzn-lambda-context". In the web application, you can retrieve the value of this http header and deserialize it into a JSON object. Check out Express.js in Zip on how to use it.

github.com

実際にリクエストヘッダーをのぞいてみます。

@app.get("/")
def read_root(req: Request):
    logger.info(req.headers)
    return {"Hello": "World"}

以下のようなログが出力されました。

JSON に整形すると以下の形式です。ドキュメントに記載されている通り、"x-amzn-lambda-context" フィールド内の request_id が Lambda の requestId と一致しています。ちなみに "x-amzn-request-context" は API Gateway から Lambda に送信するメタデータのようです。

当初は "x-amzn-request-context" の requestId を参照すればよいのかなと思いましたが、Lambda 起動時に出力される requestId とは一致しなかったため "x-amzn-lambda-context" を参照するのが正しいと判断しました。今回用意した環境では Lambda Function URL を利用しているため "x-amzn-request-context" の requestId と "x-amzn-lambda-context" の request_id と一致しているものと考えられます。

{
  "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256",
  "x-amzn-tls-version": "TLSv1.3",
  "x-amzn-trace-id": "Root=1-66b4ead5-533d2c0e3c4e9f076fe8a483;Parent=371a72bb3eefa053;Sampled=0;Lineage=01ffc78c:0",
  "x-forwarded-proto": "https",
  "host": "<host>",
  "x-forwarded-port": "443",
  "x-forwarded-for": "<x-forwarded-for>",
  "accept": "*/*",
  "user-agent": "curl/7.88.1",
  "x-amzn-request-context": "{\"routeKey\":\"$default\",\"accountId\":\"anonymous\",\"stage\":\"$default\",\"requestId\":\"85203a83-e02c-4284-b6dd-3f0def1ae128\",\"apiId\":\"<apiId>\",\"domainName\":\"<host>\",\"domainPrefix\":\"<domainPrefix>\",\"time\":\"08/Aug/2024:15:57:09 +0000\",\"timeEpoch\":1723132629444,\"http\":{\"method\":\"GET\",\"path\":\"/\",\"protocol\":\"HTTP/1.1\",\"sourceIp\":\"<sourceIp>\",\"userAgent\":\"curl/7.88.1\"}}",
  "x-amzn-lambda-context": "{\"request_id\":\"85203a83-e02c-4284-b6dd-3f0def1ae128\",\"deadline\":1723132633704,\"invoked_function_arn\":\"<invoked_function_arn>\",\"xray_trace_id\":\"Root=1-66b4ead5-533d2c0e3c4e9f076fe8a483;Parent=371a72bb3eefa053;Sampled=0;Lineage=01ffc78c:0\",\"client_context\":null,\"identity\":null,\"env_config\":{\"function_name\":\"lambda-web-adapter-sample\",\"memory\":128,\"version\":\"$LATEST\",\"log_stream\":\"\",\"log_group\":\"\"}}",
  "content-length": "0"
}

実装

それでは requestId を取得してログ出力してみます。"x-amzn-lambda-context" の値は文字列型のため、いちど json.loads して dict 型に変換し、request_id を取り出す関数を用意します。ローカルでの動作確認も想定されるため、"x-amzn-lambda-context" が存在しない場合は None で出力します。

def get_request_id(headers: Headers) -> str | None:
    lambda_context = headers.get("x-amzn-lambda-context")
    if not lambda_context:
        return None

    request_id = json.loads(lambda_context).get("request_id")
    return request_id


@app.get("/")
def read_root(req: Request):
    request_id = get_request_id(req.headers)
    logger.info(f"request_id: {request_id}")

    return {"Hello": "World"}

ローカル(Docker Compose) では以下のようにログに出力されました。想定通りです。

app-1  | INFO:     request_id: None
app-1  | INFO:     192.168.65.1:55473 - "GET / HTTP/1.1" 200 OK

では Lambda をデプロイしてログを確認してみます。無事 requestId が出力されました。やったね!

おわりに

Lambda Web Adapter で requestId を取得する方法を紹介しました。今回はシンプルに requestId を出力するのみでしたが、実運用では logger のレコードに組み込んだ方がよいでしょう。僕がいま関わっているプロジェクトでは共通 logger を用意してそこで requestId や request URL、HTTP Method などを JSON で出力していて、CloudWatch Logs Insights での調査が容易に行えるようになりました。その話もまたどこかで紹介できればと思います。

余談ですが今回用意した Lambda は CDK で作成しています。最初は API Gateway もつくろうかなと思ったのですが面倒だったので Lambda Function URL を有効化してみました。想像以上に簡単に設定できたので、このようなテンポラリな環境を立ち上げるぶんには十分ですね。

アプリケーションコードと CDK もふくめたサンプルコード全文は以下を参照してください。

github.com