Quarkus + Micrometer で CloudWatch にメトリクスを送ってみる

Quarkus は Micrometer を経由してメトリクスを収集することができます。今回は CloudWatch へメトリクスを送信する方法を調査しました。

参考にするドキュメント

Quarkus には Micrometer の拡張は存在しますが、 CloudWatch の実装はないようなので、 Quarkus のガイドにある「Creating a customized MeterRegistry」の章を参考に実装します。

Micrometer には CloudWatch のモジュールがあるので、そちらのドキュメントも参照しました。

必要な手順

  1. IAM ユーザーの作成
  2. Quarkus アプリケーションにモジュールを追加
  3. MeterRegistry の初期化処理を書く

IAM ユーザーの作成

先に IAM ユーザーを作成しておきます。認証情報タイプは「アクセスキー – プログラムによるアクセス」で、必要な権限は cloudwatch:PutMetricData のみです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "cloudwatch:PutMetricData",
            "Resource": "*"
        }
    ]
}

アクセスキーID と シークレットキーは後で必要になるので、手元にひかえておきます。

拡張とモジュールの追加

必要なモジュールを追加します。 pom.xml に以下を追記します。

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-micrometer</artifactId>
    </dependency>
    <dependency>
      <groupId>io.micrometer</groupId>
      <artifactId>micrometer-registry-cloudwatch2</artifactId>
    </dependency>

MeterRegistry の初期化処理

CloudWatch 用の MeterRegistry の初期化処理を書きます。

application.properties から設定を取るようにするため、 interface を書きます。

@ConfigMapping(prefix = "metrics.cloud-watch")
public interface Config {
    /**
     * メトリクスの収集を有効にします。
     */
    @WithDefault("false")
    boolean enabled();

    /**
     * CloudWatch の名前空間。
     */
    @WithDefault("quarkus-metrics")
    String namespace();

    /**
     * AWS のリージョン。
     * 
     * 指定しない場合は AWS SDK により自動で設定されます。
     * 
     * @see DefaultAwsRegionProviderChain
     */
    Optional<String> region();

    /**
     * アクセスキー ID。
     * 
     * {@link #accessKeyId} と {@link #secretAccessKey} の両方が指定された場合のみ使用します。
     * 指定しない場合は AWS SDK により自動で設定されます。
     * 
     * @see DefaultCredentialsProvider
     */
    Optional<String> accessKeyId();

    /**
     * シークレットアクセスキー。
     * 
     * {@link #accessKeyId} と {@link #secretAccessKey} の両方が指定された場合のみ使用します。
     * 指定しない場合は AWS SDK により自動で設定されます。
     * 
     * @see DefaultCredentialsProvider
     */
    Optional<String> secretAccessKey();

}

application.properties は以下のような内容です。

metrics.cloud-watch.enabled=true
metrics.cloud-watch.access-key-id=【アクセスキーID】
metrics.cloud-watch.secret-access-key=【シークレットアクセスキー】

アクセスキー ID とシークレットアクセスキーは設定からとらなくても、 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY 環境変数を設定すれば AWS SDK が自動で拾ってくれます1またはシステムプロパティの aws.accessKeyIdと aws.secretAccessKey でも 。今回は環境変数を設定するのが面倒だったので application.properties から拾うようにしてみました。EC2 や ECS で実行した場合は何もしなくても、実行中のインスタンス/タスクの権限が自動で適用されるはずです。

CloudWatchMeterRegistry の初期化は以下のような内容です。 CDI のプロデューサーを書きます。

public class MeterRegistryProducer {

    @Produces
    @Singleton
    public CloudWatchMeterRegistry createMeterRegistry(Clock clock, Config config) {
        CloudWatchConfig cloudWatchConfig = new CloudWatchConfig() {

            @Override
            public String get(String s) {
                return null;
            }

            @Override
            public boolean enabled() {
                return config.enabled();
            }

            @Override
            public String namespace() {
                return config.namespace();
            }
        };

        AwsCredentialsProvider credentialProvider = null;
        if (config.accessKeyId().isPresent() && config.secretAccessKey().isPresent()) {
            credentialProvider = StaticCredentialsProvider.create(new AwsCredentials() {
                @Override
                public String accessKeyId() {
                    return config.accessKeyId().get();
                }

                @Override
                public String secretAccessKey() {
                    return config.secretAccessKey().get();
                }
            });
        }

        CloudWatchAsyncClient client = CloudWatchAsyncClient.builder()
                .region(config.region().map(Region::of).orElse(null))
                .credentialsProvider(credentialProvider)
                .build();

        CloudWatchMeterRegistry meterRegistry = new CloudWatchMeterRegistry(
                cloudWatchConfig,
                clock,
                client);

        // EC2 のインスタンス ID をタグに追加(ディメンション)
        var instanceId = "unknown";
        try {
            instanceId = EC2MetadataUtils.getInstanceId();
        } catch (SdkClientException e) {
            // EC2 ではない場合
        }

        var tag = Tag.of("InstanceId", instanceId);
        meterRegistry.config()
                .meterFilter(MeterFilter.commonTags(List.of(tag)));

        // ECS の場合は $ECS_CONTAINER_METADATA_URI_V4/task からタスクIDを取る必要がありそう

        return meterRegistry;
    }
}

デフォルトだとホスト名がディメンションに含まれないので、インスタンス ID などを追加する処理を入れてみました。

起動してしばらくすると、 CloudWatch に指定した namespace が追加されます。

中身を見てみると、 JVM のメモリ使用量がヒープとそれ以外、さらに細かい項目まで出ているので、メモリのパフォーマンスチューニングをする時に使えそうです。

メモリのメトリクス

ほかに HTTP のリクエスト数も送ってくれるようです。どのエンドポイントでエラーが発生しやすいか調査できそうです。

HTTP のメトリクス

コメントする

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください