「DogStatsD」を用いてDatadogでFargateコンテナ内のカスタムメトリクスを収集してみた
はじめに
みなさんこんにちは、クラウド事業本部コンサルティング部の浅野です。
Datadogでホスト、コンテナ内のカスタムメトリクスを収集する方法はいくつか存在しますが、今回はDatadogの機能である「DogStatsD」を用いて、AWS Fargate内のコンテナで稼働するAPIサーバーのカスタムメトリクスを収集してみました。
設定がとても簡潔で、高速にカスタムメトリクスを収集でき、Datadog内でのダッシュボードも簡単に設定できます。
DogStatsDとは
まず、「StatsD」とは、アプリケーションからメトリクス(例: カウント、タイミング、ゲージなど)を収集し、モニタリングツールに送信するためのネットワークデーモンツールです。
簡単に言うと、アプリのパフォーマンスデータを集めて、モニタリングサービス等に渡す前に事前集計や整形処理を行ってくれるデーモンアプリです。
「DogStatsD」は上記「StatsD」をベースにDatadogが機能を拡張したDatadog Agentに付属したメトリクス収集サービスです。今回は「メトリクス」を収集しますが、それ以外にも「イベント」や「サービスチェック」など様々なデータを収集してくれます。
収集するメトリクス
DogStatsDで収集してくれるメトリクスタイプには以下の種類が存在します。
取得するデータの特性と用途を考慮しながら、各タイプごとに用意されているコードを用いて適切な設定を行う必要があります。
メトリクスタイプ | 保存時の型 | 説明 | 主な用途 |
---|---|---|---|
COUNT | RATE | 発生回数をカウントし、1秒あたりの発生率として保存。 | APIリクエスト数、ジョブ実行回数など |
GAUGE | GAUGE | 現在値を記録。最新の値のみ保持。 | CPU使用率、温度、メモリ使用量など |
SET | GAUGE | フラッシュ期間中の一意な値の数をカウント。 | 一意ユーザー数、ユニークID数など |
HISTOGRAM | GAUGE / RATE | 値の分布を記録。平均、中央値、最大、95パーセンタイルなどを自動生成。 | 処理時間、レスポンスタイムの統計分析など |
TIMER | GAUGE / RATE | コードブロックの実行時間を測定(内部的にHISTOGRAMとして扱われる)。 | 関数・APIの処理時間測定 |
DISTRIBUTION | DISTRIBUTION | 値の分布を集約し、パーセンタイル(50, 75, 90, 95, 99)などを算出。 | ファイルサイズやレスポンス時間など、広範な分布分析 |
詳しくは公式ドキュメントを参照してください。
今回はFlask APIをFargateコンテナ内で立ち上げ、以下のメトリクス名と種類を設定してみます。
よくあるAPI監視シチュエーションを想定しています。
# | メトリクス名 | DogStatsD型 | Datadog保存型 | 説明 | 送信タイミング |
---|---|---|---|---|---|
1 | demo_api.requests.count | COUNT | RATE | リクエスト総数 | 全リクエスト |
2 | demo_api.requests.errors | COUNT | RATE | エラー数 | status_code >= 400 の場合のみ |
3 | demo_api.response.time_ms | HISTOGRAM | GAUGE/RATE | レスポンスタイム(ミリ秒) | 全リクエスト |
4 | demo_api.request.size_bytes | DISTRIBUTION | DISTRIBUTION | リクエストサイズ(バイト) | 全リクエスト |
5 | demo_api.response.size_bytes | DISTRIBUTION | DISTRIBUTION | レスポンスサイズ(バイト) | 全リクエスト |
ヒストグラムは一つ設定すると以下の様な複数のメトリクスタイプを収集してくれます。
具体的には「demo_api.response.time_ms (HISTOGRAM) 」から以下の5つが自動生成されます。
生成メトリクス名 | Datadog型 | 説明 |
---|---|---|
demo_api.response.time_ms.avg | GAUGE | 平均レスポンスタイム |
demo_api.response.time_ms.median | GAUGE | 中央値 |
demo_api.response.time_ms.95percentile | GAUGE | 95パーセンタイル値 |
demo_api.response.time_ms.max | GAUGE | 最大値 |
demo_api.response.time_ms.count | RATE | サンプル数(計測回数) |
構成
DogStatsDを用いてカスタムメトリクスを収集するイメージ図は下記の通りです。
1つのタスクの中でメインコンテナ(app)内から「UDP:8125ポート」を経由してPush型でサイドカーコンテナ内の「DogStatsD」にデータポイントを送信します。
サイドカーコンテナ内のDogStatsDが10秒ごとにデータポイントを集計しHTTPSエンドポイント経由でDatadog側に送信してくれます。
ECSサービスやタスクを分離して収集する場合はセキュリティグループの設定等に気をつけましょう。
今回は使用しませんが、UDPではなくUnix ドメインソケットを用いて通信する方法もある様です。
CDKプロジェクト
前回Datadogで「AWS ECS on AWS Fargate」統合を試した記事を掲載しましたが、その時のインフラ・プロジェクト構成は全く同じです。
今回もCDKを用いてインフラリソースから、ECSサービス、タスク定義まで全て一括でデプロイしています。
.
├── README.md
├── app
│ ├── Dockerfile # python-3.12-slimイメージ
│ ├── app.py # アプリケーションコード(FastAPI)
│ └── requirements.txt # ライブラリ定義
├── bin
│ └── demo-fargate-datadog-sidecar.ts # リソース定義
├── cdk.json
├── cdk.out
├── jest.config.js
├── lib
│ └── demo-fargate-datadog-sidecar-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test-endpoints.sh # APIエンドポイントテスト用スクリプト
└── tsconfig.json
/app
python3をコンテナ内にインストールし、ASGIをつかってFast APIサーバーを起動するシンプルなDockerfileです。
FROM python:3.12-slim
# Set working directory
WORKDIR /app
# Copy requirements and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app.py .
# Expose port
EXPOSE 80
# Start application with uvicorn
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]
必要なライブラリを「requirements.txt」に定義しています。
fastapi
uvicorn
datadog
下記「app.py」に記述していることを簡潔に説明します。
-
「optinons」プロパティに環境変数から取得したポート番号等の設定を挿入し、初期化します。今回は「DD_AGENT_HOST」はコンテナ間通信であるため。ローカルホスト(127.0.0.1)経由でやり取りされます。
-
収集するDatadogのメトリクスに設定する共通タグとして「service」&「environment」の統合サービスタグ付けを行っています。メトリクスは「メトリクス名」と「タグ」で一意に特定されます。
-
「ヘルスチェック用」, 「ユーザーの一覧取得」,「エラー出力」を行う3つのエンドポイントを構成しており全てのリクエストを処理する前にメトリクスを収集するミドルウェアを構築しています。
import os
import time
from typing import Optional
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
from datadog import initialize, statsd
import uvicorn
options = {
'statsd_host': os.getenv('DD_AGENT_HOST', '127.0.0.1'),
'statsd_port': int(os.getenv('DD_DOGSTATSD_PORT', '8125'))
}
initialize(**options)
# 共通タグ
COMMON_TAGS = [
f"environment:{os.getenv('DD_ENV', 'demo')}",
f"service:{os.getenv('DD_SERVICE', 'demo-api')}"
]
def track_api_request(
endpoint: str,
method: str,
status_code: int,
response_time: float,
request_size: int,
response_size: int,
extra_tags: Optional[list] = None
):
tags = COMMON_TAGS + [
f"endpoint:{endpoint}",
f"method:{method}",
f"status_code:{status_code}",
f"status_family:{status_code // 100}xx"
]
if extra_tags:
tags.extend(extra_tags)
# 1. COUNT: リクエスト総数
statsd.increment('demo-api.requests.count', tags=tags)
# 2. COUNT: エラー数
if status_code >= 400:
statsd.increment('demo-api.requests.errors', tags=tags)
# 3. HISTOGRAM: レスポンスタイム
statsd.histogram('demo-api.response.time_ms', response_time, tags=tags)
# 4. DISTRIBUTION: リクエストサイズ
statsd.distribution('demo-api.request.size_bytes', request_size, tags=tags)
# 5. DISTRIBUTION: レスポンスサイズ
statsd.distribution('demo-api.response.size_bytes', response_size, tags=tags)
app = FastAPI(
title="Demo Fargate Datadog API",
description="APIエンドポイント監視デモ(DogStatsDメトリクス送信)",
version="1.0.0"
)
@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
# リクエスト開始時刻
start_time = time.time()
# リクエストサイズを取得
request_size = int(request.headers.get("content-length", 0))
try:
# 次のミドルウェアまたはエンドポイント処理を実行
response: Response = await call_next(request)
# レスポンスサイズを取得
response_size = int(response.headers.get("content-length", 0))
# レスポンスタイムを計算(ミリ秒)
response_time = (time.time() - start_time) * 1000
# メトリクス送信
track_api_request(
endpoint=request.url.path,
method=request.method,
status_code=response.status_code,
response_time=response_time,
request_size=request_size,
response_size=response_size
)
return response
except Exception as e:
# エラー発生時もメトリクス送信
response_time = (time.time() - start_time) * 1000
track_api_request(
endpoint=request.url.path,
method=request.method,
status_code=500,
response_time=response_time,
request_size=request_size,
response_size=0
)
# エラーレスポンスを返す
return JSONResponse(
status_code=500,
content={"error": "Internal Server Error", "detail": str(e)}
)
# ヘルスチェック用
@app.get("/")
async def health_check():
return {"status": "healthy"}
# ユーザー一覧取得
@app.get("/api/v1/users")
async def get_users():
time.sleep(0.05)
return {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
{"id": 3, "name": "Charlie"}
]
}
# エラーエンドポイント(エラー率テスト用)
@app.get("/api/v1/error")
async def error_endpoint():
return JSONResponse(
status_code=500,
content={"error": "This is a test error"}
)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=80)
python用「datadog」ライブラリの詳しい使用方法は以下のリポジトリを参照してください。
/lib/demo-fargate-datadog-sidecar-stack.ts
スタック自体は前回の記事と変わっておらず、詳しい説明はそちらにも掲載していますので参考にしてください。
事前にDatadog APIキーを出力してAWS Secrets Managerに保存していることが前提となりますので、環境変数「DD_API_KEY_SECRET_ARN」として保存したキーのARNを設定を忘れない様に注意が必要です!
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
import { Construct } from 'constructs';
import { DatadogECSFargate } from 'datadog-cdk-constructs-v2';
export class DemoFargateDatadogSidecarStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPCの作成
const vpc = new ec2.Vpc(this, 'DemoVpc', {
vpcName: 'demo-fargate-datadog-vpc',
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
});
// VPCにタグを追加
cdk.Tags.of(vpc).add('Name', 'demo-fargate-datadog-vpc');
// ALB用セキュリティグループの作成
const albSecurityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
securityGroupName: 'demo-fargate-datadog-alb-sg',
description: 'Security group for ALB',
vpc: vpc,
allowAllOutbound: true,
});
// インターネットからのHTTPアクセスを許可
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP from internet'
);
// ECSサービス用セキュリティグループの作成
const ecsSecurityGroup = new ec2.SecurityGroup(this, 'ECSSecurityGroup', {
securityGroupName: 'demo-fargate-datadog-ecs-sg',
description: 'Security group for ECS Fargate tasks',
vpc: vpc,
allowAllOutbound: true,
});
// ALBからのHTTPアクセスを許可
ecsSecurityGroup.addIngressRule(
albSecurityGroup,
ec2.Port.tcp(80),
'Allow HTTP from ALB'
);
// ECSクラスターの作成
const cluster = new ecs.Cluster(this, 'DemoCluster', {
clusterName: 'demo-fargate-datadog-cluster',
vpc: vpc,
});
// ALBの作成
const alb = new elbv2.ApplicationLoadBalancer(this, 'DemoALB', {
loadBalancerName: 'demo-fargate-datadog-alb',
vpc: vpc,
internetFacing: true,
vpcSubnets: {
subnetType: ec2.SubnetType.PUBLIC,
},
securityGroup: albSecurityGroup,
});
// ターゲットグループの作成
const targetGroup = new elbv2.ApplicationTargetGroup(this, 'DemoTargetGroup', {
targetGroupName: 'demo-fargate-tg',
vpc: vpc,
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
targetType: elbv2.TargetType.IP,
healthCheck: {
path: '/',
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
},
deregistrationDelay: cdk.Duration.seconds(30),
});
// リスナーの作成
alb.addListener('DemoListener', {
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
defaultTargetGroups: [targetGroup],
});
// CloudWatch Logsロググループの作成
const logGroup = new logs.LogGroup(this, 'DemoLogGroup', {
logGroupName: '/ecs/demo-fargate-datadog',
removalPolicy: cdk.RemovalPolicy.DESTROY,
retention: logs.RetentionDays.ONE_WEEK,
});
// Datadog ECS Fargateコンストラクトの初期化
const datadogECS = new DatadogECSFargate({
apiKeySecretArn: process.env.DD_API_KEY_SECRET_ARN,
site: 'datadoghq.com',
imageVersion: '7-rc-full',
env: 'demo',
service: 'demo-api',
dogstatsd: { // カスタムメトリクス送信に必要
isEnabled: true,
isOriginDetectionEnabled: true,
isSocketEnabled: true,
},
});
// Datadogコンストラクトを使用したタスク定義の作成
const taskDefinition = datadogECS.fargateTaskDefinition(this, 'DemoTaskDef', {
family: 'demo-fargate-datadog-task',
cpu: 512,
memoryLimitMiB: 1024,
});
// メインアプリケーションコンテナ(FastAPI)の追加
taskDefinition.addContainer('app', {
containerName: 'app',
image: ecs.ContainerImage.fromAsset('./app', {
platform: Platform.LINUX_AMD64,
}),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'app',
logGroup: logGroup,
}),
portMappings: [
{
containerPort: 80,
protocol: ecs.Protocol.TCP,
},
],
essential: true,
});
// Fargateサービスの作成
const service = new ecs.FargateService(this, 'DemoService', {
serviceName: 'demo-fargate-datadog-service',
cluster: cluster,
taskDefinition: taskDefinition,
desiredCount: 1,
minHealthyPercent: 100,
maxHealthyPercent: 200,
enableExecuteCommand: true,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
assignPublicIp: false,
securityGroups: [ecsSecurityGroup],
healthCheckGracePeriod: cdk.Duration.seconds(60),
});
// サービスをターゲットグループに登録
service.attachToApplicationTargetGroup(targetGroup);
}
}
Datadogコンストラクトを初期化する際に「dogstatsd」の設定を有効化する必要があります。
設定の詳細はこちらに記載してあります。
datadog-cdk-constructs/src/ecs/fargate/README.md at main · DataDog/datadog-cdk-constructs · GitHub
// Datadog ECS Fargateコンストラクトの初期化
const datadogECS = new DatadogECSFargate({
apiKeySecretArn: process.env.DD_API_KEY_SECRET_ARN,
site: 'datadoghq.com',
imageVersion: '7-rc-full',
env: 'demo',
service: 'demo-api',
dogstatsd: { // カスタムメトリクス送信に必要
isEnabled: true,
isOriginDetectionEnabled: true,
isSocketEnabled: true,
},
});
その他以下のような「DogStatsD」周りの環境変数をにて詳細な設定が可能です。
動作確認
タスク定義
上記CDKスタックをデプロイします。以下のタスク定義が作成され、サービスが起動しました。
エンドポイントにもアクセスすると無事起動が確認できました。
以下が作成されたタスク定義の内容です。メインコンテナ(app)に設定されている環境変数と、サイドカーコンテナ(datadog-agent)に設定されている環境変数の違いを意識して確認してみてください。
{
"compatibilities": ["EC2", "FARGATE"],
"containerDefinitions": [
{
"environment": [
{ "name": "DD_DOGSTATSD_ORIGIN_DETECTION_CLIENT", "value": "true" },
{ "name": "DD_INSTALL_INFO_TOOL_VERSION", "value": "datadog-cdk-constructs" },
{ "name": "DD_ECS_TASK_COLLECTION_ENABLED", "value": "true" },
{ "name": "DD_DOGSTATSD_ORIGIN_DETECTION", "value": "true" },
{ "name": "DD_SITE", "value": "datadoghq.com" },
{ "name": "DD_INSTALL_INFO_INSTALLER_VERSION", "value": "3.2.2" },
{ "name": "ECS_FARGATE", "value": "true" },
{ "name": "DD_DOGSTATSD_TAG_CARDINALITY", "value": "orchestrator" },
{ "name": "DD_SERVICE", "value": "demo-api" },
{ "name": "DD_ENV", "value": "demo" },
{ "name": "DD_INSTALL_INFO_TOOL", "value": "cdk" }
],
"healthCheck": {
"command": ["CMD-SHELL", "/probe.sh"],
"interval": 10,
"retries": 3,
"startPeriod": 60,
"timeout": 5
},
"image": "public.ecr.aws/datadog/agent:7-rc-full",
"mountPoints": [
{
"containerPath": "/var/run/datadog",
"readOnly": false,
"sourceVolume": "dd-sockets"
}
],
"name": "datadog-agent",
"portMappings": [
{ "containerPort": 8126, "hostPort": 8126, "protocol": "tcp" },
{ "containerPort": 8125, "hostPort": 8125, "protocol": "udp" }
],
"secrets": [
{
"name": "DD_API_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:************:secret:demo-datadog-api-key-******"
}
]
},
{
"dockerLabels": {
"com.datadoghq.tags.service": "demo-api",
"com.datadoghq.tags.env": "demo"
},
"environment": [
{ "name": "DD_SERVICE", "value": "demo-api" },
{ "name": "DD_DOGSTATSD_URL", "value": "unix:///var/run/datadog/dsd.socket" },
{ "name": "DD_ENV", "value": "demo" },
{ "name": "DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED", "value": "false" },
{ "name": "DD_TRACE_AGENT_URL", "value": "unix:///var/run/datadog/apm.socket" }
],
"image": "************.dkr.ecr.ap-northeast-1.amazonaws.com/...:************************",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/demo-fargate-datadog",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "app"
}
},
"mountPoints": [
{
"containerPath": "/var/run/datadog",
"readOnly": false,
"sourceVolume": "dd-sockets"
}
],
"name": "app",
"portMappings": [
{ "containerPort": 80, "hostPort": 80, "protocol": "tcp" }
]
}
],
"cpu": "512",
"executionRoleArn": "arn:aws:iam::************:role/DemoFargateDatadogSidecar-DemoTaskDefExecutionRole4-******",
"family": "demo-fargate-datadog-task",
"memory": "1024",
"networkMode": "awsvpc",
"revision": 10,
"status": "ACTIVE",
"taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:************:task-definition/demo-fargate-datadog-task:10",
"taskRoleArn": "arn:aws:iam::************:role/DemoFargateDatadogSidecar-DemoTaskDefTaskRole560BAE-******",
"volumes": [{ "name": "dd-sockets" }]
}
Datadog画面
これでAPIサーバーが起動しましたので、用意したエンドポイントを複数呼び出してからDatadog側の画面を確認してみます。
Datadog画面のサイドバーから「Metrics」>「Summary」を選択します。
検索バーから「demo_api」と検索すると収集したカスタムメトリクス一覧が出てきました。
その内「demo_api.requests.count」を詳しくみてみます。「DogStatsD」経由で取得されていること、データタイプは「Rate」で設定されており、Intervalも10秒になっていることが確認できました。
また「Advanced」設定も確認でき 過去のメトリクスの取り込み オプションを有効化できる様です。
また、一つのメトリクスには「app.py」内で設定した「service」, 「environment」以外にもデフォルトで多数の「Tag」が設定されていました。
ECSサービスはどれで、どのコンテナ内で収集されているか非常に識別しやすいですね。
またリクエストカウント値のボリュームも確認できました。
ダッシュボードの作成
折角なので収集したカスタムメトリクスをダッシュボード化してまとめてみましょう。
サイドバーの「Dashboards」>「New Dashboard」を選択し「demo-fargate-app-custom-metrics-dashboard」という名称をつけて「New Dasoboard」を選択しました。
時系列グラフにするなら「Timeseries」、総数カウントなら「Query Value」などのGraphが選択できます。
リクエスト数の推移を時系列でみたいので「Timeseries」を選択してみましょう。メトリクス値として「demo_api.requests.count」を選択してECSサービスでフィルタリングするために「from」項目に「ecs_service:demo-fargate-datadog-service」を設定します。名前を適宜つけて「Save」します。簡単に可視化できました。
残りのメトリクスも適宜設定してあげます。ダッシュボードの作成UIは非常にわかりやすく、何のドキュメントも見ていないですが、10分程度で以下のカスタムダッシュボードが作成できました。素晴らしい!
最後に
今回はDatadogでFargateコンテナ内のカスタムメトリクスを「DogStatsD」の機能で収集しました。
このように、DogStatsD で定義したカスタムメトリクスを Datadog ダッシュボード上で体系的に可視化することで、API の利用状況・パフォーマンス・安定性を直感的に把握できるようになります。
特に、HISTOGRAM や DISTRIBUTION 型メトリクスを一つ設定することで、単なる平均値では見えないレスポンスのばらつきや傾向を定量的に簡単に追跡できる点が大きな強みだと思いました。
IaC周りのツールも整っており非常に簡単に設定できるので、皆さんもぜひ試してみてください!今回は以上です!