New Relic APMをECSタスクで使う

株式会社ミクシィが運営するカラオケ動画コミュニティサービス[KARASTA(カラスタ)](https://karasta.net)の開発チームでリーダーをしている@cgetcです。

WEBサービスを運営していると定期的に実行したい処理が出てくると思います。
運営しているサービスでは、DjangoのカスタムコマンドをFargateのECSタスクで定期実行することで実現しています。
New Relic APMを利用し、アプリケーション監視をしたいのですが、ECSタスクで利用するためには工夫が必要だったので要点をまとめました。

# 【追記】　Djangoカスタムコマンドでの監視について
ドキュメントにDjangoカスタムコマンドでの監視についての記述がありました。
https://docs.newrelic.com/install/python/?python-non-web=non-web-yes#django

実際に試してみましたが、ログなども収集されるので、こちらの手順のほうがおすすめです。
トラッキングしたデータをNewRelicに反映されない場合、まずは先の説明にあるとおり、startup_timeoutの値を見直してみるとよいです。


# New Relic APMの非WEBの監視について
ECSタスクはWEBアプリケーションではないため、New Relic APMを使うにはWEB監視ではトラックされません。
公式のドキュメントによると、非WEBの監視にはPythonエージェントAPIを使う必要があり、マニュアルで設定する必要があります。
https://docs.newrelic.com/jp/docs/apm/agents/python-agent/python-agent-api/guide-using-python-agent-api/
https://docs.newrelic.com/install/python/?python-non-web=non-web-yes#review-the-manual-initialization-steps-for-non-web-apps-5

通常であれば、トランザクション開始時に自動的に初期化処理をしてくれます。
しかし、ECSタスクではPythonエージェントが動いておらず、コンテナ起動後にすぐに処理を実行してしまいます。
推測ですが、初期化処理が完了する前に処理が終わってしまっているのだと思われます。
そのため、初期化処理を実行し、アプリケーションを登録する必要があります。

## アプリケーションの登録
初期化処理ではPythonエージェントを別スレッドで起動し、その起動が完了しているかを確認しています。
アプリケーションを監視するにはPythonエージェントが起動してる必要があり、完了するまで待つ必要があります。
その待ち時間がstartup_timeoutという設定値で、待ち時間の最大秒数を指定しています。
この値を適切に設定しないとPythonエージェントを停止してしまうため、トラッキングしたデータをNewRelicに送信しません。
startup_timeoutのデフォルト値は2.5秒と短く設定されていたため、何も設定しない状態では正しくデータが反映されませんでした。
FargateのECSタスクで実行し、デバッグログを確認したところ、たしかにPythonエージェントの起動完了に10秒程度かかっていました。
（実行環境はCPUアーキテクチャがARM64でFargateのタスクサイズは最小構成の`vCPU: 256`　`メモリ: 512MB`）
公式ドキュメントの非WEB監視のサンプルスクリプトでもstartup_timeoutが10秒になっていました。
（これに気づけばよかったのですが、こんなに時間がかるものだと思っていませんでした。）

※ NewRelic Agentはトラックしたデータを一時的に保持するためにも、メモリを消費します。実行する処理でメモリを大量に消費することがある場合、OutOfMemoryが発生する可能性が高くなります。

## トラックする関数
Djangoでは　`app.management.commands`　内にCommandクラスを実装することでカスタムコマンドを作成する事ができます。
カスタムコマンドではCommandクラスのhandleメソッドに実行処理を実装します。
すべてのカスタムコマンドをトラックできるようにするために、django.core.management.base.Commandクラスを継承せず、そのクラスを継承した基底クラスを作成し、そのクラスに必要な処理を実装しました。
具体的には以下のような基底クラスを作成し、継承するようにしました。


```py
import newrelic.agent
from django.core.management.base import BaseCommand as DjangoBaseCommand

newrelic.agent.initialize()　　　# 初期化処理


class BaseCommand(DjangoBaseCommand):
    def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
        super().__init__(stdout=stdout, stderr=stderr, no_color=no_color, force_color=force_color)

        newrelic_application = newrelic.agent.register_application(timeout=30)  # アプリケーションの登録
        self.handle = newrelic.agent.BackgroundTaskWrapper(self.handle, application=newrelic_application)　　　# トラックする関数を指定

    
    def execute(self, *args, **options):
        try:
            return super().execute(*args, **options)
        finally:
            newrelic.agent.shutdown_agent()


```

BackgroundTaskWrapperで関数をBackgroundTaskに変更し、トラックすることができるようになります。
しかし、これだけではコマンドのhandleメソッドは変更した関数にはならないので、self.handleに代入する必要があります。


# 最後に
New Relicのドキュメントはところどころ日本語訳がされておらず、欲しい情報がどこに記載されているのかも分からず苦労しました。

New Relicエージェントはブラックボックスで、そもそもデータがトラックされているのか、データがNew Relicに送信されていないのか、挙動を把握するのにも苦労しました。
New Relicエージェントのデバッグログを有効にして、正しくNew Relicにデータが反映されている環境とそうでない環境のログを見比べ、どこに異常があるのかを把握たりソースコードを読むことで、ようやくドキュメントに書いてあることが理解できました。

New Relic APMの非WEB監視を利用する際に参考になれば幸いです。