はじめに
昨今のアプリケーションではモノリスへの回帰、モジュラモノリス化なども話題になることが多いですが、分散システムによって協調して実現されたものが多くアプリケーションのオブザーバビリティという側面で全体を把握する難しさはソフトウェアエンジニアの課題と思います
全部自前で用意しようとするとOpenTelemetryなどでのtrace,metricsを出力するための実装やLog Aggregator、閲覧のためのソフトウェアなど・・・用意するものがたくさんあります。一度用意してしまえばあとは焼きましみたいなものなので横展開するだけですが、まっさらな状態だと骨が折れるものです。そこでなんとか実装を省略して楽にオブザーバビリティを手に入れたいという欲求を叶えるため"New Relic"を使って検証してみることにしました
New Relic APMの概要と特長
当社のAPM(Application Performance Monitoring)は、貴社の全てのアプリケーションとマイクロサービスについて統合されたモニタリングサービスを提供します。最新のスタックの数百もの依存関係から、アプリのシンプルなウェブトランザクションタイムとスループットまで、すべての項目を監視します。事前に構築されたカスタムダッシュボードを通じて、メトリクス、イベント、ログ、トランザクション(MELT)を監視することで、アプリの健全性をリアルタイムで追跡します
利用した瞬間にダッシュボード上でアプリケーションがモニタリング出来る体験がすごく良いです。また、今回は利用していませんがインフラリソースの測定やブラウザモニタリングも提供されています。Pixieとの連携も気になりますね。まさに全方位という感じです。
APMを利用してリクエストからレスポンスまでのパフォーマンスが可視化され、MELTによる問題箇所の把握が容易でパフォーマンス改善が期待出来ます
Node.jsアプリケーションのセットアップ
New Relicアカウントを登録している前提で進めます
ここから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
以上です
※一部加工しています
分散トレーシングも記録されています
トレースコンテキストの伝播も自動でしてくれていますし、Prismaのinstrumentationも動いていました
winstonをloggerに使っていたのですがLogs in contextも実現されていてtraceと絡めたログ調査が捗るようになります
fluentbitを使ったログ転送
APM Agentを使っていればログも送信してくれるのですが、手元にfirelensでログ送信を実装したECSのアプリケーションがあったのでそこからNew Relicにログも送信するようにしてみたので紹介します
元々はコスト的な問題からFireLensでerrorログはCloudWatchに。ログ全体はS3に保存するようにしていました。
アプリケーションのコンテナはfirelensにログを送信して
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用のファイルをロードし実現していました
// 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に送信するためのプラグインはこちらです
Docker Imageは提供されているのですがNew Relic専用という感じでした
aws-for-fluent-bitの方はプラグインを追加で読み込むようにはなっていないように見えます
そこでaws-observability/aws-for-fluent-bitをbase Imageにしてnewrelic pluginを追加したImageを作成して利用することにしました。fluent_bit_init_processでplugin追加出来たら良かったのですが修正が必要だったので、お試しという感じで割り切ってentrypoint.shやextra.conf雑にファイルCopyしちゃってます。
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"]
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
[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なりで渡すようにします
firelensConfig: {
type: ecs.FirelensLogRouterType.FLUENTBIT,
options: {
enableECSLogMetadata: true,
},
},
secrets: {
licenseKey: ecs.Secret.fromSecretsManager(secrets),
},
巨大なログを送る場合などこのパターンを使うかもしれません
さいごに
まずは無料で始められるので手軽に優れたオブザーバビリティを手に入れるにはいい選択なのではないでしょうか。他にも機能が豊富で便利過ぎるのでもっと使ってみようと思います