AWS X-rayでアプリケーションのリクエストをトレースし可視化する

awsdockergolangmonitoring

AWS X-rayはリクエストをトレースして、タイムラインやサービスマップを可視化、分析できるサービス。 サービスの数が増えると見えづらくなる、どこにどれくらいのトラフィックがあって、どこで問題が起きているのかといったことを一目で確認できる。

料金はトレースの記録と取得に対してかかり、SamplingRuleの設定によって抑えることができる。

今回はローカルにdocker-composeで立ち上げたWebサーバーからセグメントデータをxray-daemon経由で送り、コンソール上で取得できることを確認する。全体のコードはGitHubにある。

xray-daemonは次のようなフォーマットのデータを受け取りバッチで送るデーモン。

$ cat segment.txt
{"format": "json", "version": 1}
{"trace_id": "1-594aed87-ad72e26896b3f9d3a27054bb", "id": "6226467e3f845502", "start_time": 1498082657.37518, "end_time": 1498082695.4042, "name": "test.elasticbeanstalk.com"}

$ cat segment.txt > /dev/udp/127.0.0.1/2000

ローカルで動かす場合は -o を付けてインスタンスメタデータを読みに行かないようにする必要がある。ドキュメントでは.aws/rootにマウントしているが、そうすると送る際に NoCredentialProviders: no valid providers in chain. Deprecated. になってしまう。Dockerfileを見たところxrayユーザーで動かしていることが分かったので /home/xray にしている。

version: "3.9"
services:
  xray-daemon:
    image: amazon/aws-xray-daemon:3.x
    ports:
      - "2000:2000/udp"
    command: 
      - "-o" # Don't check for EC2 instance metadata.
    volumes:
      - ~/.aws:/home/xray/.aws:ro
    environment:
      AWS_REGION: ap-northeast-1
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ~/.aws:/root/.aws:ro
    depends_on:
      - xray-daemon
    environment:
      AWS_REGION: ap-northeast-1

xray-sdkhttp.Clienthttp.Handler をWrapしたり、AWS SDKのconfigを追加することで透過的にセグメントデータが送られる。

package main

import (
	"io"
	"net/http"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-xray-sdk-go/instrumentation/awsv2"
	"github.com/aws/aws-xray-sdk-go/xray"
	"github.com/labstack/echo/v4"
)

func main() {
	if err := xray.Configure(xray.Config{
		DaemonAddr:     "xray-daemon:2000",
		ServiceVersion: "1.0.0",
	}); err != nil {
		panic(err)
	}

	e := echo.New()
	e.Use(echo.WrapMiddleware(func(h http.Handler) http.Handler {
		return xray.Handler(xray.NewFixedSegmentNamer("test-app"), h)
	}))
	e.GET("/", hello)
	e.Logger.Fatal(e.Start(":8080"))
}

func hello(c echo.Context) error {
	ctx := c.Request().Context()
	// http request
	req, err := http.NewRequest(http.MethodGet, "https://aws.amazon.com/", nil)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
	resp, err := xray.Client(http.DefaultClient).Do(req.WithContext(ctx))
	if err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()

	// sub segment
	subCtx, subSeg := xray.BeginSubsegment(ctx, "waiting-something")

	// aws sdk
	cfg, err := config.LoadDefaultConfig(subCtx)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
	awsv2.AWSV2Instrumentor(&cfg.APIOptions)
	svc := s3.NewFromConfig(cfg)
	if _, err := svc.ListBuckets(subCtx, &s3.ListBucketsInput{}); err != nil {
		c.JSON(http.StatusInternalServerError, err.Error())
		return nil
	}

	subSeg.Close(nil)

	return c.JSON(http.StatusOK, "hello")
}

サービス間のTraceID等の受け渡しのために次のようなTracing header付与される

X-Amzn-Trace-Id: Root=1-5759e988-bd862e3fe1be46a994272793;Sampled=1

リクエストを送るとコンソール上からタイムラインとサービスマップが確認できる。

タイムライン

もしテスト対象のコードにxrayが含まれているなら AWS_XRAY_SDK_DISABLED=true で実行すると良さそうだ。 contextにsegmentデータが含まれていないと、通常 segment cannot be found になってしまうが、それも回避できる。

func test() {
	os.Setenv("AWS_XRAY_SDK_DISABLED", "true")
	xray.BeginSubsegment(context.TODO(), "test")
}

あとはCDKでSamplingRuleとGroupを作成した。追加料金はかかるが、Insightを有効にすることでGroupの異常を検知することができる。

new CfnSamplingRule(this, 'TestAppSamplingRule', {
  samplingRule: {
    ruleName: "test-app",
    resourceArn: "*",
    priority: 10,
    fixedRate: 0,
    reservoirSize: 10,
    serviceName: "*",
    serviceType: "*",
    host: "localhost:8080",
    httpMethod: "*",
    urlPath: "*",
    version: 1,
  }
})

new CfnGroup(this, 'TestAppGroup', {
  groupName: "test-app-group",
  filterExpression: 'http.url CONTAINS "localhost:8080"',
  insightsConfiguration: {
    insightsEnabled: true,
    notificationsEnabled: false
  }
})