はじめに
AWS ECS 上で稼働している 株式会社クレアヴォイアンス のサービス KikitAI に、OpenTelemetry を導入しました。
今回はそのトレース情報を AWS X-Ray で可視化し、さらにアプリ内で発生する 500エラー に対して、閾値を超えた際に通知が届く仕組みを構築していきます。
Laravelアプリケーションの場合はライブラリを入れるだけで自動的に、リクエストとレスポンス、データベース呼び出しなどのトレースデータを送信してくれます。(自動計装)
構成
OpenTelemetryを使って収集したデータは、なんらかのシステムで可視化できるようにします。私達の場合はローカルではjagerを使い、ステージング以上ではXRayを使う方式を取りました。
処理の順序としては
- Laravelのライブラリでトレースデータを作成しOTELプロトコルでコレクターに送信
- コレクターはデータの可視化ツール(jaegerもしくはXray)にデータを送信
XRayパターンの場合は、ECSでPHPコンテナのあるタスク内にコレクター用のコンテナを配置し(サイドカー)、そこからXRayに送る構成となります。
LaravelのトレースデータをAWSXRayに送る
アプリケーション側
phpコンテナにpeclでopentelemetryをインストールするようにします。
pecl install opentelemetry
またphp.iniに以下を追加します。
OTEL_EXPORTER_OTLP_ENDPOINTは、送信先のコレクターとなります。OTEL_TRACES_EXPORTERがOTELプロトコルを使うという意味になります。
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で必要なライブラリを追加します。
composer require open-telemetry/exporter-otlp \
open-telemetry/opentelemetry-auto-laravel \
open-telemetry/sdk
(ローカルで一度送信できるか試すと安心です。)
XRayにログをputできるロールを作成する
ECSからXRayに送信するための権限が必要となります。以下ポリシーを持つロールを作成してください。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AWSXRayDaemonWriteAccess",
"Effect": "Allow",
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
"xray:GetSamplingStatisticSummaries"
],
"Resource": [
"*"
]
}
]
}
AWS ECS側の設定
タスク定義でコレクターのコンテナを追加します。
↓コレクターのコンテナだけ抜粋
{
"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エラー発生時に通知する
通知までの処理は以下のような順序で処理していきます。
- Amazon EventBridge で 5分毎にlambdaを実行するようにする(cron的な)
- lambdaではXRayのデータを取得して5分前~現在までで500エラーレスポンスを返した数を数え、CloudWatchのメトリクスとして数を記録します
- CloudWathアラームで5分毎にメトリクスの値がしきい値を超えてないか確認し、超えてたらAmazonSNSで通知します
それでは順番に作っていきます。
lambdaに付与するロールを作成する
以下のポリシーを持つロールを作成します。
- logs:CreateLogStream
- logs:PutLogEvents
- xray:GetServiceGraph
- cloudwatch:PutMetricData
lambdaのコードを書く
pythonでステータスが「障害」(500)となっているレスポンスをカウントし、CloudWatchにメトリクスとして記録するコードを書きました。
(xrayclient.get_service_graph でxrayのデータを取得できるので、この結果を利用して任意のデータをCloudWatchに送信することができるので要件に応じてコードを書いてください)
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://github.com/aws-samples/aws-xray-cloudwatch-event/tree/master