Lambdaの実行時間がタイムアウトの8割を超えたらアラートを出す

## はじめに

この記事は、[ミロゴス Advent Calendar 2022](https://qiita.com/advent-calendar/2022/milogos) 1日目の記事です。

社内システムで利用しているAWS Lambda(Node.js)の実行時間がタイムアウト時間の
8割に達した時にアラートを出したいという要件があったため対応しました。

✳︎ 記事はCDKがある程度書かれている前提で記載しています
✳︎ 変数部分は適宜置き換えて下さい

### ログストリームのLambda実行後ログから実行時間を取得してアラートを出す

実装内容は下記です。
実装はTypeScriptで行います。

- CDK
    - 対象ロググループにlambdaの実行ログからDuration値を検知するMetricFilterを作成する
    - 60秒間での合計検知数を見るMetricを作成する
    - 閾値をタイムアウト時間の8割でアラームを定義


##### MetricFilterの定義・LogGroupへの追加

```ts:
const metricFilterProps: logs.MetricFilterOptions = {
     metricNamespace: metricNameSpace,
     metricName: metricName,
     filterPattern: logs.FilterPattern.literal('[type=REPORT, request_id_label, request_id, duration_label, duration, ...]'),
     metricValue: '$duration',
};

logGroup.addMetricFilter('MetricFilter', metricFilterProps);
```

`REPORT`から始まるログはスペース区切りになっているので、下記部分でフィールドを指定してduration値を`$duration`として取得できるようにしています。
[スペース区切りログイベントから値を取得するメトリクスフィルターの使用](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html#extract-log-event-values)

```ts:
filterPattern: logs.FilterPattern.literal('[type=REPORT, request_id_label, request_id, duration_label, duration, ...]')
```

取得した`$duration`はmetric値としてそのまま使います
```ts:
metricValue: '$duration'
```

##### Metric定義

```ts:
const metric = new cloudwatch.Metric({
    namespace: metricNameSpace,
    metricName: metricName,
    period: Duration.seconds(60),
    statistic: cloudwatch.Statistic.SUM,
});
```

####  Alarm定義

```ts:
const alarm = new cloudwatch.Alarm(this, 'Alarm', {
     alarmName: alarmName,
     alarmDescription: alarmName,
     comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
     metric: metric,
     threshold: lambdaTimeout * 0.8,
     evaluationPeriods: 1,
     datapointsToAlarm: 1,
     actionsEnabled: true,
     treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
```

今回は閾値を実行時間の8割としたいので、下記のようにしています。

```ts:
threshold: lambdaTimeout * 0.8,
```

### （別パターン）Lambda側で実行時間を検証する
採用はしませんでしたが、最初Lambda側で実行時間をチェックするような実装をしようと思っていたので、そちらも記載します。

実装内容は下記です。
- Lambda
    - タイムアウトの8割を過ぎたタイミングでcloudwatchにログを吐き出す
- CDK
    - 対象ロググループにlambdaの実行ログから「WARNING elapsed time」を検知するMetricFilterを作成する
    - 60秒間での合計検出数を見るMetricを作成する
    - 閾値1でアラームを定義

#### Lambda
実行時間の取得は`context`オブジェクトの`getRemainingTimeInMillis()`メソッドを利用します。
下記のドキュメントにも書かれていますが、こちらのメソッドは実行がタイムアウトするまでの残り時間をミリ秒で返してくれます。
[Node.js の AWS Lambda context オブジェクト](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-context.html)

##### 実装

```ts:
export const handler = async (event, context) => {
  const timeout = 60000;
  setTimeout(
    () => console.log(`WARNING elapsed time ${timeout - context.getRemainingTimeInMillis()}ms`),
    timeout * 0.8,
  );
```

lambda handler実行後すぐにsetTimeout実行し、無理やり実行時間を見ています。

#### CDK

##### MetricFilterの定義・LogGroupへの追加

```ts:
const metricFilterProps: logs.MetricFilterOptions = {
     metricNamespace: metricNameSpace,
     metricName: metricName,
     filterPattern: logs.FilterPattern.literal('WARNING elapsed time'),
     metricValue: 1,
};

logGroup.addMetricFilter('MetricFilter', metricFilterProps);
```

##### Metric定義

```ts:
const metric = new cloudwatch.Metric({
    namespace: metricNameSpace,
    metricName: metricName,
    period: Duration.seconds(60),
    statistic: cloudwatch.Statistic.SUM,
});
```

####  Alarm定義

```ts:
const alarm = new cloudwatch.Alarm(this, 'Alarm', {
     alarmName: alarmName,
     alarmDescription: alarmName,
     comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
     metric: metric,
     threshold: 1,
     evaluationPeriods: 1,
     datapointsToAlarm: 1,
     actionsEnabled: true,
     treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
```

ただ、自前でログを出さなくても、Lambda実行時はかれるログストリーム上の`REPORT`から始まるログに、`Duration`(実行時間)も記載されているため、上記の実装はやめて下記のようにしました。

### まとめ
以上でlambdaの実行時間によるアラート設定ができました！
Lambdaの実行後ログに色々書いてあるおかげで余計なことせずに済みました。