[アップデート] Lambda@Edgeでもログレベルの変更やJSON形式のログ出力を簡単に行えるようになりました
Lambda@Edgeでも出力するログレベルを楽にコントロールをしたい
こんにちは、のんピ(@non____97)です。
皆さんはLambda@Edgeでも出力するログレベル楽にコントロールをしたいなと思ったことはありますか? 私はあります。
以下記事で紹介されているアップデートでログ制御に関する以下対応が行えるようになりました。
- JSON 構造化形式での Lambda 関数ログのキャプチャ
- Lambda 関数ログのログレベルの詳細度の制御
- Lambda がログを送信する Amazon CloudWatch ロググループの設定
ただし、Lambda@Edgeはこのアップデートの恩恵を受けることはできていませんでした。Lambda@Edgeでは以下Lambda関数の機能がサポートされていません。
以下の Lambda 機能は、Lambda@Edge でサポートされていません。
- [自動] (デフォルト) 以外の Lambda ランタイム管理設定
- VPC 内のリソースにアクセスするための Lambda 関数の設定
- Lambda 関数のデッドレターキュー
- Lambda 環境変数 (自動的にサポートされる予約環境変数は除く)
- レイヤーによる AWS Lambda 依存関係の管理を使用する Lambda 関数
- AWS X-Ray の使用
- Lambda プロビジョニング済み同時実行
- コンテナイメージを使用した Lambda 関数の作成
- arm64 アーキテクチャを使用する Lambda 関数
- エフェメラルストレージが 512 MB を超える Lambda 関数
- JSON 構造化形式での Lambda 関数ログのキャプチャ
- Lambda 関数ログのログレベルの詳細度の制御
- Lambda がログを送信する Amazon CloudWatch ロググループの設定
- カスタマーマネージドキーを使用した .zip デプロイパッケージの暗号化
個人的にはデバッグなどで一時的にログレベルを変更したい時が辛いところです。
通常のLambda関数であれば環境変数でログレベルを渡し、内部でその環境変数をベースにLoggerを設定することが多いと思います。
ただし、Lambda@Edgeは環境変数の設定をすることができません。つまり、ログレベルを変更する際は都度Lambda関数のコード自体を変更する必要がありました。
今回アップデートでLambda@Edgeでも先述のログ制御が行えるようになりました。
これで簡単にログレベルの制御が可能になりました。
他にもJSON形式でのログ出力もLambda関数側で簡単に行える要因あったのは嬉しいポイントです。
実際に試してみたので紹介します。
やってみた
検証環境
検証環境は以下のとおりです。
以下記事で使用した仕組みを利用します。
この仕組みで言うとLambda@Edgeはオリジンリクエストの際にAcceptヘッダーやS3バケット上のオブジェクトを見て、WebPを返すか否かの判定をする処理を担っています。
Lambda関数のコード
Lambda@Edgeとして動作するLambda関数のコードは以下のとおりです。
import { CloudFrontRequestEvent } from "aws-lambda";
import {
S3Client,
HeadObjectCommand,
NotFound,
S3ServiceException,
} from "@aws-sdk/client-s3";
// Constants
const IMAGE_EXTENSION_PATTERN = /\.(jpe?g|png)$/i;
const s3Client = new S3Client({
followRegionRedirects: true,
region: process.env.AWS_REGION,
});
/**
* Lambda@Edge handler for WebP image conversion
*/
export const handler = async (event: CloudFrontRequestEvent) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
// Check WebP support using case-insensitive header check
const acceptHeader = request.headers["accept"]?.[0]?.value ?? "";
const viewerAcceptWebP = acceptHeader.split(",").some((type) => {
const [mimeType, params = ""] = type.trim().split(";");
if (mimeType === "image/webp") {
return true;
}
const qMatch = params.match(/q=([0-9.]+)/);
const q = qMatch ? parseFloat(qMatch[1]) : 1.0;
if (q < 1) {
return false;
}
return mimeType === "*/*" || mimeType === "image/*";
});
console.debug({
message: "WebP support check completed",
uri,
viewerAcceptWebP,
acceptHeader,
});
// Process if the request is for an image and browser supports WebP
if (viewerAcceptWebP && IMAGE_EXTENSION_PATTERN.test(uri)) {
// Extract bucket information from origin
const s3Origin = request.origin?.s3;
if (!s3Origin?.domainName) {
console.debug({
message: "S3 origin not found",
uri,
});
return request;
}
const bucketName = s3Origin.domainName.split(".")[0];
const webpKey = uri.startsWith("/")
? uri.slice(1) + ".webp"
: uri + ".webp";
try {
// Check if WebP version exists
await s3Client.send(
new HeadObjectCommand({
Bucket: bucketName,
Key: webpKey,
})
);
// WebP exists, modify request
request.headers["x-original-uri"] = [
{
key: "x-original-uri",
value: uri,
},
];
request.uri = `${uri}.webp`;
console.debug({
message: "WebP version found and request modified",
originalUri: uri,
newUri: request.uri,
});
} catch (error) {
if (error instanceof NotFound) {
// WebP file doesn't exist, silently use original image
console.debug({
message: "WebP version not found, using original image",
uri,
webpKey,
});
return request;
}
// Log other errors
console.error({
message: "Error checking WebP existence",
region: process.env.AWS_REGION,
bucket: bucketName,
key: webpKey,
error: error instanceof Error ? error.message : String(error),
errorStack: error instanceof Error ? error.stack : undefined,
errorType: error instanceof S3ServiceException ? error.name : "Unknown",
});
}
} else {
console.debug({
message: "Skipping WebP processing",
uri,
isImage: IMAGE_EXTENSION_PATTERN.test(uri),
supportsWebP: viewerAcceptWebP,
});
}
return request;
};
ひとしきりconsole.debug()
でログレベルがDEBUG以下の場合にログ出力するようにしています。
AWS CDKのコード
AWS CDKのコードは以下のとおりです。
return new cdk.aws_lambda_nodejs.NodejsFunction(this, functionName, {
runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
bundling: {
minify: true,
tsconfig: path.join(__dirname, "../src/lambda/tsconfig.json"),
format: cdk.aws_lambda_nodejs.OutputFormat.ESM,
},
awsSdkConnectionReuse: false,
architecture: cdk.aws_lambda.Architecture.X86_64,
timeout: cdk.Duration.seconds(5),
role: role,
entry: path.join(__dirname, entry),
+ loggingFormat: cdk.aws_lambda.LoggingFormat.JSON,
+ applicationLogLevelV2: cdk.aws_lambda.ApplicationLogLevel.DEBUG,
+ systemLogLevelV2: cdk.aws_lambda.SystemLogLevel.INFO,
});
アプリケーションログはログレベルをDEBUG、システムログはINFOでJSON形式でログ出力するようにしています。
アプリケーションログとシステムログで設定できるログレベルは以下のとおりです。
ログレベル | 標準的な使用状況 |
---|---|
TRACE (最も詳細) | コードの実行パスを追跡するために使用される最も詳細な情報 |
DEBUG | システムデバッグの詳細情報 |
INFO | 関数の通常の動作を記録するメッセージ |
WARN | 対処しないと予期しない動作を引き起こす可能性がある潜在的なエラーに関するメッセージ |
ERROR | コードが期待どおりに動作しなくなる問題に関するメッセージ |
FATAL (詳細度が最も低い) | アプリケーションの機能停止を引き起こす重大なエラーに関するメッセージ |
ログレベル | 使用方法 |
---|---|
DEBUG (詳細度が最も高い) | システムデバッグの詳細情報 |
INFO | 関数の通常の動作を記録するメッセージ |
WARN (詳細度が最も低い) | 対処しないと予期しない動作を引き起こす可能性がある潜在的なエラーに関するメッセージ |
抜粋 : Lambda 関数の高度なログ記録コントロールの設定 - AWS Lambda
動作確認
この状態でアクセスしてみます。
> curl -I https://www.non-97.net/test.png -H "Accept:image/webp"
HTTP/2 404
content-type: text/html
content-length: 12
date: Thu, 17 Apr 2025 01:23:22 GMT
last-modified: Tue, 25 Feb 2025 02:38:39 GMT
etag: "347dfa37997b9353b3da6992f8753439"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 00e57612ea90b844bafde55ba310ccc8.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: Q5PLmuIWUEt5-AS-GGslc-laYQcyIBuZrZUWDNwCnz0MrYoO496pCQ==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
> curl -I https://www.non-97.net/non__97.png -H "Accept:image/png, image/*;q=1.0"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Thu, 17 Apr 2025 01:24:22 GMT
last-modified: Tue, 25 Feb 2025 02:38:38 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 7d7a3c2bfaf3829a0c2cf20c167810ae.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: 57hbHuPqg1W3FkGA53pAk2L4GXPH4oERdb4XwDkyt8s8YeXiqco_Wg==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
CloudWatch Logsで確認すると、以下ログが出力されていました。
{
"time": "2025-04-17T01:23:18.661Z",
"type": "platform.initStart",
"record": {
"initializationType": "on-demand",
"phase": "init",
"runtimeVersion": "nodejs:22.v35",
"runtimeVersionArn": "arn:aws:lambda:ap-northeast-1::runtime:8ce0861cbacc1c3e68742d7537616587fd39490817ec413514cf66613b6bed7d",
"functionName": "us-east-1.WebsiteStack-ContentsDeliveryConstructRewriteToWeb-qBTJvrRRqQ0Q",
"functionVersion": "2",
"instanceId": "2025/04/17/[2]b095dacd261d406a9b9ff1acd76e2f17",
"instanceMaxMemory": 134217728
}
}
{
"time": "2025-04-17T01:23:19.080Z",
"type": "platform.start",
"record": {
"requestId": "d4ce37e3-3ded-46fb-a479-f0faeda7e5dc",
"version": "2"
}
}
{
"timestamp": "2025-04-17T01:23:19.082Z",
"level": "DEBUG",
"requestId": "d4ce37e3-3ded-46fb-a479-f0faeda7e5dc",
"message": {
"message": "WebP support check completed",
"uri": "/test.png",
"viewerAcceptWebP": true,
"acceptHeader": "image/webp"
}
}
{
"timestamp": "2025-04-17T01:23:20.608Z",
"level": "DEBUG",
"requestId": "d4ce37e3-3ded-46fb-a479-f0faeda7e5dc",
"message": {
"message": "WebP version not found, using original image",
"uri": "/test.png",
"webpKey": "test.png.webp"
}
}
{
"time": "2025-04-17T01:23:20.629Z",
"type": "platform.report",
"record": {
"requestId": "d4ce37e3-3ded-46fb-a479-f0faeda7e5dc",
"metrics": {
"durationMs": 1548.054,
"billedDurationMs": 1549,
"memorySizeMB": 128,
"maxMemoryUsedMB": 98,
"initDurationMs": 415.632
},
"status": "success"
}
}
{
"time": "2025-04-17T01:24:19.901Z",
"type": "platform.start",
"record": {
"requestId": "67dcef0c-52da-4052-b511-6e4bb8dc9935",
"version": "2"
}
}
{
"timestamp": "2025-04-17T01:24:20.107Z",
"level": "DEBUG",
"requestId": "67dcef0c-52da-4052-b511-6e4bb8dc9935",
"message": {
"message": "WebP support check completed",
"uri": "/non__97.png",
"viewerAcceptWebP": true,
"acceptHeader": "image/png, image/*;q=1.0"
}
}
{
"timestamp": "2025-04-17T01:24:20.949Z",
"level": "DEBUG",
"requestId": "67dcef0c-52da-4052-b511-6e4bb8dc9935",
"message": {
"message": "WebP version found and request modified",
"originalUri": "/non__97.png",
"newUri": "/non__97.png.webp"
}
}
{
"time": "2025-04-17T01:24:21.007Z",
"type": "platform.report",
"record": {
"requestId": "67dcef0c-52da-4052-b511-6e4bb8dc9935",
"metrics": {
"durationMs": 1105.321,
"billedDurationMs": 1106,
"memorySizeMB": 128,
"maxMemoryUsedMB": 99
},
"status": "success"
}
}
アプリケーションログレベルをDEBUG
からINFO
に変更して再度デプロイします。
return new cdk.aws_lambda_nodejs.NodejsFunction(this, functionName, {
runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
bundling: {
minify: true,
tsconfig: path.join(__dirname, "../src/lambda/tsconfig.json"),
format: cdk.aws_lambda_nodejs.OutputFormat.ESM,
},
awsSdkConnectionReuse: false,
architecture: cdk.aws_lambda.Architecture.X86_64,
timeout: cdk.Duration.seconds(5),
role: role,
entry: path.join(__dirname, entry),
loggingFormat: cdk.aws_lambda.LoggingFormat.JSON,
+ applicationLogLevelV2: cdk.aws_lambda.ApplicationLogLevel.INFO,
- applicationLogLevelV2: cdk.aws_lambda.ApplicationLogLevel.DEBUG,
systemLogLevelV2: cdk.aws_lambda.SystemLogLevel.INFO,
});
デプロイ後に同様にアクセスします。
> curl -I https://www.non-97.net/test.png -H "Accept:image/webp"
HTTP/2 404
content-type: text/html
content-length: 12
date: Thu, 17 Apr 2025 04:33:35 GMT
last-modified: Tue, 25 Feb 2025 02:38:39 GMT
etag: "347dfa37997b9353b3da6992f8753439"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 e47b43971a4fe3d8e6ac20fb2a92327c.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: J16PYkurL-rG4VwgtbSG6bMNkduHSwaN4XH0nJQlwyqm33JwGjBBPg==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
> curl -I https://www.non-97.net/non__97.png -H "Accept:image/png, image/*;q=1.0"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Thu, 17 Apr 2025 04:33:39 GMT
last-modified: Tue, 25 Feb 2025 02:38:38 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 fe707d17f9bf2bbec18e874a73b8a21a.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: dUzH1EJs5Yob1HlHchw7o3bxXtq_m3_l1f4lohDUtLmWUc9-fp3Bcw==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
この時のログは以下のとおりです。
{
"time": "2025-04-17T04:33:30.597Z",
"type": "platform.initStart",
"record": {
"initializationType": "on-demand",
"phase": "init",
"runtimeVersion": "nodejs:22.v35",
"runtimeVersionArn": "arn:aws:lambda:ap-northeast-1::runtime:8ce0861cbacc1c3e68742d7537616587fd39490817ec413514cf66613b6bed7d",
"functionName": "us-east-1.WebsiteStack-ContentsDeliveryConstructRewriteToWeb-qBTJvrRRqQ0Q",
"functionVersion": "3",
"instanceId": "2025/04/17/[3]945c29df82034ba3bed42db43a145b85",
"instanceMaxMemory": 134217728
}
}
{
"time": "2025-04-17T04:33:31.088Z",
"type": "platform.start",
"record": {
"requestId": "8d3b0c27-475b-4ada-aa6b-95f6e33d39a4",
"version": "3"
}
}
{
"time": "2025-04-17T04:33:32.588Z",
"type": "platform.report",
"record": {
"requestId": "8d3b0c27-475b-4ada-aa6b-95f6e33d39a4",
"metrics": {
"durationMs": 1499.572,
"billedDurationMs": 1500,
"memorySizeMB": 128,
"maxMemoryUsedMB": 100,
"initDurationMs": 487.745
},
"status": "success"
}
}
{
"time": "2025-04-17T04:33:37.665Z",
"type": "platform.start",
"record": {
"requestId": "a521e81b-fc2d-407f-b1d0-f28ea8b1ef3b",
"version": "3"
}
}
{
"time": "2025-04-17T04:33:37.907Z",
"type": "platform.report",
"record": {
"requestId": "a521e81b-fc2d-407f-b1d0-f28ea8b1ef3b",
"metrics": {
"durationMs": 241.122,
"billedDurationMs": 242,
"memorySizeMB": 128,
"maxMemoryUsedMB": 100
},
"status": "success"
}
}
はい、先ほど出力されていたDEBUGログが出力されなくなりました。
意図した通りに動作していますね。
Lambda@Edgeのログ制御が楽になった
Lambda@Edgeでもログレベルの変更やJSON形式のログ出力を簡単に行えるようになったアップデートを紹介しました。
ログレベルの切り替えが面倒だなと思っていた方にとっては朗報ではないでしょうか。嬉しいアップデートです。
また、個人的にはログ分析の観点からJSON形式でのログ出力がオススメです。ログ分析基盤との兼ね合いを考慮しながら、JSON形式で出力できるならJSON形式で出力すると良いかと思います。
今回の検証で使用したリポジトリは以下に保存しています。
この記事が誰かの助けになれば幸いです。
以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!