Lambdaのログ出力にオススメしたいNode.jsのライブラリ DAZN Lambda Powertools
CX事業本部@大阪の岩田です。
先日のブログでPythonのライブラリLambda Powertoolsについてご紹介しました。今日はNode.jsのLambda向けに色々な便利機能を提供してくれるDAZN Lambda Powertoolsについてご紹介します。
環境
今回利用した環境です
- ランタイム: Node.js 12.x
- dazn-lambda-powertools-layer: 1.15.6
DAZN Lambda Powertoolsとは?
Node.jsで利用できるAWS Lambda用のミドルウェアを集めたライブラリです。GitHubのリポジトリはこちらです。
https://github.com/getndazn/dazn-lambda-powertools
概要についてはREADMEに貼られている以下の画像を見て頂くのが分かり良いですが、
- 構造化ログ(JSONログ)の出力
- 複数のLambdaを横断的につなげるためのIDの付与、パース
- ログのサンプリング
- タイムアウトのログ出力
といった機能を持っています。
Lambdaを使ってシステムを構築すると、自然とマイクロサービスなアーキテクチャに寄っていきます。マイクロサービスなアーキテクチャでは1つの機能に対して複数のLambdaが起動することが多く、複数のLambdaのログを横断的につなぐために、リクエストIDのような項目をログに残しておかないと、有事の調査が難しくなります。DAZN Lambda Powertoolsを利用すると、複数Lambda間で自動的にx-correlation-id
という属性値を取り回してくれるようになり、開発者はビジネスロジックの実装に集中できるようになります。
やってみる
実際にやってみましょう。DAZN Lambda PowertoolsはSAR(AWS Serverless Application Repository)で公開されているので、まずは以下のリンクからデプロイしてしまいましょう。
デプロイが完了するとNode.js8.10とNode.js 10.xと互換性のあるLambdaレイヤーが作成されます。記事執筆時点では「互換性のあるランタイム」にNode.js 12.xが設定されていませんでしたが、今回試した範囲ではNode.js 12.xでも問題なく正常に動作していました。
ログの出力
これでDAZN Lambda Powertoolsを利用する準備が整いました。簡単にログの出力を試してみましょう。
普通にログを出力する
まずは普通のログ出力からです。以下のコードを実行します。
const Log = require('@dazn/lambda-powertools-logger') exports.handler = async (event) => { Log.info('Hello from Lambda'); // TODO implement const response = { statusCode: 200, body: JSON.stringify('Hello from Lambda!'), }; return response; };
上記のコード実行後にCloudWatch Logsに出力されたログです。
2020-01-06T05:45:16.955Z 44a1424e-051e-4d2c-bfbb-5740a1940b4c INFO { "message": "Hello from Lambda", "awsRegion": "ap-northeast-1", "functionName": "lambda-powertools-nodejs", "functionVersion": "$LATEST", "functionMemorySize": "128", "level": 30, "sLevel": "INFO" }
functionVersion等の項目が自動的に付与されつつ、JSON形式でログが出力されています。
ログに出力する項目を追加する
今度は自分で項目を追加してみましょう。ログ出力メソッドの第2引数にオブジェクトを渡すことで出力項目を追加できます。
Log.info('Hello from Lambda', {attr1: 'val1', attr2: 'val2'});
CloudWatch Logsに出力されたログです。
2020-01-06T05:46:02.771Z efe26c8b-007c-42c3-9a95-09752f81cd60 INFO { "message": "Hello from Lambda", "attr1": "val1", "attr2": "val2", "awsRegion": "ap-northeast-1", "functionName": "lambda-powertools-nodejs", "functionVersion": "$LATEST", "functionMemorySize": "128", "level": 30, "sLevel": "INFO" }
自分で追加したattr1
,attr2
がログに出力されています。
ログのメッセージにJSONを出力する
今度はログのメッセージにJSONを出力してみましょう。普通にログ出力メソッドの引数にオブジェクトを渡してやればOKです。
Log.info(event);
CloudWatch Logsに出力されたログです。
2020-01-06T05:48:01.805Z cbc53e9b-2937-4c7a-8965-36e39716d9ed INFO { "message": { "key1": "value1", "key2": "value2", "key3": "value3" }, "awsRegion": "ap-northeast-1", "functionName": "lambda-powertools-nodejs", "functionVersion": "$LATEST", "functionMemorySize": "128", "level": 30, "sLevel": "INFO" }
Lambdaのテストイベントに設定したkey1
,key2
,key3
がログに出力されています。
例外発生時のログ出力
最後に例外発生時のログも試してみましょう。error
の第2引数にキャッチした例外のオブジェクトを渡してやります。
try { throw new Error('some error'); } catch (e) { Log.error('some error raised', e); }
CloudWatch Logsに出力されたログです。
2020-01-06T05:49:29.250Z b289c0ad-ab23-4da5-9492-9458746efb4a INFO { "message": "some error raised", "errorName": "Error", "errorMessage": "some error", "stackTrace": "Error: some error\n at Runtime.exports.handler (/var/task/index.js:6:12)\n at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)", "awsRegion": "ap-northeast-1", "functionName": "lambda-powertools-nodejs", "functionVersion": "$LATEST", "functionMemorySize": "128", "level": 50, "sLevel": "ERROR" }
スタックトレース等の情報が出力されていることが分かります。
複数のLambdaを横断するログを分析する
続いてDAZN Lambda Powertoolsの特徴でもある複数のLambdaを横断するログの分析を試してみます。こんな感じのシステムをシミュレーションしてみます。
- Lambda1がLambda2を非同期呼び出し
- Lambda2がDynamoDBに書き込み
- DynamoDBストリームからLambda3が起動する
この一連の流れを横断的に分析してみます。
まずLambda1のソースコードです
const Lambda = require('@dazn/lambda-powertools-lambda-client'); const Log = require('@dazn/lambda-powertools-logger'); const wrap = require('@dazn/lambda-powertools-pattern-basic'); exports.handler = wrap(async (event, context) => { Log.info('start lambda 1'); const invokeAsyncReq = { FunctionName: 'dazn-lambda-powertools-2', InvokeArgs: JSON.stringify({ message: 'hello lambda' }) } await Lambda.invokeAsync(invokeAsyncReq).promise() });
ポイントが2つあります。
まずhandlerの処理を@dazn/lambda-powertools-pattern-basic
から読み込んだモジュールでwrapします。
@dazn/lambda-powertools-pattern-basic
というパッケージに「よくある処理」のセットが事前に用意されています。このパッケージからexportされている処理でLambdaのhandlerをwrapすることで、複数のLambda間で情報を受け渡すための諸々の処理が自動で付与されます。
次に普通のAWS SDKのクライアントではなく、@dazn/lambda-powertools-lambda-client
から読み込んだLambdaのクライアントを利用します。
@dazn/lambda-powertools-lambda-client
はAWS SDKのLambdaクライアントのラッパーとして振る舞い、eventデータに複数のLambda間で情報を受け渡すための諸々の情報をセットしてくれます。
続いてLambda2のソースコードです。事前にDynamoDBストリームを有効化した適当なテーブルを用意しておいて下さい。この例ではId
というパーティションキーを持つdazn-lambda-powertools
を事前に作成しています。
const DynamoDB = require('@dazn/lambda-powertools-dynamodb-client'); const Log = require('@dazn/lambda-powertools-logger'); const wrap = require('@dazn/lambda-powertools-pattern-basic') exports.handler = wrap(async (event, context) => { Log.info('start lambda 2'); await DynamoDB.put({ TableName: 'dazn-lambda-powertools', Item: { Id: '1', attr1: 'some val1', attr2: 'some val2', } }).promise(); });
Lambda1と同様に@dazn/lambda-powertools-pattern-basic
から読み込んだ処理でhandlerをwrapしつつ、DynamoDBのクライアントを@dazn/lambda-powertools-dynamodb-client
に置き換えます。
最後にLambda3のソースコードです。Lambda2から書き込んだテーブルのストリームから起動するように設定して下さい。
const Log = require('@dazn/lambda-powertools-logger'); const wrap = require('@dazn/lambda-powertools-pattern-basic') exports.handler = wrap(async (event, context) => { const events = context.parsedDynamoDbEvents; events.forEach(evt => { evt.logger.info('lambda 3 process record'); }); });
Lambda1と同様に@dazn/lambda-powertools-pattern-basic
から読み込んだ処理でhandlerをwrapしつつ、context.parsedDynamoDbEvents
の結果をイベントデータとして利用するのがポイントです。
準備ができたら適当なテストイベントからLambda1を実行します。実行後にDynamoDBのテーブルを確認すると、以下のようなアイテムがPutされていることが確認できます。
{ "__context__": { "awsRequestId": "ad7d8292-d051-4fe0-a55e-f7c7c68a481b", "call-chain-length": 2, "debug-log-enabled": "false", "x-correlation-id": "c6f5143c-297e-4662-8a68-9b38dc64e8d5" }, "attr1": "some val1", "attr2": "some val2", "Id": "1" }
__context__
という属性にDAZN Lambda Powertoolsが利用する諸々の属性が入っていることが分かります。
DynamoDBのアイテムから確認できたx-correlation-id
を指定し、CloudWatch Logs Insightsから以下のクエリを実行してみましょう。
fields @timestamp, `x-correlation-id`, `awsRequestId`, `call-chain-length`,`message` | filter `x-correlation-id` = "<DynamoDBに登録されていたx-correlation-id>" | sort `call-chain-length`
関連する3つのLambdaのログをまとめて抽出できました!!
まとめ
DAZN Lambda Powertoolsの紹介でした。DAZN Lambda Powertoolsは今回紹介していない機能以外にもDataDogとの連携を容易にするための機能や、機密情報をログに出力しないようにマスクする機能など、様々な機能を兼ね備えています。皆さんも一度試してみてはいかがでしょうか?