AWS Lambda 関数で Winston のログを X-Ray のトレースと紐付ける
こんにちは、製造ビジネステクノロジー部の若槻です。
最近 AWS Lambda 関数ランタイム(Node.js)上でロガーツールとして Winston を採用する機会がありました。Winston を使うと各種レベルの構造化ログを標準出力するロガーを簡単に仕込むことが可能となります。
さて、AWS Lambda ベースのサーバーレスオブザーバビリティ | AWS Observability Best Practices に則るなら、ログに加えて取得をするようにしておきたいのが、アプリケーションフロー全体でマイクロサービス間のリクエストを可視化するトレースのデータです。AWS の場合だと AWS X-Ray を使ってトレースを取得し、さらにそのトレースに CloudWatch Logs のログを紐付けることにより、効率的なリクエストの追跡が可能となります。
この AWS X-Ray のトレースは、例えば Powertools for AWS Lambda (TypeScript) の Logger を使っている場合は簡単な設定でログとの紐付けが行えます。一方で、Winston などその他のロギングライブラリを使っている場合はその紐付けにひと工夫が必要です。
今回は、AWS Lambda 関数で Winston の出力ログを X-Ray のトレースと紐付ける実装を試してみました。
試してみた
結論から言うと、次のように Winston のカスタムフォーマットを使うことで、AWS X-Ray のトレース ID をログに追加することができました。(Serverless Express は Lambda プロキシ統合を簡単にするために使っているだけです)
import serverlessExpress from "@codegenie/serverless-express";
import express, { Request, Response } from "express";
import winston from "winston";
import AWSXRay from "aws-xray-sdk-core";
// Winston フォーマットに AWS X-Ray の情報を追加するカスタムフォーマット
const addXRay = winston.format((info) => {
try {
const seg = AWSXRay.getSegment(); // 現在のセグメントを取得
if (seg) {
info.traceId = (seg as any).trace_id;
}
} catch {}
return info;
});
// Winston ロガーの設定
const logger = winston.createLogger({
format: winston.format.combine(
addXRay(), // AWS X-Ray の情報を追加
winston.format.json() // JSON 形式でログを出力
),
transports: [new winston.transports.Console()], // 標準出力(コンソール)にログを出力
});
const app = express();
app.use(express.json());
app.get("/hello", async (_: Request, res: Response): Promise<void> => {
logger.info("Hello request", { path: "/hello" }); // AWS X-Ray の情報を含むログ出力
res.json({ message: "Hello, world!" });
});
export const handler = serverlessExpress({ app });
Lambda 関数および API Gateway Rest API の設定は次のように行いました。ここでは、AWS CDK を使ってリソースを定義しています。ここで Lambda 関数のトレースの有効化は忘れずに行うようにしましょう。
import * as cdk from "aws-cdk-lib";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as logs from "aws-cdk-lib/aws-logs";
import * as lambda_nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
export class MainStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
/**
* Lambda Function
*/
const handler = new lambda_nodejs.NodejsFunction(this, "Handler", {
entry: "src/rest-api-router.ts",
logGroup: new logs.LogGroup(this, "LogGroup"),
tracing: lambda.Tracing.ACTIVE, // AWS X-Ray によるトレースを有効化
});
/**
* API Gateway REST API
*/
new apigateway.LambdaRestApi(this, "RestApi", {
handler,
deployOptions: {
tracingEnabled: true, // AWS X-Ray によるトレースを有効化
},
});
}
}
動作確認を行います。REST API にリクエストを送信します。
$ curl https://exk563a88b.execute-api.ap-northeast-1.amazonaws.com/prod/hello
{"message":"Hello, world!"}
AWS X-Ray のコンソールでトレースを確認すると、トレース ID が含まれているログが表示されていますね。
ログを開いた様子です。先ほどのコードで追加した traceId
及び path
、message
が含まれています。
以上、Winston の出力ログを X-Ray のトレースと紐付けることができました。
Winston ロガーに X-Ray の情報を追加しない場合
念の為、Winston ロガーに X-Ray の情報を追加しない場合のログ出力も確認しておきます。以下のように addXRay()
フォーマットを削除した場合、X-Ray のトレース ID はログに含まれませんでした。
addXRay() フォーマットを削除した場合の実装
import serverlessExpress from "@codegenie/serverless-express";
import express, { Request, Response } from "express";
import winston from "winston";
// Winston ロガーの設定
const logger = winston.createLogger({
format: winston.format.combine(
// AWS X-Ray の情報を追加しない
winston.format.json() // JSON 形式でログを出力
),
transports: [new winston.transports.Console()], // 標準出力(コンソール)にログを出力
});
const app = express();
app.use(express.json());
app.get("/hello", async (_: Request, res: Response): Promise<void> => {
logger.info("Hello request", { path: "/hello" }); // AWS X-Ray の情報を含むログ出力
res.json({ message: "Hello, world!" });
});
export const handler = serverlessExpress({ app });
これだと CloudWatch Logs のロググループを別途検索する必要があり、トレースとログの統合的な追跡が難しくなります。
おわりに
AWS Lambda 関数で Winston の出力ログを X-Ray のトレースと紐付ける実装を試してみました。
以上