CloudWatch RUM と X-Ray でフロントエンドからバックエンドAPI・その先の呼び出しまで1本のトレースで追えるようにしてみた

CloudWatch RUM と X-Ray でフロントエンドからバックエンドAPI・その先の呼び出しまで1本のトレースで追えるようにしてみた

CloudWatch RUM と AWS X-Ray を連携させて、ユーザーのリクエストからバックエンドの処理まで一本のトレースで追える仕組みを構築してみました。
2026.07.14

リテールアプリ共創部のるおんです。

Webアプリを本番で運用していると、フロントエンドでユーザーの画面に起きたエラーが、こちらからはまったく見えない という悩みにぶつかります。フロントの console.error はユーザーのブラウザの中で消えていきますし、「画面が真っ白になった」「保存ボタンが効かなかった」という声が来ても、それがフロントのJSエラーなのか、APIが500を返したのか、そもそもネットワークが切れたのか を後から突き合わせる手段がありません。

そこで Amazon CloudWatch RUM を導入することにしました。しかしRUM は「フロントで何が起きたか」を集めてくれますが、それだけだと フロントのイベントとバックエンドのログが別々の島 のままです。本当に知りたいのは「このユーザーのこの操作 が、どのAPIリクエスト になって、バックエンドで何をした結果 失敗したのか」という 1本の線 です。

今回は、CloudWatch RUM に AWS X-Ray 連携 を組み合わせて、フロントエンドのリクエストから、バックエンドAPI(API Gateway → Lambda)、その先の呼び出し(DynamoDB)までを、1本のトレースIDで貫いて追える ようにしてみました。題材は、フロントが React 、バックエンドが API Gateway + Lambda(Hono)+ DynamoDB 、インフラが AWS CDK という、よくある構成です。設定のやり方だけでなく、最後にマネジメントコンソールで「成功したリクエスト」と「失敗したリクエスト」がそれぞれどう見えるか まで、実際の画面で辿ります。

先に結論

フロントエンドでエラーが発生したら、その呼び出しリクエストと内部のログがX-RayのトレースIDで一本につながるようにしました。

スクリーンショット 2026-07-14 22.54.51

スクリーンショット 2026-07-14 23.02.10

  • CloudWatch RUM をCDKで立て、フロントに aws-rum-webライブラリを使用することにより、JSエラー・ページロード・すべてのHTTP通信 が収集できる。cwLogEnabled にしておけば CloudWatch Logs にも流すことが可能。
  • RUM を入れただけでは フロントのイベントとバックエンドのトレースは別々 。1本に繋ぐには X-Ray 連携 が必要。
  • フロント側で enableXRay: trueaddXRayTraceIdHeader(API宛てに限定) にし、APIリクエストに X-Amzn-Trace-Id ヘッダ をつけて送信。これをAPI Gatewayが引き継ぐことで、フロント → API GW → Lambda → DynamoDB が同一トレースID で繋げることが可能。
  • バックエンド側(API Gateway / Lambda)の X-Ray はデフォルトOFF 。CDKで tracingEnabled: trueTracing.ACTIVE を明示して初めてトレースに参加する。
  • マネジメントコンソールからもCLIからもトレースIDで問い合わせることでトレースの最初から最後までを確認できる。

構成を1枚にするとこうです。運び役は X-Amzn-Trace-Id という1つのHTTPヘッダです。

ブラウザ(RUM) ──X-Amzn-Trace-Id──▶ API Gateway ──▶ Lambda ──▶ DynamoDB
   │                                   │            │           │
   └─ JSエラー / HTTP / ページロード     └───────── 全部 同じ トレースID の下にぶら下がる ────────┘

CloudWatch RUM と X-Ray 連携とは

まず登場人物を整理します。

Amazon CloudWatch RUM は、実際のユーザーのブラウザ から、JSエラー・ページの表示速度・HTTP通信といったデータを集めてくれる仕組みです(RUM = Real User Monitoring)。フロントに JavaScript の SDK(aws-rum-web)を入れておくと、ユーザーの画面で起きたことが CloudWatch に送られてきます。

AWS X-Ray は、1つのリクエストが 複数のサービスをまたいで どう流れたかを、トレースID という共通の識別子で繋いで可視化する分散トレーシングの仕組みです。API Gateway や Lambda は、X-Ray を有効にすると自動でこのトレースに参加します。

この2つは、RUM の X-Ray 連携 で繋がります。RUM がフロントのHTTPリクエストに X-Ray のトレースヘッダ(X-Amzn-Trace-Id を載せ、その先のバックエンドが同じトレースIDでトレースを続けてくれます。こうして、フロントのRUMイベントと、バックエンドのX-Rayトレースが、1本の線として繋がります

そして、この繋がりをまとめて見られるのが、CloudWatch の Application Signals(コンソールの「アプリケーションマップ」「トレースマップ」)です。RUM のイベントから、対応する X-Ray トレース、さらにその区間の CloudWatch Logs まで辿れるようになります。「このユーザーのこの操作が、どのAPIになって、バックエンドで何をしたか」を1本で追う、という冒頭の目的が、ここでかないます。

やってみた

1. CDK で RUM(AppMonitor)を立てる

まず、フロントの受け皿になる RUM の AppMonitor を作ります。AppMonitor は「監視したいWebアプリ1つ」を表す RUM のリソース で、そのアプリで起きたデータ(JSエラー・HTTP・セッション・トレース)は、すべてこの1つの AppMonitor に集まります。フロントのSDKは、この AppMonitor のIDを宛先にしてデータを送ります。

ブラウザから RUM へデータを送るには送信の認証が要るので、未認証の Cognito Identity Pool と、PutRumEvents だけを許可したゲストロール をセットで用意します(ブラウザに配るものなので、権限は最小に絞ります)。

infra/lib/construct/rum-construct.ts
import {
  CfnOutput,
  Stack,
  aws_cognito,
  aws_iam,
  aws_rum,
} from "aws-cdk-lib";
import { Construct } from "constructs";

export class RumConstruct extends Construct {
  constructor(scope: Construct, id: string, props: { appDomain: string }) {
    super(scope, id);

    // ブラウザからの送信認証に使う未認証Identity Pool
    const identityPool = new aws_cognito.CfnIdentityPool(this, "RumIdentityPool", {
      identityPoolName: "rum-identity-pool",
      allowUnauthenticatedIdentities: true,
    });

    // 未認証ゲストに割り当てるロール(この後PutRumEventsだけを許可する)
    const guestRole = new aws_iam.Role(this, "RumGuestRole", {
      assumedBy: new aws_iam.FederatedPrincipal(
        "cognito-identity.amazonaws.com",
        {
          StringEquals: {
            "cognito-identity.amazonaws.com:aud": identityPool.ref,
          },
          "ForAnyValue:StringLike": {
            "cognito-identity.amazonaws.com:amr": "unauthenticated",
          },
        },
        "sts:AssumeRoleWithWebIdentity",
      ),
    });

    new aws_cognito.CfnIdentityPoolRoleAttachment(this, "RoleAttachment", {
      identityPoolId: identityPool.ref,
      roles: { unauthenticated: guestRole.roleArn },
    });

    const appMonitor = new aws_rum.CfnAppMonitor(this, "AppMonitor", {
      name: "web-app-monitor",
      domain: props.appDomain, // このドメイン以外からのイベントは受け付けない
      // イベントをCloudWatch Logsへもエクスポートする(Logs Insightsでの検索・突き合わせ用)
      cwLogEnabled: true,
      appMonitorConfiguration: {
        identityPoolId: identityPool.ref,
        guestRoleArn: guestRole.roleArn,
        telemetries: ["errors", "performance", "http"],
        sessionSampleRate: 1,
        allowCookies: false,
        // RUMのクライアント側トレースをX-Rayへ連携する(後述。フロントSDKのenableXRayと対で必要)
        enableXRay: true,
      },
    });

    // ゲストロールにはこのAppMonitorへのPutRumEventsだけを許可する(最小権限)
    guestRole.addToPolicy(
      new aws_iam.PolicyStatement({
        actions: ["rum:PutRumEvents"],
        resources: [
          Stack.of(this).formatArn({
            service: "rum",
            resource: "appmonitor",
            resourceName: appMonitor.name,
          }),
        ],
      }),
    );

    // フロントのconfigに転記する値
    new CfnOutput(this, "AppMonitorId", { value: appMonitor.attrId });
    new CfnOutput(this, "IdentityPoolId", { value: identityPool.ref });
    new CfnOutput(this, "GuestRoleArn", { value: guestRole.roleArn });
  }
}

ポイントは3つです。

  • cwLogEnabled: true にしておくと、RUM のイベントが CloudWatch Logs にもエクスポート されます。RUM コンソールで見るだけでなく、Logs Insights で検索したり、他のログと突き合わせたりできるようになります
  • telemetries: ["errors", "performance", "http"] で、JSエラー・パフォーマンス・すべてのHTTP通信 を収集対象にしています。この http が、あとで効いてきます
  • enableXRay: true … これが AppMonitor 側の X-Ray 連携 のスイッチです。実は enableXRay は「フロントの SDK 側」と「この AppMonitor 側」の 2箇所 にあり、両方 ON にして初めて RUM のセッションと X-Ray トレースがコンソール上で相互に辿れるようになります(詳しくは後述のハマりどころで)

デプロイすると、AppMonitorId などが CfnOutput に出てくるので、これをフロントの設定に転記します。

2. フロントに RUM を仕込む(ここが本題)

フロント(React)側に aws-rum-web を入れて初期化します。

pnpm install aws-rum-web

まず 繋がらない例 から見せます。素直に初期化するとこうなりますが、これだと RUM は「集めるだけ」 になります。

繋がらない例(X-Ray連携なし)
import { AwsRum } from "aws-rum-web";

new AwsRum(APP_MONITOR_ID, "1.0.0", REGION, {
  identityPoolId: IDENTITY_POOL_ID,
  guestRoleArn: GUEST_ROLE_ARN,
  telemetries: ["errors", "performance", "http"],
});

この状態でも、JSエラーもHTTP通信のタイミングも RUM に集まります。ですが、フロントのHTTPイベントと、バックエンドの X-Ray トレースを 結びつけるIDが無い ので、両者は別々の島のままです。「このリクエストがバックエンドのどのトレースに対応するか」が辿れません。

繋ぐために足すのが、enableXRayaddXRayTraceIdHeader の2つ です。

web/src/support/observability/rum.ts
 import { AwsRum, type AwsRumConfig } from "aws-rum-web";

+// API宛てのURLだけにトレースヘッダを付けるための正規表現
+const apiUrlPattern = new RegExp(escapeRegExp(config.apiUrl));

 const rumConfig: AwsRumConfig = {
   identityPoolId: config.rum.identityPoolId,
   guestRoleArn: config.rum.guestRoleArn,
-  telemetries: ["errors", "performance", "http"],
+  telemetries: [
+    "errors",
+    "performance",
+    // httpプラグイン: API宛てのリクエストにのみ X-Amzn-Trace-Id を注入する
+    ["http", { addXRayTraceIdHeader: [apiUrlPattern] }],
+  ],
   sessionSampleRate: 1,
   allowCookies: false,
+  // 送信リクエストにX-Rayトレースを開始し、クライアント側セグメントを記録する
+  enableXRay: true,
 };

 new AwsRum(config.rum.appMonitorId, "1.0.0", config.rum.region, rumConfig);

この2つが、それぞれ別の仕事をしています。

  • enableXRay: true … RUM が、送信するHTTPリクエストについて X-Ray のトレースを開始 し、クライアント側のセグメントを記録します
  • addXRayTraceIdHeader … 実際に X-Amzn-Trace-Id ヘッダをリクエストに注入 して、その先のサーバーにトレースを継続させる ためのものです

そして重要なのが、addXRayTraceIdHeadertrue(全リクエスト)ではなく、API宛てのURLに限定 している点です。RUM の http テレメトリはフロントの すべてのHTTP通信 を対象にするので、addXRayTraceIdHeader: true にすると、Google Tag Manager のような 第三者のドメインへのリクエストにまで トレースヘッダを付けてしまいます。相手のサーバーは X-Amzn-Trace-Id を許可していないので、その第三者リクエストが CORS で弾かれて壊れます 。なので、自分たちのAPIのURLにマッチしたときだけ ヘッダを付けるように絞ります。

3. バックエンド側でトレースを継続する

フロントが X-Amzn-Trace-Id を送っても、受け取るバックエンドがそのトレースを継続する設定 になっていないと繋がりません。API Gateway と Lambda で X-Ray を有効にします。CDK ではこうです。

API Gateway と Lambda のトレース有効化
 // API Gateway: ステージでトレースを有効にする
 new aws_apigateway.RestApi(this, "Default", {
   deployOptions: {
     stageName: "api",
+    tracingEnabled: true, // ← 受け取った X-Amzn-Trace-Id を継続する
   },
 });

 // Lambda: アクティブトレースを有効にする
 new nodejs.NodejsFunction(this, "ApiLambda", {
   // ...
+  tracing: aws_lambda.Tracing.ACTIVE, // ← Lambda / DynamoDB等の下流を自動計測
 });

tracingEnabled: true の API Gateway は、リクエストに X-Amzn-Trace-Id が付いていれば、新しいトレースIDを発行せずにそれを継続 します。Lambda を Tracing.ACTIVE にしておくと、Lambda 自身に加えて、Lambda が呼ぶ下流(DynamoDB や、外部への呼び出し)まで自動でサブセグメントとして計測 してくれます。

ここで重要なのは、この2つは自分で有効にしないと動かない ということです。API Gateway のトレースも Lambda のアクティブトレースも デフォルトはOFF で、このフラグを入れて初めて、各サービスが X-Ray に参加してセグメントを作り、ログにトレースIDを出すようになります。言いかえると、フロント(RUMのSDK設定)・API Gateway(tracingEnabled)・Lambda(Tracing.ACTIVE)の3つそれぞれで有効にして、初めて1本になる ということです。どれか1つでも欠けると、そこで線が途切れてしまいます。

4. CORS に X-Amzn-Trace-Id を足す(見落としがち)

最後に、意外と忘れやすいのがこれです。フロント(例: https://<webのドメイン>)とAPI(例: https://<apiのドメイン>)は 別オリジン なので、X-Amzn-Trace-Id のような 独自ヘッダを付けたリクエストは、ブラウザがプリフライト(OPTIONS)を飛ばして許可を確認 します。API Gateway 側で このヘッダを明示的に許可 していないと、プリフライトで弾かれてリクエストが飛びません。

CDK のCORS設定で、allowHeadersX-Amzn-Trace-Id を足します。

infra/lib/construct/api-construct.ts
 defaultCorsPreflightOptions: {
   allowOrigins: props.allowOrigins,
   allowMethods: aws_apigateway.Cors.ALL_METHODS,
-  allowHeaders: aws_apigateway.Cors.DEFAULT_HEADERS,
+  // X-Amzn-Trace-Id: フロント(RUM)が付与するX-Rayトレースヘッダ。
+  // 別オリジンなので許可しないとプリフライトで弾かれ、トレースが継続されない
+  allowHeaders: [
+    ...aws_apigateway.Cors.DEFAULT_HEADERS,
+    "X-Amzn-Trace-Id",
+  ],
 },

Cors.DEFAULT_HEADERS の中身(Content-Type / Authorization など)には X-Amzn-Trace-Id含まれていません 。なので、明示的に足す必要があります。ここを忘れると、他の設定が全部正しくても トレースが繋がらない ので注意です。

マネジメントコンソールで、フロントからバックまで辿る

設定はこれで揃いました。ここからは 実際にブラウザで操作して 、フロントで起きたことがバックエンドまで1本のトレースで繋がっているかを、マネジメントコンソールで辿って確かめます。実際の運用でやりたいのも「フロントで起きた失敗を起点に、バックエンドの原因まで降りていく」ことなので、ここが今回いちばん「使える」パートです。まず RUM から X-Ray トレースへ渡る入口 を押さえ、そのうえでわざと失敗するエラーを投げるようにして、これがどう見えるかを見ていきます。

わざとエラーを発生させてみる

今回は検証のため、Todo一覧を返すユースケースに、想定外の例外をわざと1行仕込みました 。DynamoDBからの取得を終えた 直後 に投げているのがポイントで、これによってトレースには DynamoDB まで乗った状態で 500 になります。

get-todos-use-case.ts(Todo一覧を返すユースケース)
// TODOを全件取得(DynamoDB)
const todos = await this.#todoRepository.findAllByUserId({ userId, completed });

+ // 検証用: 想定外の例外をわざと投げてHTTP 500を発生させる(本来は無いコード)
+ throw new Error("予期しない内部エラー");

return { todos };

このユースケースが投げた例外は、アプリの共通エラーハンドラが捕まえて HTTP 500 に変換されます。ブラウザには「不明なエラーが発生しました」が出て、裏では GET /todos が 500 になっています。

スクリーンショット 2026-07-14 22.54.51

RUM のセッションから X-Ray トレースへ(入口)

入口は CloudWatch RUM です。AppMonitor を開いて 「HTTP リクエスト」タブ を見ると、HTTP fault rate(5xx)・HTTP error rate(4xx)といったレートが並びます。
オレンジ色のグラフは400系のエラー、赤色のグラフは500系エラー、紫色はネットワークに関するエラーを示しています。
失敗が跳ねた点をクリックすると、その失敗に相関するセッション が出てきます。

スクリーンショット 2026-07-14 21.44.22

スクリーンショット 2026-07-14 21.48.48

「相関があるセッション」の中から特定のセッションを開くと、下の 「セッションパフォーマンスの詳細」 に、そのセッション中に飛んだ全HTTP通信 が並びます。第三者への計測ビーコン(今回のログでは www.google-analytics.com 。GTM経由で読み込まれます)に混じって、自分たちのAPIへのリクエスト(GET /todos)が 500 で 出ています。第三者ビーコンは status=0(クロスオリジンでレスポンスを読めない)で記録されるので、ステータス付きの GET /todos だけ を目で拾えます。

スクリーンショット 2026-07-14 21.49.43

そして、この失敗した GET /todos(500)の行をそのままクリックすると、その1リクエストのX-Rayトレースへ直接飛べます 。このトレースを開くと、 クライアント → API Gateway → Lambda → DynamoDB のフル1本が出ます 。リクエストはバックエンドに届いているので、経路は最後まで伸びます。
また、エラーを検知しているRUMとAPI Gateway のセグメントが赤くマークされています。

スクリーンショット 2026-07-14 22.25.37

また、セグメントのタイムラインを見ると、どのリソースでどのタイミングにエラーが起きているかわかります。
今回は Lambda の中で例外が起きているので Lambda も赤くなりそうに思えますが、そうはなりません。アプリが例外を捕まえて 500 のレスポンスを"正常に返した"ため、Lambda 自体の処理は成功(OK・緑)と識別されます 。赤(障害)になるのは、その 500 を最終的にクライアントへ返した API Gateway と、それを受け取ったクライアント側(RUM)だけで、Lambda・DynamoDB は緑のままです。

500トレースの各セグメント
AWS::RUM::AppMonitor       dev-WebAppMonitor         fault=True
AWS::ApiGateway::Stage     <APIのドメイン>/          fault=True   status=500
AWS::Lambda                dev-ApiLambda             fault=None   status=200
AWS::DynamoDB::Table       DynamoDB(認証の引き当て) fault=None   status=200
AWS::DynamoDB::Table       DynamoDB(一覧取得)       fault=None   status=200

ここに、成功と失敗を見分けるポイント が出ています。

  • クライアント(RUM AppMonitor)も fault=True … トレースの起点にあるこのノードは、ブラウザ(実ユーザー)側から見た結果 です。フロントが受け取ったのは5xxなので、起点のクライアントも赤(fault)。「バックエンドの500が、実際にユーザーの画面まで届いた失敗だった」 ことが、この起点で分かります
  • API Gateway が fault=True / status=500 … X-Ray は 5xx を fault4xx を error として区別します(成功は無印)。トレースマップや一覧の はこの fault です
  • Lambda 自体は status=200 … 「Lambda がエラーを捕捉して 500 のレスポンスを"正常に"返した」ことを表します。アプリの共通エラーハンドラが効いていて、Lambdaランタイムとしてはクラッシュしていない ので、関数のセグメントはOK扱いです
  • DynamoDB は2回とも 200 … 例外を投げる前に、認証の引き当てと一覧取得までは成功しています

ここで大事なのは、トレースは「API境界で500だった=どこで落ちたか」までは教えてくれますが、「なぜ落ちたか」まではセグメントに出ない ことです。
具体的なログを追いたい場合は、同じ画面の下の 「ログ」欄 に出ている Lambda の構造化ログ で分かります。

スクリーンショット 2026-07-14 22.16.16

エラーの行がこれです。

500の原因が分かるログ行(抜粋)
{
  "level": "ERROR",
  "message": "予期せぬエラー",
  "xray_trace_id": "1-6a561590-xxxxxxxxxxxxxxxxxxxxxxxx",
  "data": {
    "name": "Error",
    "location": "/server/src/use-case/get-todos-use-case.ts:48",
    "message": "予期しない内部エラー"
  }
}

GET /todos が 500 になった原因は、get-todos-use-case.ts の48行目で投げられた想定外の例外 だということがわかります。

ここまでで、トレースで「どこで(API境界の500)」、相関ログで「なぜ(どのコード行の例外か)」まで、同じ画面から降りてこられました

CLIで一気通貫に引き当てる(調査の自動化)

ここまではマネジメントコンソールで辿りましたが、同じことをAWS CLIでもできます 。コンソールをポチポチする代わりにコマンドで引けると、調査を手順として残せますし、AIエージェントに「この構成はこう調べて」と渡せます

ここで効いてくるのが、最初に入れた cwLogEnabled です。RUMのデータは、これが無いと RUMコンソールのUIの中にしか無く、コマンドから触れません 。AIエージェントは画面をクリックできませんが、cwLogEnabled でRUMのデータを CloudWatch Logs に流しておけば、バックエンドのログと同じように aws logs コマンドで検索できます 。つまり「フロントの観測データを、UIではなくコマンドの届く場所に出しておく」ことが、調査を自動化・エージェント化するための下準備 になっています。

実際に以下のようなロググループが作成されています。

スクリーンショット 2026-07-14 22.29.04

題材は、さっきの 500(想定外の例外) をそのまま使います。調査は RUM → X-Ray → CloudWatch Logs の3ステップ。すべて同じトレースIDで繋がります

① RUMのログから「どのページ・どの操作・何のステータス」と trace_id を引く

cwLogEnabled で RUM のイベントは CloudWatch Logs にも流れているので、Logs Insights で「失敗したHTTPイベント」を引きます。

aws logs start-query --region ap-northeast-1 \
  --log-group-name /aws/vendedlogs/RUMService_<AppMonitor> \
  --start-time <開始epoch> --end-time <終了epoch> \
  --query-string 'fields @message
    | filter @message like /todos/ and @message like /500/
    | sort @timestamp desc | limit 1'
# → 返ってきた queryId で aws logs get-query-results --query-id <queryId>

返ってきた RUM の http イベント(抜粋・整形)がこちらです。

RUMのhttp_event(抜粋)
{
  "event_type": "com.amazon.rum.http_event",
  "metadata": { "pageId": "/", "domain": "<webのドメイン>", "browserName": "Google Chrome" },
  "user_details": { "sessionId": "<セッションID>" },
  "event_details": {
    "request": { "url": "https://<APIのドメイン>/todos", "method": "GET" },
    "response": { "status": 500 },
    "trace_id": "1-6a561590-xxxxxxxxxxxxxxxxxxxxxxxx"
  }
}

この1件から、どのページ(/)で・どのAPI操作(GET /todos)が・何のステータス(500)になったか 、そして 次のステップの鍵になる trace_id が全部取れます。

② その trace_id で X-Ray の全体像を引く(どこで落ちたか)

aws xray batch-get-traces --region ap-northeast-1 \
  --trace-ids "1-6a561590-xxxxxxxxxxxxxxxxxxxxxxxx" \
  --query 'Traces[0].Segments[].Document'

セグメントを整理すると、こうなっていました(コンソールで見たのと同じ内容です)。

トレースの各セグメント
AWS::RUM::AppMonitor       dev-WebAppMonitor         fault=True
AWS::ApiGateway::Stage     <APIのドメイン>/          fault=True   status=500
AWS::Lambda                dev-ApiLambda             fault=None   status=200
AWS::DynamoDB::Table       DynamoDB(認証の引き当て) fault=None   status=200
AWS::DynamoDB::Table       DynamoDB(一覧取得)       fault=None   status=200

バックエンドのセグメントが最後まで並び(届いている)、API Gateway が 500 で fault マーク 、一方 Lambda 自体は status=200 ── アプリのエラーハンドリングが 500 を"正常に"返しているのが読めます。ただし 「なぜ500か」まではセグメントに出ない ので、次のログで確定させます。

③ その trace_id で Lambda のエラーログを引く(何が・どこで投げられたか)

Lambda のロググループに対して、同じ trace_id で Logs Insights を投げます。

aws logs start-query --region ap-northeast-1 \
  --log-group-name /aws/lambda/dev-ApiLambda \
  --start-time <開始epoch> --end-time <終了epoch> \
  --query-string 'fields @timestamp, @message
    | filter xray_trace_id = "1-6a561590-xxxxxxxxxxxxxxxxxxxxxxxx"
    | sort @timestamp asc'

すると、そのリクエストのアプリログが時系列で並び、エラーの行にクラス名・投げられたファイル:行・メッセージ が入っています。

エラーのログ行(抜粋)
{
  "level": "ERROR",
  "message": "予期せぬエラー",
  "xray_trace_id": "1-6a561590-xxxxxxxxxxxxxxxxxxxxxxxx",
  "data": {
    "name": "Error",
    "location": "/server/src/use-case/get-todos-use-case.ts:48",
    "message": "予期しない内部エラー"
  }
}

GET /todos が 500 になった原因は、get-todos-use-case.ts の48行目で投げられた想定外の例外 という同じ結論に辿り着けました。

ここまでで、コンソールで辿った「RUM → X-Ray → ログ」の流れが、そっくりそのまま3つのコマンドになりました。入口の trace_id さえ取れれば、あとはそれを鍵に X-Ray とログを引くだけ です。

CLIで引けるということは、この調査手順をそのまま自動化・エージェント化できる ということです。「RUMで失敗を見つけたら、その trace_id で X-Ray とログを引く」を定型化しておけば、フロントの異常から原因のコード行まで、人が画面を辿らなくても降りていける ようになります。ここで cwLogEnabled が効いていて、画面をクリックできないAIエージェントでも、RUMのデータにコマンドで触れる のがポイントです。

おわりに

今回は、CloudWatch RUM に X-Ray 連携を組み合わせて、フロントエンドのリクエストから、バックエンドAPI・その先の呼び出しまでを1本のトレースIDで追える ようにしてみました。「フロントのエラーが本番で見えない」「エラーの声は来るけど原因の切り分けができない」という課題を持っている方にとって、RUM と X-Ray の組み合わせは強力な選択肢だと思います。特に、フロントとバックのログを別々に眺めるのをやめて、1本のトレースに繋ぐ ところまでやると、調査の体験が大きく変わります。同じようにフロントの可観測性で悩んでいる方の参考になれば幸いです。

以上、どなたかの参考になれば幸いです。

参考

https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM.html

https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html

https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Application-Monitoring-Sections.html

https://github.com/aws-observability/aws-rum-web

https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM-get-started.html

この記事をシェアする

関連記事