Amazon Web Services ブログ

AWS Distro for OpenTelemetry の新機能 – トレースのサポートが一般的に利用可能に

昨年の re:Invent の前、AWS でサポートされている OpenTelemetry プロジェクトの安全なディストリビューションである AWS Distro for OpenTelemetryパブリックプレビューをご紹介しました。OpenTelemetry は、アプリケーションの動作とパフォーマンスをよりよく理解するために、テレメトリデータをインストルメント化、生成、収集、およびエクスポートするためのツール、API、および SDK を提供します。2021 年 9 月 22 日、アップストリームの OpenTelemetry は、そのコンポーネントのトレース安定性マイルストーンを発表しました。2021 年 9 月 23 日、トレースのサポートが AWS Distro for OpenTelemetry で一般的に利用可能になったことをお知らせします。

OpenTelemetry を使用すると、アプリケーションを一度だけインストルメント化し、トレースを複数のモニタリングソリューションに送信できます。

AWS Distro for OpenTelemetry を使用して、Amazon Elastic Compute Cloud (Amazon EC2)Amazon Elastic Container Service (Amazon ECS)Amazon Elastic Kubernetes Service (EKS)、および AWS Lambda、ならびにオンプレミスで実行されているアプリケーションをインストルメント化できます。AWS Fargate で実行され、ECS または EKS を介してオーケストレートされたコンテナもサポートされています。

AWS Distro for OpenTelemetry によって収集されたトレーシングデータを AWS X-Ray や、次のようなパートナーの送信先に送信できます。

自動インストルメンテーションエージェントを使用すると、コードを変更せずにトレースを収集できます。現在、自動インストルメンテーションは Java および Python アプリケーションで利用できます。Python の自動インストルメンテーションのサポートは、現在 AWS SDK のみを対象としています。OpenTelemetry SDK を使用して、他のプログラミング言語 (Go、Node.js、.NET など) を使用してアプリケーションをインストルメント化できます。

これが Java アプリケーションで実際にどのように機能するのか見てみましょう。

自動インストルメンテーションを使用した Java アプリケーションのトレースの視覚化
Amazon Simple Storage Service (Amazon S3) バケットと Amazon DynamoDB テーブルのリストを表示するシンプルな Java アプリケーションを作成します。

package com.example.myapp;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.ListTablesResponse;
import software.amazon.awssdk.services.dynamodb.model.ListTablesRequest;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

import java.util.List;

/**
 * Hello world!
 *
 */
public class App {

    public static void listAllTables(DynamoDbClient ddb) {

        System.out.println("DynamoDB Tables:");

        boolean moreTables = true;
        String lastName = null;

        while (moreTables) {
            try {
                ListTablesResponse response = null;
                if (lastName == null) {
                    ListTablesRequest request = ListTablesRequest.builder().build();
                    response = ddb.listTables(request);
                } else {
                    ListTablesRequest request = ListTablesRequest.builder().exclusiveStartTableName(lastName).build();
                    response = ddb.listTables(request);
                }

                List<String> tableNames = response.tableNames();

                if (tableNames.size() > 0) {
                    for (String curName : tableNames) {
                        System.out.format("* %s\n", curName);
                    }
                } else {
                    System.out.println("No tables found!");
                    System.exit(0);
                }

                lastName = response.lastEvaluatedTableName();
                if (lastName == null) {
                    moreTables = false;
                }
            } catch (DynamoDbException e) {
                System.err.println(e.getMessage());
                System.exit(1);
            }
        }

        System.out.println("Done!\n");
    }

    public static void listAllBuckets(S3Client s3) {

        System.out.println("S3 Buckets:");

        ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build();
        ListBucketsResponse listBucketsResponse = s3.listBuckets(listBucketsRequest);
        listBucketsResponse.buckets().stream().forEach(x -> System.out.format("* %s\n", x.name()));

        System.out.println("Done!\n");
    }

    public static void listAllBucketsAndTables(S3Client s3, DynamoDbClient ddb) {
        listAllBuckets(s3);
        listAllTables(ddb);
    }

    public static void main(String[] args) {

        Region region = Region.EU_WEST_1;

        S3Client s3 = S3Client.builder().region(region).build();
        DynamoDbClient ddb = DynamoDbClient.builder().region(region).build();

        listAllBucketsAndTables(s3, ddb);

        s3.close();
        ddb.close();
    }
}

Apache Maven を使用してアプリケーションをパッケージ化します。S3 および DynamoDB とのインタラクションに使用する AWS SDK for Java 2.x などの依存関係を管理する Project Object Model (POM) ファイルを次に示します。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <groupId>com.example.myapp</groupId>
  <artifactId>myapp</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>myapp</name>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>bom</artifactId>
        <version>2.17.38</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>s3</artifactId>
    </dependency>
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>dynamodb</artifactId>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <mainClass>com.example.myapp.App</mainClass>
            </manifest>
          </archive>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Maven を使用して、すべての依存関係を含む Java Archive (JAR) 実行可能ファイルを作成します。

$ mvn clean compile assembly:single

アプリケーションを実行してトレーシングデータを取得するには、次の 2 つのコンポーネントが必要です。

1 つのターミナルで、Docker を使用して AWS Distro for OpenTelemetry Collector を実行します。

$ docker run --rm -p 4317:4317 -p 55680:55680 -p 8889:8888 \
         -e AWS_REGION=eu-west-1 \
         -e AWS_PROFILE=default \
         -v ~/.aws:/root/.aws \
         --name awscollector public.ecr.aws/aws-observability/aws-otel-collector:latest

コレクターは、トレースを受信してモニタリングプラットフォームに転送する準備ができました。デフォルトでは、AWS Distro for OpenTelemetry Collector は AWS X-Ray にトレースを送信します。コレクターの設定を編集して、エクスポーターを変更したり、さらにエクスポーターを追加したりできます。例えば、ドキュメントに従って、OLTP プロトコルを使用してテレメトリデータを送信するように OLTP エクスポーターを設定できます。ドキュメントには、他のパートナーの送信先を設定する方法も記載されています。

AWS Distro for OpenTelemetry Auto-Instrumentation Java Agent の最新バージョンをダウンロードします。ここで、アプリケーションを実行し、エージェントを使用してテレメトリデータをキャプチャします。コードに特定のインストルメンテーションを追加する必要はありません。OTEL_RESOURCE_ATTRIBUTES 環境変数で、サービスの名前と名前空間を設定します。

$ OTEL_RESOURCE_ATTRIBUTES=service.name=MyApp,service.namespace=MyTeam \
  java -javaagent:otel/aws-opentelemetry-agent.jar \
       -jar myapp/target/myapp-1.0-SNAPSHOT-jar-with-dependencies.jar

想定どおり、S3 バケットのリストをグローバルに取得し、リージョン内の DynamoDB テーブルのリストを取得します。

より多くのトレーシングデータを生成するために、前のコマンドを数回実行します。アプリケーションを実行するたびに、テレメトリデータがエージェントによって収集され、コレクターに送信されます。コレクターはデータをバッファリングし、設定されているエクスポーターに送信します。デフォルトでは、トレースは X-Ray に送信されています。

ここで、AWS X-Ray コンソールでサービスマップを見て、アプリケーションと他のサービスとのインタラクションを確認します。

コンソールのスクリーンショット。

表示されています! コードに変更がなければ、アプリケーションによる S3 API と DynamoDB API に対する呼び出しが表示されます。エラーは発生していないため、すべての円が緑色になっています。円の中には、呼び出しの平均レイテンシーと 1 分あたりのトランザクション数が表示されています。

Java アプリケーションへのスパンの追加
自動的に収集される情報は、トレースを使用してより多くの情報を提供することで改善できます。例えば、アプリケーションのさまざまな部分で同じサービスとのインタラクションがある場合があり、それらのインタラクションをサービスマップで分離すると便利です。この方法によれば、エラーやレイテンシーが高い場合は、アプリケーションのどの部分が影響を受けるかを知ることができます。

その方法の 1 つは、スパンまたはセグメントを使用することです。スパンは、論理的に関連するアクティビティのグループを表します。例えば、listAllBucketsAndTables メソッドは 2 つのオペレーションを実行しています。1 つは S3 を使用しており、もう 1 つは DynamoDB を使用しています。それらをスパンでまとめてグループ化したいと思います。OpenTelemetry を使用する最も迅速な方法は、@WithSpan アノテーションをメソッドに追加することです。通常、メソッドの結果は引数に依存するため、@SpanAttribute アノテーションを使用して、メソッド呼び出しのどの引数を自動的に属性としてスパンに追加するかを記述します。

@WithSpan
    public static void listAllBucketsAndTables(@SpanAttribute("title") String title, S3Client s3, DynamoDbClient ddb) {

        System.out.println(title);

        listAllBuckets(s3);
        listAllTables(ddb);
    }

@WithSpan アノテーションと @SpanAttribute アノテーションを使用できるようにするには、それらをコードにインポートし、必要な OpenTelemetry 依存関係を POM に追加する必要があります。これらの変更はすべて OpenTelemetry の仕様に基づいており、使用している実際の実装や、テレメトリデータの視覚化または分析に使用するツールには依存していません。これらの変更は、アプリケーションをインストルメント化するために一度だけ行う必要があります。これは素晴らしいことですよね。

スパンの仕組みをよりよく理解するために、同じオペレーションを逆の順序で実行する別のメソッドを作成します。まず DynamoDB テーブルをリストし、次に S3 バケットをリストします。

    @WithSpan
    public static void listTablesFirstAndThenBuckets(@SpanAttribute("title") String title, S3Client s3, DynamoDbClient ddb) {

        System.out.println(title);

        listAllTables(ddb);
        listAllBuckets(s3);
    }

これで、アプリケーションは 2 つのメソッド (listAllBucketsAndTableslistTablesFirstAndThenBuckets) を交互に実行しています。簡単にするために、インストルメント化されたアプリケーションの完全なコードを次に示します。

package com.example.myapp;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.ListTablesResponse;
import software.amazon.awssdk.services.dynamodb.model.ListTablesRequest;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

import java.util.List;

import io.opentelemetry.extension.annotations.SpanAttribute;
import io.opentelemetry.extension.annotations.WithSpan;

/**
 * Hello world!
 *
 */
public class App {

    public static void listAllTables(DynamoDbClient ddb) {

        System.out.println("DynamoDB Tables:");

        boolean moreTables = true;
        String lastName = null;

        while (moreTables) {
            try {
                ListTablesResponse response = null;
                if (lastName == null) {
                    ListTablesRequest request = ListTablesRequest.builder().build();
                    response = ddb.listTables(request);
                } else {
                    ListTablesRequest request = ListTablesRequest.builder().exclusiveStartTableName(lastName).build();
                    response = ddb.listTables(request);
                }

                List<String> tableNames = response.tableNames();

                if (tableNames.size() > 0) {
                    for (String curName : tableNames) {
                        System.out.format("* %s\n", curName);
                    }
                } else {
                    System.out.println("No tables found!");
                    System.exit(0);
                }

                lastName = response.lastEvaluatedTableName();
                if (lastName == null) {
                    moreTables = false;
                }
            } catch (DynamoDbException e) {
                System.err.println(e.getMessage());
                System.exit(1);
            }
        }

        System.out.println("Done!\n");
    }

    public static void listAllBuckets(S3Client s3) {

        System.out.println("S3 Buckets:");

        ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build();
        ListBucketsResponse listBucketsResponse = s3.listBuckets(listBucketsRequest);
        listBucketsResponse.buckets().stream().forEach(x -> System.out.format("* %s\n", x.name()));

        System.out.println("Done!\n");
    }

    @WithSpan
    public static void listAllBucketsAndTables(@SpanAttribute("title") String title, S3Client s3, DynamoDbClient ddb) {

        System.out.println(title);

        listAllBuckets(s3);
        listAllTables(ddb);

    }

    @WithSpan
    public static void listTablesFirstAndThenBuckets(@SpanAttribute("title") String title, S3Client s3, DynamoDbClient ddb) {

        System.out.println(title);

        listAllTables(ddb);
        listAllBuckets(s3);

    }

    public static void main(String[] args) {

        Region region = Region.EU_WEST_1;

        S3Client s3 = S3Client.builder().region(region).build();
        DynamoDbClient ddb = DynamoDbClient.builder().region(region).build();

        listAllBucketsAndTables("My S3 buckets and DynamoDB tables", s3, ddb);
        listTablesFirstAndThenBuckets("My DynamoDB tables first and then S3 bucket", s3, ddb);

        s3.close();
        ddb.close();
    }
}

そして、追加の OpenTelemetry 依存関係を含む更新された POM を次に示します。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <groupId>com.example.myapp</groupId>
  <artifactId>myapp</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>myapp</name>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>bom</artifactId>
        <version>2.16.60</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>s3</artifactId>
    </dependency>
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>dynamodb</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-extension-annotations</artifactId>
      <version>1.5.0</version>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-api</artifactId>
      <version>1.5.0</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <mainClass>com.example.myapp.App</mainClass>
            </manifest>
          </archive>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

これらの変更でアプリケーションをコンパイルし、数回再実行します。

$ mvn clean compile assembly:single

$ OTEL_RESOURCE_ATTRIBUTES=service.name=MyApp,service.namespace=MyTeam \
  java -javaagent:otel/aws-opentelemetry-agent.jar \
       -jar myapp/target/myapp-1.0-SNAPSHOT-jar-with-dependencies.jar

ここで、これらのアノテーションによって提供される追加情報を使用してコンピューティングされた X-Ray サービスマップを見てみましょう。

コンソールのスクリーンショット。

2 つのメソッドと、それらが呼び出す他のサービスが表示されました。エラーや高レイテンシーがある場合、2 つの方法がどのように影響を受けるかを簡単に理解できます。

X-Ray コンソールの [Traces] (トレース) セクションで、いくつかのトレースの raw データを確認します。title 引数には @SpanAttribute アノテーションが付けられているため、各トレースは metadata セクションでその引数の値を持ちます。

コンソールのスクリーンショット。

Lambda 関数からのトレースの収集
前のステップは、オンプレミス、EC2、およびコンテナで実行されているアプリケーションで機能します。トレースを収集し、Lambda 関数で自動インストルメンテーションを使用するには、AWS マネージド OpenTelemetry Lambda Layers を使用できます (いくつかの例がリポジトリに含まれています)。

関数に Lambda レイヤーを追加したら、環境変数 OPENTELEMETRY_COLLECTOR_CONFIG_FILE を使用して、独自の設定をコレクターに渡すことができます。AWS Distro for OpenTelemetry with AWS Lambda の使用の詳細については、ドキュメントを参照してください。

可用性と料金
AWS Distro for OpenTelemetry を使用して、オンプレミスと AWS で実行されているアプリケーションからテレメトリデータを取得できます。AWS Distro for OpenTelemetry を使用するための追加コストはありません。設定によっては、OpenTelemetry データの送信先である AWS のサービス (AWS X-RayAmazon CloudWatch、および Amazon Managed Service for Prometheus (AMP)) の料金をお支払いいただく場合があります。

詳細については、10 月 7 日 (木) の午前 10 時 (太平洋標準時)/午後 1 時 (米国東部標準時(夏時間))/午後 7 時 (中央ヨーロッパ夏時間) に開催されるこちらのウェビナーにぜひご参加ください。

AWS Distro for OpenTelemetry を使用して、アプリケーションのインストルメンテーションを簡素化し、可観測性を向上させましょう。

Danilo

原文はこちらです。