はじめに
前回はメトリクスを CloudWatch の OpenTelemetry(以下 OTel)で送りました。今回は残りの2つのシグナル、ログとトレースを同じ EC2 から送ります。前回の設定ファイルに足す形で、1本のエージェントから3つとも送る構成にします。宛先が3つに分かれることと、トレースだけ事前設定が必要なことがポイントです。なお、前回の記事は ap-northeast-1 でしたが、今回は us-east-1 で記載しています。
環境
| 役割 | 値 |
|---|---|
| EC2 | Amazon Linux 2023 |
| VPC エンドポイント |
com.amazonaws.us-east-1.monitoring、com.amazonaws.us-east-1.logs、com.amazonaws.us-east-1.xray、com.amazonaws.us-east-1.ec2(Name タグを取得する場合) |
| IAM(EC2 ロール) | CloudWatchAgentServerPolicy |
| ログの取得元 | /var/log/otel-demo/app.log |
| トレースの取得元 | OTLP(OpenTelemetry Protocol)で localhost:4318 へ送ったスパン |
※スパンは処理1区間の記録です。「いつ始まり、いつ終わり、何をしたか」を1件にまとめたもので、これを親子関係でつなげた1本の流れがトレースになります。
仕組み
3つのシグナルと3つの宛先
OTel のシグナルは3種類あり、CloudWatch 側の受け口も3つに分かれています。
| シグナル | エンドポイント | 署名するサービス名 |
|---|---|---|
| メトリクス | https://monitoring.us-east-1.amazonaws.com/v1/metrics |
monitoring |
| ログ | https://logs.us-east-1.amazonaws.com/v1/logs |
logs |
| トレース | https://xray.us-east-1.amazonaws.com/v1/traces |
xray |
設定の流れ
設定の流れを並べると次のようになります。水色が EC2 上または CLI で実施する設定、緑が CloudWatch 側で自動的に動く部分です。
事前準備
① VPC エンドポイントを追加する
プライベートサブネットなので、宛先ごとにインターフェース型の VPC エンドポイントが必要です。メトリクスの monitoring に加えて、logs と xray を作ります。
aws ec2 create-vpc-endpoint --region us-east-1 --vpc-endpoint-type Interface --vpc-id vpc-xxxxxxxx --service-name com.amazonaws.us-east-1.logs --subnet-ids subnet-xxxxxxxx --security-group-ids sg-xxxxxxxx --private-dns-enabled
aws ec2 create-vpc-endpoint --region us-east-1 --vpc-endpoint-type Interface --vpc-id vpc-xxxxxxxx --service-name com.amazonaws.us-east-1.xray --subnet-ids subnet-xxxxxxxx --security-group-ids sg-xxxxxxxx --private-dns-enabled
② ロググループを作る
OTLP のログエンドポイントは既存のロググループへ書き込む仕様なので、宛先は先に作っておきます。あとで YAML のヘッダに指定する /otel/demo を作ります。
aws logs create-log-group --log-group-name /otel/demo --region us-east-1
③ Transaction Search を有効にする
必要なコマンドは2つです。やっているのは「受け取る側の許可」と「送る側の宛先の切り替え」の2つだけで、どちらもアカウント単位の設定なので、EC2 ロールではなく作業者の権限で実行します。
1つ目は CloudWatch Logs 側のリソースポリシーです。スパンをロググループへ書き込むのは X-Ray のサービス(xray.amazonaws.com)なので、aws/spans と /aws/application-signals/data の2つに対して logs:PutLogEvents を許可します。
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) && aws logs put-resource-policy --region us-east-1 --policy-name TransactionSearchXRayAccess --policy-document '{"Version":"2012-10-17","Statement":[{"Sid":"TransactionSearchXRayAccess","Effect":"Allow","Principal":{"Service":"xray.amazonaws.com"},"Action":"logs:PutLogEvents","Resource":["arn:aws:logs:us-east-1:'"$ACCOUNT_ID"':log-group:aws/spans:*","arn:aws:logs:us-east-1:'"$ACCOUNT_ID"':log-group:/aws/application-signals/data:*"],"Condition":{"ArnLike":{"aws:SourceArn":"arn:aws:xray:us-east-1:'"$ACCOUNT_ID"':*"},"StringEquals":{"aws:SourceAccount":"'"$ACCOUNT_ID"'"}}}]}'
2つ目は X-Ray 側の保存先の切り替えです。既定ではスパンは X-Ray が内部に持つトレースストアに入りますが、CloudWatchLogs に切り替えるとロググループへ入るようになります。
aws xray update-trace-segment-destination --destination CloudWatchLogs --region us-east-1
Status が ACTIVE になっていることを確認します。反映まで10分ほどかかります。
aws xray get-trace-segment-destination --region us-east-1
メトリクスとログの設定
① ベースの JSON を用意する
前回と同じ JSON に grpc_endpoint を足したものです。この otlp の受け口が、あとでトレースの入口になります。
sudo tee /tmp/agent.json > /dev/null <<'EOF'
{
"agent": {
"metrics_collection_interval": 60
},
"opentelemetry": {
"collect": {
"otlp": {
"grpc_endpoint": "0.0.0.0:4317",
"http_endpoint": "0.0.0.0:4318"
}
}
}
}
EOF
② YAML を書く
メトリクスとログの2本のパイプラインを1つのファイルにまとめます。前回の YAML に、ログの受け口(filelog)・署名(sigv4auth/cwlogs)・送信先(otlphttp/cwlogs)を足した形です。
sudo tee /tmp/otel.yaml > /dev/null <<'EOF'
receivers:
hostmetrics/cwagent:
collection_interval: 60s
scrapers:
cpu:
metrics:
system.cpu.utilization:
enabled: true
memory:
metrics:
system.memory.utilization:
enabled: true
filesystem:
include_fs_types:
fs_types: [xfs, ext4]
match_type: strict
metrics:
system.filesystem.utilization:
enabled: true
filelog/cwagent:
include: [/var/log/otel-demo/app.log]
start_at: beginning
processors:
resourcedetection/cwagent:
detectors: [env, ec2]
ec2:
tags:
- ^Name$
resource/cwagent:
attributes:
- key: service.name
value: otel-demo
action: upsert
- key: deployment.environment
value: test
action: upsert
batch/cwagent:
send_batch_size: 200
timeout: 10s
extensions:
sigv4auth/cwmetrics:
region: us-east-1
service: monitoring
sigv4auth/cwlogs:
region: us-east-1
service: logs
exporters:
otlphttp/cwmetrics:
metrics_endpoint: https://monitoring.us-east-1.amazonaws.com/v1/metrics
auth:
authenticator: sigv4auth/cwmetrics
otlphttp/cwlogs:
logs_endpoint: https://logs.us-east-1.amazonaws.com/v1/logs
headers:
x-aws-log-group: /otel/demo
x-aws-log-stream: default
auth:
authenticator: sigv4auth/cwlogs
debug/cwagent:
verbosity: detailed
service:
extensions: [sigv4auth/cwmetrics, sigv4auth/cwlogs]
pipelines:
metrics/cwagent:
receivers: [hostmetrics/cwagent]
processors: [resourcedetection/cwagent, resource/cwagent, batch/cwagent]
exporters: [otlphttp/cwmetrics, debug/cwagent]
logs/cwagent:
receivers: [filelog/cwagent]
processors: [resourcedetection/cwagent, resource/cwagent, batch/cwagent]
exporters: [otlphttp/cwlogs, debug/cwagent]
EOF
ログでメトリクスと違うのは3点です。sigv4auth の service が logs になること、otlphttp が metrics_endpoint ではなく logs_endpoint を持つこと、そして headers で宛先のロググループとログストリームを指定することです。
③ 設定を反映する
ベースの JSON で起動してから YAML を append します。前回と同じコマンドです。
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -c file:/tmp/agent.json -s && sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -c file:/tmp/otel.yaml -s
④ ログを書いて届くか確認する
読み取り元のファイルを作り、行を1つ足します。エージェントが読めるようにパーミッションを設定します。
sudo mkdir -p /var/log/otel-demo && sudo touch /var/log/otel-demo/app.log && sudo chmod 755 /var/log/otel-demo && sudo chmod 644 /var/log/otel-demo/app.log
sudo sh -c 'echo "$(date -Is) INFO order accepted id=1001" >> /var/log/otel-demo/app.log'
ロググループに届いたかは CLI で確認できます。
sleep 30 && aws logs tail /otel/demo --since 5m --region us-east-1
CloudWatch コンソールからも確認します。上記で書いた行が body に追加されています。
トレースの設定
① OTLP の受け口を確認する
トレースは YAML を書きません。ベースの JSON で OTLP の受け口(4318)を開けていれば、エージェントが X-Ray のエンドポイントへ送信します。受け口が起動しているかはエージェントのログで確認します。
sudo grep "Starting HTTP server" /opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log | tail -n 5
② スパンを送る
アプリを計装する場合は OTel SDK のエクスポーターを http://localhost:4318 に向けますが、ここでは中身が見えるように OTLP の JSON を直接投げます。親子2つのスパンを作ります。
TRACE_ID=$(openssl rand -hex 16) && ROOT_ID=$(openssl rand -hex 8) && CHILD_ID=$(openssl rand -hex 8) && END=$(date +%s%N) && START=$((END - 500000000)) && CSTART=$((START + 100000000)) && CEND=$((END - 100000000)) && echo "$TRACE_ID"
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:4318/v1/traces -H 'Content-Type: application/json' -d @- <<EOF
{
"resourceSpans": [{
"resource": {
"attributes": [
{"key": "service.name", "value": {"stringValue": "otel-demo"}}
]
},
"scopeSpans": [{
"scope": {"name": "manual"},
"spans": [
{
"traceId": "$TRACE_ID",
"spanId": "$ROOT_ID",
"name": "GET /orders",
"kind": 2,
"startTimeUnixNano": "$START",
"endTimeUnixNano": "$END",
"attributes": [
{"key": "http.request.method", "value": {"stringValue": "GET"}},
{"key": "url.path", "value": {"stringValue": "/orders"}},
{"key": "http.response.status_code", "value": {"intValue": 200}}
],
"status": {"code": 1}
},
{
"traceId": "$TRACE_ID",
"spanId": "$CHILD_ID",
"parentSpanId": "$ROOT_ID",
"name": "SELECT orders",
"kind": 3,
"startTimeUnixNano": "$CSTART",
"endTimeUnixNano": "$CEND",
"attributes": [
{"key": "db.system", "value": {"stringValue": "postgresql"}}
],
"status": {"code": 1}
}
]
}]
}]
}
EOF
200 が返れば、エージェントが受け取って X-Ray のエンドポイントへ転送しています。
③ aws/spans に入ったスパンを見る
スパンは aws/spans に入るので、ここも CLI で確認できます。
sleep 60 && aws logs tail aws/spans --since 10m --region us-east-1
ルートスパンは次のようになっていました(見やすいように改行しています)。
2026-09-25T05:09:05.131000+00:00 default {
"resource": {
"attributes": {
"deployment.environment.name": "aws_ec2:default",
"service.name": "otel-demo",
"cloud.region": "us-east-1",
"host.image.id": "ami-xxxxxxxxxxxx",
"host.type": "t3.small",
"cloud.availability_zone": "us-east-1a",
"cloud.provider": "aws",
"cloud.account.id": "xxxxxxxxxxxx",
"cloud.resource_id": "arn:aws:ec2:us-east-1:xxxxxxxxxxxx:instance/i-xxxxxxxxxxxx",
"host.name": "ip-10-0-xxx-xxx.ec2.internal",
"cloud.platform": "aws_ec2",
"host.id": "i-xxxxxxxxxxxx"
}
},
"scope": {
"name": "manual",
"version": "",
"attributes": {
"cloudwatch.solution": "otel-otlp",
"cloudwatch.source": "cloudwatch-agent"
}
},
"traceId": "c71c228c997fd8357ee53220079399b5",
"spanId": "b1eb4adbdd960d09",
"flags": 0,
"name": "GET /orders",
"kind": "SERVER",
"startTimeUnixNano": 1790312944631642214,
"endTimeUnixNano": 1790312945131642214,
"durationNano": 500000000,
"attributes": {
"aws.local.service": "otel-demo",
"aws.local.operation": "GET /orders",
"http.status_code": 200,
"aws.span.kind": "LOCAL_ROOT",
"url.path": "/orders",
"http.request.method": "GET",
"telemetry.extended": "true",
"PlatformType": "AWS::EC2",
"http.response.status_code": 200,
"aws.local.environment": "aws_ec2:default"
},
"status": {"code": "OK"}
}
子スパンは resource と scope が同じなので、違う部分だけ抜き出します。
"traceId": "c71c228c997fd8357ee53220079399b5",
"spanId": "15a78a482fe11828",
"parentSpanId": "b1eb4adbdd960d09",
"name": "SELECT orders",
"kind": "CLIENT",
"durationNano": 300000000,
"attributes": {
"aws.local.service": "otel-demo",
"aws.local.operation": "UnmappedOperation",
"aws.span.kind": "CLIENT",
"telemetry.extended": "true",
"db.system": "postgresql",
"PlatformType": "AWS::EC2",
"aws.remote.service": "postgresql",
"aws.local.environment": "aws_ec2:default",
"aws.remote.operation": "UnknownRemoteOperation"
},
"status": {"code": "OK"}
子の parentSpanId がルートの spanId と一致していて、2つが1本のトレースとして繋がっています。
④ トランザクション検索で確認する
コンソールからは CloudWatch → Application Signals(APM)→ トランザクション検索で見ます。既定で入っているクエリに traceId の絞り込みを足すと、送ったスパンだけが出ます。
結果は2行になります。
まとめ
メトリクス・ログ・トレースの3つを同じ CloudWatch Agent 1本で送れることが確認できました。宛先とサービス名がシグナルごとに違うだけで、書き方の構造は同じです。






