Serverless Operations, inc

>_cd /blog/id_s5xdjsct2

title

AWS Lambda Powertools (TypeScript) を使って、Context 情報を含めた構造化ログにカスタマイズする方法

summary

AWS Lambda Powertools を利用することで、AWS Lambdaの実装がとても便利になりました。Python, TypeScript, Java など様々な言語で利用できるようになっており、ロガー、トレーサー、メトリクスの機能を軸として言語ごとに様々な機能が提供されています。この記事では、AWS Lambda Powertools (TypeScript) を入れてまずやりたくなるロガーのカスタマイズ方法についてご紹介します。

AWS Lambda Powertools とは?

AWS Lambda Powertools は AWS サーバーレスアプリケーション開発者向けに作成された OSS ユーティリティライブラリです。AWS Lambda を使って簡単な運用ツールや設定から本格的な WebAPI やデータ分析までできいるようになっているこの頃ですが、ロガー、トレーシング、またカスタムメトリクスを簡単に CloudWatch に送信できる機能など、従来は様々な汎用ライブラリを組み合わせてチューニングして利用してきた部分が簡単に実装ようになってきました。

Python の場合、API ルーティングを含む様々なアノテーションが用意されているなど高機能で、TypeScript や Java 版も同等ではないものの基本的な機能は揃っており、まだ触っていない方は一度は触れてみることをおすすめします。

この記事では、AWS Lambda Powertools (TypeScript) を入れてまずやっておきたくなる、ロガーの設定に関してご紹介します。

構造化ログのメリット

AWS Lambda Powertools を入れる目的の一つで、ロガーを利用できる点です。昨今多くのアプリケーションのログは構造化ログといい、JSON 形式のログを出すことが多くなっています。ここに、AWS Lambda の場合ですと実行コンテキストごとに requestId など様々な情報を包含しているため、CloudWatch Logs から { $.level = "ERROR" } または { $.requestId = "xxx" } と検索することで、ある項目の文字列に絞り込んだ検索や、requestid を指定していわゆる Lambdalith な構成でも該当リクエストのログのみ絞り込んでログを確認することが可能になります。

Node.js ランタイムですと pino, bunyan, winston, lambda-log など様々なロガーのライブラリがあり、それぞれ JSON 形式でログを出力することは可能ですが、それなりに凝った設定を行う必要があったかと思います。AWS Lambda Powertools はロガーのライブラリと構造化ログの設定がすでに含まれているため、ロガーのインスタンスを生成するだけで構造化ログとして出力されます。

デフォルトのログでも良いけど、少しカスタマイズしたい

アプリケーション開発者は様々な理由でログのフォーマットを工夫しカスタマイズしたくなります。システムの用件やトラブルシューティング、また独自のデータ分析や問題発生時のトレースとトラブルシューティングなどに役立てるためです。

まず、AWS Lambda Powertools はデフォルトで以下のようなログを出してくれます。

​{
    "cold_start": true,
    "function_arn": "arn:aws:lambda:eu-west-1:123456789012:function:shopping-cart-api-lambda-prod-eu-west-1",
    "function_memory_size": 128,
    "function_request_id": "c6af9ac6-7b61-11e6-9a41-93e812345678",
    "function_name": "shopping-cart-api-lambda-prod-eu-west-1",
    "level": "INFO",
    "message": "This is an INFO log with some context",
    "service": "serverlessAirline",
    "timestamp": "2021-12-12T21:21:08.921Z",
    "xray_trace_id": "abcdef123456abcdef123456abcdef123456"
}​

これを、たとえば以下のような形に変更するには、どのようにすれば良いでしょうか。

CustomLogFormatter クラスを作成し、ロガー作成時に渡すだけ

結論としては LogFormatter を継承した CustomLogFormatter クラスを作成し、Logger 作成時にコンストラクタに渡す形で実現できるようになります。具体的には、以下のようなファイルとソースコードを作成しておきます。

まずは、カスタムログのフォーマットを作成しておきます。

export type CustomLogFormat = {
  level: string
  name: string
  msg: string
  time: string
  correlationIds?: {
    awsRequestId?: string
    xRayTraceId?: string
    appTraceId?: string
    appUserId?: string
  },
  lambda?: {
    name?: string
    memory?: string
    isColdStart?: boolean
  }
}

次に、LogFormatter を継承した CustomLogFormatter クラスを作成します。フォーマットのパーツに合わせて項目のマッピングを書いていきます。

import { LogFormatter, LogItem } from '@aws-lambda-powertools/logger'
import { LogAttributes, UnformattedAttributes } from '@aws-lambda-powertools/logger/types'
import { CustomLogFormat } from './custom-log-format'

class CustomLogFormatter extends LogFormatter {

  public formatAttributes(attributes: UnformattedAttributes, additionalLogAttributes: LogAttributes): LogItem {

    const baseAttributes: CustomLogFormat = {
      ...this.getEssentialProps(attributes),
      correlationIds : this.getCorrelationIds(attributes),
      lambda         : this.getLambdaProps(attributes)
    }

    const logItem = new LogItem({ attributes: baseAttributes })
    logItem.addAttributes(additionalLogAttributes) // add any attributes not explicitly defined

    return logItem
  }

  public getEssentialProps(attributes: UnformattedAttributes) {
    return {
      time  : this.formatTimestamp(attributes.timestamp), // You can extend this function
      name  : attributes.serviceName,
      msg   : attributes.message,
      level : attributes.logLevel
    }
  }

  public getCorrelationIds(attributes: UnformattedAttributes) {
    const awsRequestId = attributes.lambdaContext?.awsRequestId
    const xRayTraceId  = attributes.xRayTraceId
    const appTraceId   = "..."
    const appUserId    = "..."

    const isAllPropsUndefined = !(awsRequestId || xRayTraceId || appTraceId || appUserId)
    if (isAllPropsUndefined) {
      return undefined
    }

    return { awsRequestId, xRayTraceId, appTraceId, appUserId }
  }

  public getLambdaProps(attributes: UnformattedAttributes) {
    const name        = attributes.lambdaContext?.functionName
    const memory      = attributes.lambdaContext?.memoryLimitInMB
    const isColdStart = attributes.lambdaContext?.coldStart

    const isAllPropsUndefined = !(name || memory)
    if (isAllPropsUndefined) {
      return undefined
    }

    return { name, memory, isColdStart }
  }
}

export { CustomLogFormatter }

カスタムフォーマットを反映したロガーの作成

やり方はシンプルで、以下のように logFormatter に CustomLogFormatter のインスタンスを指定する形でOKです。 ただし、Lambda Context の情報をログに出すには、handler 関数の第2引数である context をロガーに渡さなければいけません。やり方はいろいろあると思いますので、適宜ロガー作成時に context 情報を渡せるような仕組みを考えておくと良いです。

import { Logger as PowertoolsLogger } from '@aws-lambda-powertools/logger'
import { CustomLogFormatter } from './logger'

const logger = new PowertoolsLogger({
  serviceName: name,
  logFormatter: new CustomLogFormatter(), // ここ!
  logLevel: process.env.LOG_LEVEL // "INFO", "DEBUG", 'ERROR", ...
})

const lambdaContext = context // Lambda Handler 関数の第二に引数から取得した context を指定
logger.addContext(lambdaContext || {
  callbackWaitsForEmptyEventLoop: true
})

export logger

Lambda をデプロイして CloudWatch から確認してみると、以下のようになります。加工次第で様々な形に編集できるので、自由度高く扱えます。

いかがだったでしょうか。この記事に関する内容、または AWSモダンアプリケーション全般に関して気になる点やサポートが必要な方は、お気軽にお問い合わせください。

Written by
COO

金 仙優

Sonu Kim

  • Facebook->
  • X->
  • GitHub->

Share

Facebook->X->
Back
to list
<-