New Relic初心者がNew RelicでLambdaを監視設定してみた。

これは[New Relic Advent Calendar 2019](https://qiita.com/advent-calendar/2019/newrelic)の23日の記事です。

---

New Relic 初心者です。
今回、ちょっとNew Relicを触る機会がありまして、
よく使うAWSのサービスの1つであるAWS Lambdaの監視を試してみました。

--- 

基本的には、
[サーバーレス機能の監視 » AWS Lambda監視 » 始めてみましょう AWS LambdaのNew Relic監視の概要](https://docs.newrelic.co.jp/docs/serverless-function-monitoring/aws-lambda-monitoring/get-started/enable-new-relic-monitoring-aws-lambda)
を実施したものになります。

## 準備
[公式構成図](https://docs.newrelic.co.jp/sites/default/files/thumbnails/image/new-relic-lambda-monitoring-architecture.png)

にある通り、CloudwatchのログをNew Relicに転送するためのLambdaを設定します。
有効化するスクリプトをダウンロードして、実行します。
https://github.com/newrelic/nr-lambda-onboarding/archive/master.zip

ちなみに、日本語ページだと、Python2.6.6が必須要件になってますが、
上記ファイルをダウンロードして実行すると、
Python3が必要と言われて、エラーになります。

こちらに関しては、英語ページの記載が正しいようです。
必須要件は `Python 3.3 or higher`
実行コマンドは `pip3 install newrelic-lambda-cli`

コマン実行の際、アカウントID、リンクするアカウント名、そのアカウントで発行するAPIキーが必要になりますので、
APIキーについては、アカウント設定画面(アカウントのドロップダウン -> Account settings -> Account -> Users and roles.)で発行しておきましょう。

### Lambda準備
Node.jsで書くことが多い(9割ぐらいかな)ので、Serverless FrameworkとNewRelic謹製のLambda Layer追加を実施。

オプション#1：Serverless Frameworkプラグイン（Node.jsおよびPython）を使用してください
オプション#2: 当社のLambda Layer（Node.jsおよびPython）を手動で追加

ちなみに、
英語だと、「Option #3: Manually add our Lambda Layer (Node.js and Python)」 が増えてますね。

[Step 2. Install instrumentation](https://docs.newrelic.com/docs/serverless-function-monitoring/aws-lambda-monitoring/get-started/enable-new-relic-monitoring-aws-lambda#instrument-lambda)

```
Option #1: Use Serverless Framework plugin (Node.js and Python)
Option #2: Add Lambda Layer with our CLI (Node.js and Python)
Option #3: Manually add our Lambda Layer (Node.js and Python)
Option #4: Manually instrument Lambda code (all available languages)
```
自前でLambda Layerを作ってというケースは少ないような気がするので、なくても大丈夫そうです。

両方とも試してみましたが、最終的にはServerless Framworkで、謹製のLambda Layerを追加をする形に落ち着きました。

以下抜粋

```yaml
service: new-relic-test-with-dynamodb

provider:
  name: aws
  runtime: nodejs12.x
  logs:
    restApi: true
  stage: dev
  region: ap-northeast-1

custom:
  defaultStage: dev
  newRelic:
    accountId: 1234567
functions:
  index:
    handler: newrelic-lambda-wrapper.handler
    environment:
      NEW_RELIC_ACCOUNT_ID: ${self:custom.newRelic.accountId}
      NEW_RELIC_LAMBDA_HANDLER: handler.index
    memorySize: 256
    timeout: 25
    layers:
      - arn:aws:lambda:ap-northeast-1:451483290750:layer:NewRelicNodeJS12X:3
    events:
      - http:
          method: any
          path: /api
          integration: lambda-proxy
```

最初ハンドラーの指定の意味がわかってなくて、ちゃんとドキュメント読め事案でした。
Layerで、ログを取得する対象のLambdaをラッパーしてるんですね。ふむふむ。

最後に、
「ステップ3。CloudWatchログを設定し、New Relic Lambdaへのストリーミングを行う」
をやるんですが、忘れて、データ来ないなーと思ったことが数回。すみません。

作ったLambda自体は、DynamoDBへの参照・更新を行う単純なものです。
Node.js v12.xランタイムで実行確認してます。
あ、エラー起きた時にどーなるか知りたかったので、乱数生成して、特定の数値の時に、異常終了するようにしてます。

```javascript
'use strict';
console.log('Loading function...');

const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient({
  region: 'ap-northeast-1'
});
const Crypto = require('crypto');

const successMessage = 'Success!'
const failureMessage = 'Failure...'

module.exports.index = async (event, context) => {
  console.log('Received event:', JSON.stringify(event, null, 2));
  console.log('Received context:', JSON.stringify(context, null, 2));

  console.log('httpMethod :', event.httpMethod);
  let message;
  let results;
  const body = JSON.parse(event.body);

  const randomInt = Math.round(Math.random() * 100, 0);
  if ((randomInt % 5) === 0) {
    return randomInt / randomInt2;
  } else {
    switch (event.httpMethod) {
        case 'DELETE':
          const resDel = await deleteItem(body)
          if (resDel) {
            message = successMessage
          } else {
            message = failureMessage
          }
          break;
        case 'GET':
          const resGet = await getItem()
          if (resGet !== null) {
            results = resGet;
            message = successMessage
          } else {
            message = failureMessage
          }
          break;
        case 'POST':
          console.log('POST body :', body  );
          const resPost = await updateItem(body)
          if (resPost) {
            message = successMessage
          } else {
            message = failureMessage
          }
          break;
        case 'PUT':
          console.log('PUT body :', body  );
          const resPut = await insertItem(body)
          if (resPut) {
            message = successMessage
          } else {
            message = failureMessage
          }
          break;
        default:
          console.log(`Unsupported method "${event.httpMethod}"`);
    }
    const returnObj = {
      message,
      results,
    }
    const response = {
      statusCode: 200,
      body: JSON.stringify(
        returnObj,
        null,
        2
      ),
    };
    return response;
  }
};
async function getItem() {
  const params = {
    TableName: process.env['DYNAMO_DB_TABLE']
  }
  try {
    const result = await dynamo.scan(params).promise();
    console.log(result);
    return result["Items"];
  } catch (error) {
    console.error(error);
    return null;
  }
}
async function deleteItem(body) {
  const params = {
    TableName: process.env['DYNAMO_DB_TABLE'],
    Item: {
      'id' : body.id
    }
  }
  try {
    const result = await dynamo.delete(params).promise();
    console.log('deleteItem Success: ', result);
    return true;
  } catch (error) {
    console.error('deleteItem Error: ', error);
    return false;
  }
}
async function insertItem(body) {
  const params = {
    TableName: process.env['DYNAMO_DB_TABLE'],
    Item: {
      'id' : body.id,
      'name': body.name
    }
  }
  try {
    const result = await dynamo.put(params).promise();
    console.log('insertItem Success: ', result);
    return true;
  } catch (error) {
    console.error('insertItem Error: ', error);
    return false;
  }
}
async function updateItem(body) {
  const params = {
    TableName: process.env['DYNAMO_DB_TABLE'],
    Item: {
      'id' : body.id,
      'name': body.name
    }
  }
  try {
    const result = await dynamo.update(params).promise();
    console.log('updateItem Success: ', result);
    return true;
  } catch (error) {
    console.error('updateItem Error: ', error);
    return false;
  }
}
```

## 実行およびNew Relic上での確認
検証としては、Apache Benchで10並列1000リクエストのリクエストを投げています。画面サンプルはそれを2回投げた結果

https://one.newrelic.com/ でログインして、
Entity Explorer -> Lambda Functions -> 設定したLambda -> Summary を開きます。

2回ほどABでリクエスト投げた結果です。

<img width="1166" alt="NewRelic1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/02209444-f311-ac21-f146-5848adab85c1.png">
<img width="1167" alt="NewRelic2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/ea6e3e20-0089-4f25-26a1-d1bce688f131.png">

そういえば、エラーレートが出てないんですけど、なんでかなーって感じです。
<img width="393" alt="NewRelic3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/5008e5fe-a45e-a719-2ffc-f04ad96f0154.png">

メトリクス上は送られてきている(Summaryの下にあるCloudwatch Metricsで確認できます)ので、
計算式がおかしいか、もっとエラーが多くないとダメなのかもしれません。。。
<img width="1181" alt="NewRelic4" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/c612f4f2-0ebd-c00d-a291-f988c01fbff6.png">
<img width="1183" alt="NewRelic5" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/ce45d557-9876-09ab-ee9e-bd35abae20f6.png">

細かいところまではみれていませんが、所感としては、
1つのLambdaについて、各種情報が数値、グラフ織り交ぜて、見れるのがいいですね。
個人的には、ColdStartが可視化されているのは評価高いです。
Cloudwatch Logsのログストリームをみれば、わかるんですが、
お、こんだけLambda起動したんか！っていうのがビジュアル的にわかるのは嬉しいです。
<img width="391" alt="NewRelic6" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/51bc3bd4-3e8c-e078-5e61-89d159ed0303.png">

そういえば、
「Provisioned Concurrency for Lambda Functions」を設定したLambdaだと、この辺どうなるんでしょうかね。検証できたらします。

## 今後
Lambdaのエラーの内容監視やDynamoDB自体の監視もできるようなんですが、
Infrastructureサブスクリプションの購入が必要なようなので、今度できたらやってみたいですね。
あと、通知って飛ばせるのかな？ってところまでみてないので、機会があればやってみたいところ

## 最後に
検証し始めてから、設定したAWSアカウントのCloudwatchの利用料金が上がっているのは、Datadogでも結構あがっていたので、
監視サービス使う上ではしょうがないでしょうかねと思う今日この頃。

---

初心者なりに頑張ってみましたが、
乱筆乱文のほど、どうぞお許しください。

また、ここ違うんじゃというご指摘お待ちしております。