本連載では、以下に示すようなマイクロサービスアーキテクチャのアプリケーション環境を構築しています。

構成図

構成図

前回は、アプリケーションの分析/デバッグサービスである「AWS X-Ray」のセグメント/サブセグメントの概念を解説し、フロントエンドのWebアプリケーションとバックエンドのマイクロサービス双方に共通する設定を実装しました。続く今回は、X-Rayデーモンへトレースデータを送信するためのフロントエンドWebアプリケーションの設定の個別の実装について、解説していきます。

なお、本連載で実際に作成するアプリケーションはGitHub上にコミットしています。以降に記載するソースコードでは、import文など本質的でない記述を省略している部分があるので、実行コードを作成する際は、必要に応じて適宜GitHubにあるソースコードも参照してください。

サンプリングルールの設定

X-Rayのトレースデータ送信では、指定されたサンプリングルールに基づいてデータが送信されます。前回も解説した通り、作成するアプリケーションでは共通のX-Ray設定クラス「XRayConfig」(frontend-webapp/src/main/java/org/debugroom/mynavi/sample/aws/microservice/frontend/webapp/config/XRayConfig.java)において、クラスパス配下にある、サンプリングルールを設定した「sampling-rules.json」を読み込んでいます。

package org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.config;

// omit

@Aspect
@Configuration
@EnableAspectJAutoProxy
public class XRayConfig extends AbstractXRayInterceptor {

    //omit

    static {
        try{
            AWSXRayRecorderBuilder builder = AWSXRayRecorderBuilder.standard()
                 .withSamplingStrategy(new LocalizedSamplingStrategy(
                         ResourceUtils.getURL("classpath:sampling-rules.json")));
            AWSXRay.setGlobalRecorder(builder.build());
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    // omit

    @Override
    @Pointcut("@within(com.amazonaws.xray.spring.aop.XRayEnabled) " +
         " && execution(* org.debugroom.mynavi.sample.aws.microservice..*.*(..))" )
    protected void xrayEnabledClasses() {
    }
}

サンプリングルールの適切なレートは、計測対象によって異なります。本連載では、全てのリクエストが計測対象となるよう、レートを「1」に設定しておきます。なお、サンプリングルールでは対象のホストやHTTPメソッド、URLパスなど細かい設定が可能です。詳細は、AWSの公式サイトにある「サンプリングルールの設定」を参照してください。

sampling-rules.jsonの記述は以下の通りです。

{
  "version": 2,
  "rules": [
    {
      "description": "Sample description.",
      "host": "*",
      "http_method": "*",
      "url_path": "/api/*",
      "fixed_target": 1,
      "rate": 1
    }
  ],
  "default": {
     "fixed_target": 1,
     "rate": 0.1
  }
}

フロントエンドのWebアプリケーションにおけるX-Rayの設定

今回は、サブセグメントとして、Controller、Service、Repositoryごとに実行時間を計測します(Spring Securityで認証の際に利用されるCustomUserDetailsも含めています)。前回設定したXRayConfigクラスのAOPポイントカット定義に基づき、各コンポーネントの実装クラスに@XRayEnabledアノテーションを付与します。以下は、Controllerにアノテーションを付与する例(frontend-webapp/src/main/java/org/debugroom/mynavi/sample/aws/microservice/frontend/webapp/app/web/SampleController.java)です。

package org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.app.web;

// omit

import com.amazonaws.xray.spring.aop.XRayEnabled;

// omit

@XRayEnabled
@Controller
public class SampleController {
    // omit
}

Repositoryクラスではバックエンドのマイクロサービスを呼び出す処理を実装していましたが、同一のTraceIDを持ち回れるように、リクエストヘッダに指定されたフォーマットでTraceIDを設定しておく必要があります。

「X-Ray SDK for Java」では、デフォルトで「Apache HttpComponents」を使ってTraceIDをリクエストヘッダに埋め込んで送信するcom.amazonaws.xray.proxies.apache.http.DefaultHttpClientが提供されていますが、これまでの連載では、HTTPリクエストの送信は、「Spring WebFlux」が提供するWebClientを使ってバックエンドのサービスを呼び出す処理を実装していたので、WebClientでTraceIDをリクエストヘッダに設定します。設定クラスの実装(frontend-webapp/src/main/java/org/debugroom/mynavi/sample/aws/microservice/frontend/webapp/config/DevConfig.java)は以下の通りです。

package org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.config;

// omit
import com.amazonaws.xray.AWSXRay;
import com.amazonaws.xray.entities.Segment;
import com.amazonaws.xray.entities.Subsegment;
import com.amazonaws.xray.entities.TraceHeader;

import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

// omit

@Configuration
public class DevConfig {

    @Autowired
    ServiceProperties serviceProperties;                                     //(A)

    @Bean
    public WebClient userWebClient(){
        return WebClient.builder()
             .baseUrl(serviceProperties.getDns())
             .filter(exchangeFilterFunction())                               //(B)
             .build();
    }

    private ExchangeFilterFunction exchangeFilterFunction(){                 //(C)
        return (clientRequest, nextFilter) -> {
         Segment segment = AWSXRay.getCurrentSegment();                      //(D)
         Subsegment subsegment = AWSXRay.getCurrentSubsegment();             //(E)
         TraceHeader traceHeader = new TraceHeader(segment.getTraceId(),
                 segment.isSampled() ? subsegment.getId() : null,
                 segment.isSampled() ? TraceHeader.SampleDecision.SAMPLED : TraceHeader.SampleDecision.NOT_SAMPLED);
                                                                             //(F)
         ClientRequest newClientRequest = ClientRequest.from(clientRequest)
                 .header(TraceHeader.HEADER_KEY, traceHeader.toString())
                 .build();                                                   //(G)
         return nextFilter.exchange(newClientRequest);
        };
   }

   // omit

DevConfigクラスコードの説明は以下の通りです。

項番 説明
A バックエンドのマイクロサービスが実行されているホストのURLを定義したプロパティファイルの値を保持するオブジェクトをインジェクションします
B WebClientに共通して行うフィルタ処理を定義します。実際の処理はCのメソッド内で行います
C フィルタ処理をorg.springframework.web.reactive.function.client.ExchangeFilterFunctionとして実装します
D com.amazonaws.xray.AWSXRayから実行中のセグメントオブジェクトを取得します。クラス変数から取得していますが、内部的な実際の処理はスレッドローカルに保存されたオブジェクトを取得しており、スレッドごとに異なるオブジェクトが得られます
E com.amazonaws.xray.AWSXRayから実行中のサブセグメントオブジェクトを取得します。クラス変数から取得していますが、内部的な実際の処理はスレッドローカルに保存されたオブジェクトを取得しており、スレッドごとに異なるオブジェクトが得られます
F com.amazonaws.xray.entities.TraceHeaderのインスタンスをD、Eの情報から生成します。なお、これらの実装はTracedHttpClientを参考にしています
G リクエストヘッダを設定した新しいリクエストを生成して、返却します

また、Controllerの処理が完了した後、TraceIDやユーザーID、実行時間をDynamoDBに保存するよう、HanderInterceptorAdapterを継承したコンポーネントを生成します。このクラスはControllerが実行された後に、オーバーライドされたメソッドが常に実行されるようInterceptorとして追加します。

オーバーライドされたメソッド内では、以前の回で実装したものと同様、Spring SecurityのSecurityContextからCustomUserDetailsを取得してユーザーIDを設定します。TraceIDは、上記の実装と同様にSegmentオブジェクトをcom.amazonaws.xray.AWSXRayから取得し、前回実装したLogクラスに設定してDynamoDBへ保存します(frontend-webapp/src/main/java/org/debugroom/mynavi/sample/aws/microservice/frontend/webapp/config/WebApp.java)。

package org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.app.web.interceptor;

// omit

import com.amazonaws.xray.AWSXRay;
import com.amazonaws.xray.entities.Segment;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.cloud.aws.log.dynamodb.model.Log;
import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.cloud.aws.log.dynamodb.repository.LogRepository;
import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.util.DateStringUtil;
import org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.app.web.security.CustomUserDetails;

@Component
public class AuditLoggingInterceptor extends HandlerInterceptorAdapter {

    @Autowired(required = false)
    LogRepository logRepository;

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                        Object handler, ModelAndView modelAndView) throws Exception {
        String userId = "0";
        SecurityContext securityContext = SecurityContextHolder.getContext();
        Authentication authentication = securityContext.getAuthentication();
        if(Objects.nonNull(authentication)){
            Object principal = authentication.getPrincipal();
            if(principal instanceof CustomUserDetails){
                userId = ((CustomUserDetails)principal)
                    .getUserResource().getUserId();
            }
        }
        Log log = Log.builder()
             .userId(userId)
             .createdAt(DateStringUtil.now())
             .traceId(getTraceId())
             .build();

        logRepository.save(log);
    }

    private String getTraceId(){
        Optional<Segment> segment = AWSXRay.getCurrentSegmentOptional();
        if (segment.isPresent()){
            return segment.get().getTraceId().toString();
        }
        return null;
    }

}

今回は、フロントエンドのWebアプリケーション個別の設定を解説しました。次回はバックエンドのマイクロサービスに必要なAWS X-Rayの個別設定について解説を進めていきます。

著者紹介


川畑 光平(KAWABATA Kohei) - NTTデータ

金融機関システム業務アプリケーション開発・システム基盤担当、ソフトウェア開発自動化関連の研究開発を経て、デジタル技術関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなど様々な開発プロジェクト支援にも携わる。AWS Top Engineers & Ambassadors選出。

本連載の内容に対するご意見・ご質問は Facebook まで。