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を利用する準備が整いました。簡単にログの出力を試してみましょう。
普通にログを出力する
まずは普通のログ出力からです。以下のコードを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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に出力されたログです。
1 2 3 4 5 6 7 8 9 10 | 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引数にオブジェクトを渡すことで出力項目を追加できます。
1 | Log.info('Hello from Lambda', {attr1: 'val1', attr2: 'val2'}); |
CloudWatch Logsに出力されたログです。
1 2 3 4 5 6 7 8 9 10 11 12 | 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です。
1 | Log.info(event); |
CloudWatch Logsに出力されたログです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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引数にキャッチした例外のオブジェクトを渡してやります。
1 2 3 4 5 | try { throw new Error( 'some error' ); } catch (e) { Log.error( 'some error raised' , e); } |
CloudWatch Logsに出力されたログです。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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のソースコードです
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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
を事前に作成しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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から書き込んだテーブルのストリームから起動するように設定して下さい。
1 2 3 4 5 6 7 8 9 10 | 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されていることが確認できます。
1 2 3 4 5 6 7 8 9 10 11 | { "__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から以下のクエリを実行してみましょう。
1 2 3 | 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との連携を容易にするための機能や、機密情報をログに出力しないようにマスクする機能など、様々な機能を兼ね備えています。皆さんも一度試してみてはいかがでしょうか?