New Relic APMを使ったNode.jsアプリケーションのオブザーバビリティ



# はじめに

昨今のアプリケーションではモノリスへの回帰、モジュラモノリス化なども話題になることが多いですが、分散システムによって協調して実現されたものが多くアプリケーションのオブザーバビリティという側面で全体を把握する難しさはソフトウェアエンジニアの課題と思います

全部自前で用意しようとするとOpenTelemetryなどでのtrace,metricsを出力するための実装やLog Aggregator、閲覧のためのソフトウェアなど・・・用意するものがたくさんあります。一度用意してしまえばあとは焼きましみたいなものなので横展開するだけですが、まっさらな状態だと骨が折れるものです。そこでなんとか実装を省略して楽にオブザーバビリティを手に入れたいという欲求を叶えるため"New Relic"を使って検証してみることにしました

## New Relic APMの概要と特長

https://docs.newrelic.com/jp/docs/apm/new-relic-apm/getting-started/introduction-apm/

> 当社のAPM(Application Performance Monitoring)は、貴社の全てのアプリケーションとマイクロサービスについて統合されたモニタリングサービスを提供します。最新のスタックの数百もの依存関係から、アプリのシンプルなウェブトランザクションタイムとスループットまで、すべての項目を監視します。事前に構築されたカスタムダッシュボードを通じて、メトリクス、イベント、ログ、トランザクション（MELT）を監視することで、アプリの健全性をリアルタイムで追跡します

利用した瞬間にダッシュボード上でアプリケーションがモニタリング出来る体験がすごく良いです。また、今回は利用していませんがインフラリソースの測定やブラウザモニタリングも提供されています。Pixieとの連携も気になりますね。まさに全方位という感じです。

APMを利用してリクエストからレスポンスまでのパフォーマンスが可視化され、MELTによる問題箇所の把握が容易でパフォーマンス改善が期待出来ます

## Node.jsアプリケーションのセットアップ

New Relicアカウントを登録している前提で進めます

https://docs.newrelic.com/jp/docs/apm/new-relic-apm/getting-started/introduction-apm/#get-started

ここからNode.jsを選択して指示に従いながら進めていきます
今回はDockerを選びます

アプリケーション名を決め

> 1 Step 1:Add 'newrelic' as a dependency to your package.json file

```
"newrelic": "latest"
```
    

> 2 Step 2:In the first line of your app's main module, add:

```
require('newrelic');
```

> 3 Add this ENV line to your Dockerfile

```
ENV NEW_RELIC_NO_CONFIG_FILE=true
```

> 4 Step 4:Set the config options via ENV directives
```
ENV NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=true \
NEW_RELIC_LOG=stdout
# etc.
```

> 5 Build your Docker image

> 6 Add environment variables and run the command

```
docker run -e NEW_RELIC_LICENSE_KEY=<license_key> \
  -e NEW_RELIC_APP_NAME="test-app" \
  your_image_name:latest
```

以上です

![スクリーンショット 2023-09-25 17.28.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10683/299cd8d8-60e0-c884-d0f3-20f8bdc15f85.png)

これだけでダッシュボードからモニタリングが可能になります
![スクリーンショット 2023-09-25 17.58.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10683/a47919cc-9286-2ef1-fd92-0e0d05ae2efd.png)

※一部加工しています

分散トレーシングも記録されています

トレースコンテキストの伝播も自動でしてくれていますし、Prismaのinstrumentationも動いていました

https://docs.newrelic.com/jp/docs/distributed-tracing/concepts/how-new-relic-distributed-tracing-works/#headers

winstonをloggerに使っていたのですがLogs in contextも実現されていてtraceと絡めたログ調査が捗るようになります

https://docs.newrelic.com/jp/docs/logs/logs-context/logs-in-context/

## fluentbitを使ったログ転送

https://newrelic.com/jp/blog/best-practices/patterns-using-logs-in-context

APM Agentを使っていればログも送信してくれるのですが、手元にfirelensでログ送信を実装したECSのアプリケーションがあったのでそこからNew Relicにログも送信するようにしてみたので紹介します

元々はコスト的な問題からFireLensでerrorログはCloudWatchに。ログ全体はS3に保存するようにしていました。

アプリケーションのコンテナはfirelensにログを送信して
```typescript
taskDefinition.addContainer(taskSettings.name, {
  image: ecs.ContainerImage.fromEcrRepository(
    containerRepository,
    taskSettings.tag
  ),
  cpu: taskSettings.cpu,
  memoryLimitMiB: taskSettings.memory,
  logging: ecs.LogDrivers.firelens({}),
  ...
```

Sidecarとして起動したFirelensからS3においたfluentbit用のファイルをロードし実現していました

```typescript
// S3 bucket for Fluentbit config files
const configBucket = new s3.Bucket(this, 
  "ApiFluentbitConfigBucket", {
    autoDeleteObjects: true,
    removalPolicy: cdk.RemovalPolicy.DESTROY,
  });
new s3deployment.BucketDeployment(this, "ApiBucketDeployment", {
  destinationBucket: configBucket,
  sources: [
    s3deployment.Source.asset(
      path.resolve("lib/fluent-bit/fluentbit-config")
    ),
  ],
  retainOnDelete: false,
});
// S3 bucket for logs
const logBucket = new s3.Bucket(this, "ApiLogBucket", {
  autoDeleteObjects: true,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Firehose
const deliveryStream = new firehose.DeliveryStream(
  this,
  "SeriApiDeliveryStream",
  {
    destinations: [
      new destinations.S3Bucket(logBucket, {
        dataOutputPrefix:
          "firehose/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/rand=!{firehose:random-string}",
        errorOutputPrefix:
          "firehoseFailures/!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/!{firehose:error-output-type}",
        compression: destinations.Compression.GZIP,
      }),
    ],
  }
);
taskDefinition.addFirelensLogRouter("FireLensLogRouter", {
  containerName: "fluentbit",
  image: ecs.ContainerImage.fromRegistry(
    "public.ecr.aws/aws-observability/aws-for-fluent-bit:init-2.31.12"
  ),
  firelensConfig: {
    type: ecs.FirelensLogRouterType.FLUENTBIT,
    options: {
      enableECSLogMetadata: true,
    },
  },
  environment: {
  FIREHOSE_DELIVERY_STREAM_NAME: deliveryStream.deliveryStreamName,
  LOG_GROUP_NAME: fargateLogGroup.logGroupName,
    aws_fluent_bit_init_s3_1: `${configBucket.bucketArn}/extra.conf`,
    aws_fluent_bit_init_file_1: "/fluent-bit/parsers/parsers.conf",
  },
  logging: ecs.LogDrivers.awsLogs({
    logGroup: new logs.LogGroup(this, "FluentbitLogGroup"),
    streamPrefix: "fluentbit",
  }),
});

configBucket.grantRead(taskDefinition.taskRole);
deliveryStream.grantPutRecords(taskDefinition.taskRole);
fargateLogGroup.grantWrite(taskDefinition.taskRole);
```

これをFirelensでS3,CloudWatch,New Relicに送信するように変更していきます

New Relicに送信するためのプラグインはこちらです

https://github.com/newrelic/newrelic-fluent-bit-output

Docker Imageは提供されているのですがNew Relic専用という感じでした

https://hub.docker.com/r/newrelic/newrelic-fluentbit-output

aws-for-fluent-bitの方はプラグインを追加で読み込むようにはなっていないように見えます

https://github.com/aws/aws-for-fluent-bit/blob/mainline/init/fluent_bit_init_process.go#L32

そこでaws-observability/aws-for-fluent-bitをbase Imageにしてnewrelic pluginを追加したImageを作成して利用することにしました。fluent_bit_init_processでplugin追加出来たら良かったのですが修正が必要だったので、お試しという感じで割り切ってentrypoint.shやextra.conf雑にファイルCopyしちゃってます。

```Dockerfile
FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:init-latest

# Install necessary tools
RUN yum -y update && \
    yum install -y \
    curl \
    && yum clean all

# Set environment variables for New Relic plugin build
ENV NR_VERSION 1.17.3
ENV NR_SO_URL "https://github.com/newrelic/newrelic-fluent-bit-output/releases/download/v${NR_VERSION}/out_newrelic-linux-amd64-${NR_VERSION}.so"

# Download the New Relic Fluent Bit Output Plugin .so file
RUN echo "xDownloading New Relic Fluent Bit Output Plugin .so file from ${NR_SO_URL}"
RUN curl -L $NR_SO_URL -o /fluent-bit/newrelic.so

# Set Fluent Bit to use the New Relic plugin
# Note: You might want to adjust Fluent Bit's configuration file to set the desired output to New Relic

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

COPY extra.conf /fluent-bit/etc/extra.conf

CMD ["sh", "/entrypoint.sh"]
```

```entrypoint.sh
echo -n "AWS for Fluent Bit Container Image Version "
cat /AWS_FOR_FLUENT_BIT_VERSION
exec /fluent-bit/bin/fluent-bit \
     -e /fluent-bit/firehose.so \
     -e /fluent-bit/cloudwatch.so \
     -e /fluent-bit/kinesis.so \
     -e /fluent-bit/newrelic.so \ # 追加
     -R /fluent-bit/parsers/parsers.conf \
     -c /fluent-bit/etc/extra.conf
```

```extra.conf
[INPUT]
    Name forward
    unix_path /var/run/fluent.sock

[INPUT]
    Name forward
    Listen 127.0.0.1
    Port 24224

# ELBヘルスチェックログ除外
[FILTER]
    Name grep
    Match *-firelens-*
    Exclude log ^(?=.*ELB-HealthChecker\/2\.0).*$

[FILTER]
    Name         parser
    Match        *-firelens-*
    Key_Name     log
    Parser       json
    Preserve_Key false
    Reserve_Data true

[FILTER]
    Name          rewrite_tag
    Match         *-firelens-*
    Rule          $log (emerg|alert|crit|error|warn|EMERG|ALERT|CRIT|ERROR|WARN|\s4\d{2}\s|\s5\d{2}\s) error false
    Emitter_Name  re_emitted

# ELBのヘルスチェック以外の全ログはFirehose経由S3へ
[OUTPUT]
    Name   kinesis_firehose
    Match  *-firelens-*
    region ap-northeast-1
    delivery_stream ${FIREHOSE_DELIVERY_STREAM_NAME}
    time_key        timestamp
    time_key_format %Y-%m-%dT%H:%M:%S.%LZ
    compression     gzip

# ELBのヘルスチェック以外の全ログはNew Relicに送信
[OUTPUT]
    Name newrelic
    Match  *-firelens-*
    apiKey ${licenseKey}

# errorタグをCloudWatch Logsへ
[OUTPUT]
    Name   cloudwatch_logs
    Match  error
    auto_create_group true
    log_group_name ${LOG_GROUP_NAME}
    log_stream_prefix from-fluent-bit-
    region ap-northeast-1
```

あとはlicense keyをSecretsManagerなりで渡すようにします

```typescript
  firelensConfig: {
    type: ecs.FirelensLogRouterType.FLUENTBIT,
    options: {
      enableECSLogMetadata: true,
    },
  },
  secrets: {
    licenseKey: ecs.Secret.fromSecretsManager(secrets),
  },
```

巨大なログを送る場合などこのパターンを使うかもしれません


## さいごに

まずは無料で始められるので手軽に優れたオブザーバビリティを手に入れるにはいい選択なのではないでしょうか。他にも機能が豊富で便利過ぎるのでもっと使ってみようと思います