Amazon Web Services 한국 블로그

AWS Distro for OpenTelemetry 추적 지원 기능 정식 출시

작년에 re:Invent를 시작하기 전에는, 저희는 AWS Distro for OpenTelemetry의 공개 미리보기를 시작했습니다. 이것은 AWS에서 제공하는 OpenTelemetry 프로젝트의 안전한 배포 버전입니다. OpenTelemetry는 원격 분석 데이터를 계측, 생성, 수집 및 내보낼 수 있는 도구, API 및 SDK를 제공하여 애플리케이션의 동작과 성능을 더 잘 이해할 수 있게 해줍니다. 어제 업스트림 OpenTelemetry는 그 구성 요소에 대한 추적 안정성 이정표를 발표했습니다. 이제 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 애플리케이션에서 사용할 수 있습니다. 파이썬에 대한 자동 계측 지원은 현재 AWS SDK에만 적용됩니다. 다른 프로그래밍 언어 (예: Go, Node.js, .NET) 를 OpenTelemetry SDK와 함께 사용하여 애플리케이션을 계측할 수 있습니다.

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을 사용하여 애플리케이션을 패키징합니다. 다음은 POM(프로젝트 객체 모델) 파일로 S3 및 DynamoDB와 상호 작용하기 위해 사용하는AWS SDK for Java 2.x와 같은 종속성을 관리합니다.

<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 아카이브(JAR) 파일을 생성합니다.

$ mvn clean compile assembly:single

애플리케이션을 실행하고 추적 데이터를 얻으려면 두 가지 구성 요소가 필요합니다.

한 터미널에서 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 자동 계측 Java 에이전트의 최신 버전을 다운로드합니다. 이제 애플리케이션을 실행하고 에이전트를 사용하여 특정 계측 코드를 추가하지 않고도 원격 분석 데이터를 캡처합니다. OTEL_RESOURCE_ATTRIBUTES 환경 변수에서 서비스 이름과 네임스페이스를 설정했습니다. [[ service.name 및 service.namespace를 X-ray에서 사용하고 있습니까? 이 두 가지를 서비스 맵에서 찾을 수 없습니다 ]]

$ 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 및 DynamoDB API에 대한 내 애플리케이션의 호출을 볼 수 있습니다. 오류가 없었으며 모든 원이 녹색입니다. 원 안에서 호출의 평균 대기 시간과 분당 트랜잭션 수를 찾았습니다.

Java 애플리케이션에 범위 추가하기
추적을 통해 더 많은 정보를 제공하면 자동으로 수집되는 정보를 개선할 수 있습니다. 예를 들어 애플리케이션의 다른 부분에서 동일한 서비스와 상호 작용할 수 있으며, 서비스 맵에서 이러한 상호 작용을 분리하기에 유용합니다. 이 방법으로 오류 또는 대기 시간이 길면 애플리케이션의 어느 부분이 영향을 받는지 알 수 있습니다.

이를 위한 한 가지 방법은 범위 또는 세그먼트를 사용하는 것입니다. 범위는 논리적으로 관련된 활동의 그룹을 나타냅니다. 예를 들어 listAllBucketsAndTables 메서드는 S3와 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);
    }

애플리케이션은 이제 두 메서드 (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 서비스 맵을 살펴보겠습니다.

콘솔 스크린샷.

이제 두 가지 메서드와 이 메서드가 호출하는 다른 서비스를 볼 수 있습니다. 오류가 있거나 대기 시간이 길면 두 가지 방법이 어떻게 영향을 받는지 쉽게 이해할 수 있습니다.

X-Ray 콘솔의 추적 섹션에서 일부 추적에 대한 원시 데이터를 살펴봅니다. title 인수에 @SpanAttribute 주석이 추가되었으므로 각 추적은 메타데이터 섹션에서 해당 인수의 값을 가집니다.

콘솔 스크린샷.

Lambda 함수에서 추적 수집
이전 단계는 온프레미스, EC2, 그리고 컨테이너에서 실행되는 애플리케이션에서 작동합니다. 추적을 수집하고 Lambda 함수에서 자동 계측을 사용하려면 AWS 관리형 OpenTelemetry Lambda 계층 (리포지토리에 몇 가지 예가 포함되어 있음)을 사용할 수 있습니다.

함수에 Lambda 레이어를 추가한 후 환경 변수 OPENTELEMETRY_COLLECTOR_CONFIG_FILE을 사용해 사용자의 자체 구성을 수집기에 전달할 수 있습니다. AWS Lambda에서 AWS Distro for OpenTelemetry을 사용하는 방법에 대한 자세한 내용은 설명서를 참조하십시오.

가용성 및 요금
AWS Distro for OpenTelemetry를 사용하여 온프레미스 및 AWS에서 실행 중인 애플리케이션에서 원격 분석 데이터를 가져올 수 있습니다. AWS Distro for OpenTelemetry를 사용하는 데 드는 추가 비용은 없습니다.

지금 AWS Distro for OpenTelemetry를 사용하여 애플리케이션 계측을 간소화하고 관찰 능력을 개선하십시오.

Danilo