X-Ray SDK for Node.jsを使って「ダウンストリームHTTP呼び出し」のトレースを手動で行う

2023.08.31

こんにちは、CX事業本部Delivery部サーバーサイドチームのmorimorikochanです。

最近、Node.jsでX-Rayを利用する機会があったのですが、ダウンストリームHTTP呼び出しがドキュメント通りに利用できなかった場面があったので、その時の解決方法について共有します。

先に結論

X-Ray SDK for Node.jsが用意しているcaptureHTTPsGlobal()を利用しなくても、addAttribute()を利用して手動でダウンストリームHTTP呼び出しと同様のセグメントを生成することができます

// 何らかの方法でセグメントを作成
// const subSegment = getSegment()?.addNewSubsegment("subsegmentName");
subSegment?.addAttribute('http', {
  request: {
    method: 'GET',
    url: 'https://classmethod.jp/',
    traced: true,
  },
  response: {
    content_length: -1,
    status: 200,
  },
});
subSegment?.addAttribute('namespace', 'remote');

背景や問題

とある案件で、X-Ray SDK for Node.jsを利用しているLambda関数を作成していました。 このLambda関数はさらに別の外部APIをいくつか呼び出しているのですが、これらのトレース情報をX-Rayに反映させたいと考えていました。 X-Rayに反映させることで、以下画像のように、X-Rayのサービスマップで直感的に外部APIの状態がわかりますし、セグメントのタイムラインから簡単にリクエストの情報が閲覧できるようになります。

X-Ray SDK for Node.jsではこれを簡単に行うために captureHTTPsGlobal() というメソッドが用意されており、外部APIを呼び出す際に httpsaxiosといったライブラリを利用している場合には自動でトレースしてくれます。 ですが、このLambda関数では外部APIへの呼び出しにhttpsaxiosといったライブラリではなく undiciというライブラリが利用されていたため、トレースすることができませんでした。

ちなみに、undiciというライブラリはHTTP/1.1に特化したHTTPクライアントで、既存のhttpに比べてベンチマークが良いことが特徴です。初めて知りました?

Node.js Undici Benchmarks

また、undiciのリポジトリのX-Rayが利用できないという旨のissueには、Diagnostics Channelを利用する方法が記述されていますが、Promiseの対応が大変そうに思われるため今回は採用しませんでした。

解決策

captureHTTPsGlobal()を利用した場合でも結局はX-RayにJSONデータが送信されています。また、X-Ray SDK for Node.jsで作成できるセグメントにはaddAttribute()で自由にプロパティを追加することができます。 なので、手動で「ダウンストリームHTTP呼び出し」と同フォーマットのセグメントを生成することができました。 ただし、あくまでサービスマップに表示することが今回の主な目的なので、captureHTTPsGlobal()を利用した場合と完全に同じような実装にはできていません。異常系のハンドリングやエラー/障害であるかどうかの判定は考慮不足な点が多いです。

具体的にどのようなセグメントなのかはこちらに記載されています

subSegment?.addAttribute('http', {
  request: {
    method: 'GET',
    url: 'https://classmethod.jp/',
    traced: true,
  },
  response: {
    content_length: -1,
    status: 200,
  },
});
subSegment?.addAttribute('namespace', 'remote');

外部APIを呼び出すたびにこの記述があると管理が大変になるので、undicifetch()のラッパーを作成し、ラッパー内でこの記述を行うようにしました。
あくまで参考程度ですので、もし利用されるなら利用は自己責任でお願いします。

import { getSegment } from 'aws-xray-sdk';
import {
  fetch as fetchByUndici,
  RequestInfo,
  RequestInit,
  Response,
} from 'undici';

/**
 * fetchをトレースできるようにしたラッパー
 * undiciのfetchと同じインタフェース
 */
export const fetch = async (
  input: RequestInfo,
  init?: RequestInit
): Promise<Response> => {
  const subSegment = getSegment()?.addNewSubsegment(input.toString());

  subSegment?.addAttribute('namespace', 'remote');
  subSegment?.addAttribute('http', {
    request: {
      method: init?.method,
      url: input,
      traced: true,
    },
  });
  
  // NOTE: リクエストヘッダーやリクエストボディには認証情報など含まれるため、X-Rayに出力しない
  // 必要であれば各利用箇所ごとにログ出力を行う

  try {
    const response = await fetchByUndici(input, init);
    subSegment?.addAttribute('http', {
      request: {
        method: init?.method,
        url: input,
        traced: true,
      },
      response: {
        // NOTE: content_lengthは出力でできない。レスポンスボディは一度読み込むと再度読み込めないため。
        status: response.status,
      },
    });

    // NOTE: レスポンスボディは一度読み込むと再度読み込めないため、ここではX-Rayに出力しない
    // 必要であれば各利用箇所ごとにログ出力を行う

    return response;
  } finally {
    subSegment?.close();
  }
};

注意点として、undicifetch()の戻り値であるResponseにはcontent-lengthが含まれていないため、content_lengthは設定することができません。

参考URL