X-Ray SDK for Java の HTTP クライアントを使うと外部へのリクエストもトレースしやすくなるようなので試してみた

2023.03.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

いわさです。

API Gateway + Lambda なサーバーレスアプリを構築していて、サービス間通信を行うために API(A) から API(B) を呼び出すシーンがありました。
その際に X-Ray 関係のヘッダーを伝搬することで API A から B まで通して確認が出来ることを教えてもらったので試していたのですが、その過程で X-Ray SDK の HTTP クライアントが便利なことを知ったので紹介したいと思います。

今回は Java Lambda を作成しようとしていたので X-Ray SDK for Java で試していますが、他のランタイムでも X-Ray SDK から何かしら機能が提供されているので探してみてください。例えば .NET であれば HttpClientXRayTracingHandler が用意されているので標準の HTTP クライアント生成時に差し込むことが出来るようになっていました。便利。

導入

まずは API を 2 つ用意します。
今回も SAM のテンプレートで Java 11 (gradle) な Helloworld からスタートしてみます。

% sam init -r java11
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Infrastructure event management
        3 - Multi-step workflow
Template: 1

Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.

Which dependency manager would you like to use?
        1 - gradle
        2 - maven
Dependency manager: 1

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: y
X-Ray will incur an additional cost. View https://aws.amazon.com/xray/pricing/ for more details

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: 

Project name [sam-app]: hoge0312xray

Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)
:

ここではポイントとして X-Ray トレーシングオプションを有効化しています。
余談だがこの機能は私が自分でコントリビュートしたものなのでちょっと思い入れがあっていつも使ってます。

で、デフォルトの関数を複製してもうひとつ API のルートを増やします。

template.yaml

:
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
:
      Events:
        HelloWorld:
          Type: Api 
          Properties:
            Path: /hello
            Method: get

  HelloWorldFunction2:
    Type: AWS::Serverless::Function
    Properties:
:
      Events:
        HelloWorld:
          Type: Api 
          Properties:
            Path: /hello2
            Method: get

:

関数コードはデフォルトから以下の変更を加えました。

  • 関数 1 からは関数 2 のエンドポイントを呼び出す。URLはあとで環境変数で渡す。
  • 関数 1、関数 2 ともに 5 秒のスリープを入れる

App1.java

:

public class App1 implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

    public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json");
        headers.put("X-Custom-Header", "application/json");

        APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
                .withHeaders(headers);
        try {
            final String pageContents = this.getPageContents(System.getenv("NEXTAPI"));
            String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents);

:
}

sam deployしたら関数 2 のエンドポイント URL を取得し、関数 1 の環境変数に設定します。
最初テンプレートの中でやろうと思ったのですが循環参照になってしまって手動設定で妥協しました。

デフォルトで実行 & トレース

実行します。
以下からだとわからないと思いますが、期待どおり 関数 1 で 10 秒、関数 2 で 5 秒程度の処理時間が発生しました。

% curl https://r8c49c7kud.execute-api.ap-northeast-1.amazonaws.com/Prod/hello2/
{ "message": "hello world", "location": "43.207.127.208" }%                                                                                                                              
% curl https://r8c49c7kud.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/ 
{ "message": "hello world", "location": "{ "message": "hello world", "location": "43.207.127.208" }" }%

この時の X-Ray トレースマップなどを見てみましょう。

関数 2 のトレースマップとセグメントタイムラインでは実行時間が 5 秒程度だったことがわかります。

関数 1 でも実行に 10 秒かかったことがわかりますが、今時点ではそのうちの 5 秒が関数 2 によって発生していることはわからないですね。
関数 1 のタイムラインだけみてボトルネックがどこにあるのかすぐには気づけ無さそうです。

X-Ray SDK を使う

ここで冒頭の X-Ray SDK for Java の HTTP クライアントを使ってみます。
今回は同じ AWS リソースに対して呼び出しをしていますが、外部 API 呼び出しの際などにもサービスグラフに追加出来るようになります。

使用のためにパッケージを追加します。

Maven Repository: com.amazonaws » aws-xray-recorder-sdk-apache-http

build.gradle

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
    implementation 'com.amazonaws:aws-lambda-java-events:3.11.0'
    implementation 'com.amazonaws:aws-xray-recorder-sdk-apache-http:2.13.0'
    testImplementation 'junit:junit:4.13.2'
}

sourceCompatibility = 11
targetCompatibility = 11

変更前のコードが InputStream 以降は処理出来るようになっていたので、HTTP クライアントの作成から InputStream 取得までを実装しました。

App1.java

:
    private String getPageContents(String address) throws IOException{
        CloseableHttpClient httpclient = HttpClientBuilder.create().build();
        HttpGet httpGet = new HttpGet(address);
        CloseableHttpResponse response = httpclient.execute(httpGet);
        HttpEntity entity = response.getEntity();
        InputStream inputStream = entity.getContent();
        try(BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
            return br.lines().collect(Collectors.joining(System.lineSeparator()));
        }
    }
:

実行後し、X-Ray コンソールを確認してみましょう。

トレースマップ

微妙に交差して見にくいですが、Function1 が 10 秒かかっていますが、API Gateway Prod ステージ経由で Function2 を呼び出して 5 秒かかっているのがトレースマップからもわかります。

セグメントタイムライン

セグメントタイムライン上もわかるのですが、Function1 上でサブセグメントも作成してくれています。
今回は API Gateway でしたが、外部のエンドポイントだとしても X-Ray SDK for Java から作成した HTTP クライアントがサブセグメント周りも自動作成してくれるので便利です。

さいごに

本日は X-Ray SDK for Java の HTTP クライアントを使って外部リクエストもトレースするというのをやってみました。

DevelopersIO を探してみると、X-Ray のトレースを拡張する系の記事は他にもいくつかありました。

どうやら Python の場合だと patch_all でサポートされているライブラリ全般にパッチを提供してサブセグメントなど自動作成してくれるようですね。Python Lambda の優位点をまたひとつ見つけてしまった。

今回のような方法で Java など他のランタイムでも、外部への HTTP リクエスト送信部分へトレース範囲を拡げることが出来るようなので是非試してみてください。