Laravel × OpenTelemetry × X-Ray で自動計装&アラート設定

はじめに

AWS ECS 上で稼働している 株式会社クレアヴォイアンス のサービス KikitAI に、OpenTelemetry を導入しました。
今回はそのトレース情報を AWS X-Ray で可視化し、さらにアプリ内で発生する 500エラー に対して、閾値を超えた際に通知が届く仕組みを構築していきます。

Laravelアプリケーションの場合はライブラリを入れるだけで自動的に、リクエストとレスポンス、データベース呼び出しなどのトレースデータを送信してくれます。(自動計装)

構成

OpenTelemetryを使って収集したデータは、なんらかのシステムで可視化できるようにします。私達の場合はローカルではjagerを使い、ステージング以上ではXRayを使う方式を取りました。

処理の順序としては

  1. Laravelのライブラリでトレースデータを作成しOTELプロトコルでコレクターに送信
  2. コレクターはデータの可視化ツール(jaegerもしくはXray)にデータを送信

XRayパターンの場合は、ECSでPHPコンテナのあるタスク内にコレクター用のコンテナを配置し(サイドカー)、そこからXRayに送る構成となります。

LaravelのトレースデータをAWSXRayに送る

アプリケーション側

phpコンテナにpeclでopentelemetryをインストールするようにします。

Prisma
pecl install opentelemetry

またphp.iniに以下を追加します。

OTEL_EXPORTER_OTLP_ENDPOINTは、送信先のコレクターとなります。OTEL_TRACES_EXPORTERがOTELプロトコルを使うという意味になります。

ShellScript
extension=opentelemetry.so
...
[otel]
OTEL_PHP_AUTOLOAD_ENABLED="true"
OTEL_SERVICE_NAME={ログのラベルにしたい文字}
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
OTEL_PROPAGATORS=baggage,tracecontext

次にcomposerで必要なライブラリを追加します。

ShellScript
composer require open-telemetry/exporter-otlp \
open-telemetry/opentelemetry-auto-laravel \
open-telemetry/sdk

(ローカルで一度送信できるか試すと安心です。)

XRayにログをputできるロールを作成する

ECSからXRayに送信するための権限が必要となります。以下ポリシーを持つロールを作成してください。

JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSXRayDaemonWriteAccess",
            "Effect": "Allow",
            "Action": [
                "xray:PutTraceSegments",
                "xray:PutTelemetryRecords",
                "xray:GetSamplingRules",
                "xray:GetSamplingTargets",
                "xray:GetSamplingStatisticSummaries"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

AWS ECS側の設定

タスク定義でコレクターのコンテナを追加します。
↓コレクターのコンテナだけ抜粋

JSON
  {
      "name": "aws-otel-collector",
      "image": "public.ecr.aws/aws-observability/aws-otel-collector:v0.43.1",
      "cpu": 0,
      "portMappings": [],
      "essential": true,
      "command": [
        "--config=/etc/ecs/ecs-default-config.yaml"
      ],
      "environment": [],
      "mountPoints": [],
      "volumesFrom": [],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": {ログ名},
          "mode": "non-blocking",
          "awslogs-create-group": "true",
          "max-buffer-size": "25m",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }

コンソールから「モニタリング - オプション」でotel-collectorのサイドカーは作れるんですが、設定ファイルが選べないんですよね…
今回の設定ファイルは以下を使っています。
https://github.com/aws-observability/aws-otel-collector/blob/main/config/ecs/ecs-default-config.yaml

また先ほど作成したロールをタスクロールに割り当ててください。(タスク実行ロールではない)

XRayとCloudWatchで500エラー発生時に通知する

通知までの処理は以下のような順序で処理していきます。

  1. Amazon EventBridge で 5分毎にlambdaを実行するようにする(cron的な)
  2. lambdaではXRayのデータを取得して5分前~現在までで500エラーレスポンスを返した数を数え、CloudWatchのメトリクスとして数を記録します
  3. CloudWathアラームで5分毎にメトリクスの値がしきい値を超えてないか確認し、超えてたらAmazonSNSで通知します

それでは順番に作っていきます。

lambdaに付与するロールを作成する

以下のポリシーを持つロールを作成します。

  • logs:CreateLogStream
  • logs:PutLogEvents
  • xray:GetServiceGraph
  • cloudwatch:PutMetricData

lambdaのコードを書く

pythonでステータスが「障害」(500)となっているレスポンスをカウントし、CloudWatchにメトリクスとして記録するコードを書きました。
(xrayclient.get_service_graph でxrayのデータを取得できるので、この結果を利用して任意のデータをCloudWatchに送信することができるので要件に応じてコードを書いてください)

Python
import os
import boto3
import datetime
import json
from botocore.exceptions import ClientError
from collections import defaultdict

# The region should be the same as your service running in X-Ray.
# If your service runs in multiple regions then you should have multiple instances of this app running
REGION_NAME = os.environ['AWS_REGION']

# php.iniと同じ名前にすること
OTEL_SERVICE_NAME = {ログのラベル名}

# 何分毎に監視をしているか
service_graph_minutes = 5

xrayclient = boto3.client(
    'xray',
    region_name=REGION_NAME
)

cwclient = boto3.client('cloudwatch')

def lambda_handler(event, context):
    #5分前~現在までのXrayのデータを取得しに行く
    service_graph_response = xrayclient.get_service_graph(
        StartTime=datetime.datetime.utcnow() - datetime.timedelta(minutes=service_graph_minutes),
        EndTime=datetime.datetime.utcnow(),
    )

    error_count = 0
    for value_services in service_graph_response['Services']:
        #RootのServiceは直下にSummaryStatisticsキーがあり、そこにresponseの結果件数が入っている
        if value_services['Name'] == OTEL_SERVICE_NAME and 'SummaryStatistics' in value_services:
            error_count += int(value_services['SummaryStatistics']['FaultStatistics']['TotalCount'])

    print('error:' + str(error_count))

    cwclient.put_metric_data(
        Namespace = {任意のメトリクスのNamespace名},
        MetricData = [
            {
                'MetricName': {CloudWatch Metrics に表示される名前}
                'Timestamp': datetime.datetime.utcnow(),
                'Value': error_count,
                'Unit': 'Count'
            },
        ]
    )

1回メトリクスを送ろう

CloudWatchAlarmを作成するのにメトリクス自体が必要です。
'Value': error_count, のところを 'Value': 1 にして一旦実行してください。CloudWatchメトリクスで新しいメトリクスができていてカウントが1になっていれば成功です。

AmazonSNSで通知先を作る

AmazonSNS→トピック→トピックの作成
タイプ: スタンダード
名前: なんの通知かわかるように
で作成
作成したトピック→サブスクリプションの作成
作成したトピックに対してどこに通知するか作成します

CloudWatchAlarmで障害時に通知するようにする

CloudWatch→アラーム→アラームの作成

メトリクスの選択:lambdaで作成されたメトリクスを選択
任意の閾値を指定できますので、要件に応じて設定しましょう。

EventBridgeで定期的にlambdaを実行する

EventBridge→スケジュール→スケジュールの作成
さきほど作成したlambda関数を呼び出すようにすれば完成です!

終わりに

laravelでOpenTelemetryでトレースデータを送信するところはとても簡単に導入できたので驚きました!
次回はトレースデータの種類を編集したいなと思っています。

今回は以下の資料を参考に設定しました。
https://aws.amazon.com/jp/blogs/news/using-amazon-cloudwatch-and-amazon-sns-to-notify-when-aws-x-ray-detects-elevated-levels-of-latency-errors-and-faults-in-your-application/

https://github.com/aws-samples/aws-xray-cloudwatch-event/tree/master

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール