Quarkus は Micrometer を経由してメトリクスを収集することができます。今回は CloudWatch へメトリクスを送信する方法を調査しました。
参考にするドキュメント
Quarkus には Micrometer の拡張は存在しますが、 CloudWatch の実装はないようなので、 Quarkus のガイドにある「Creating a customized MeterRegistry」の章を参考に実装します。
Micrometer には CloudWatch のモジュールがあるので、そちらのドキュメントも参照しました。
必要な手順
- IAM ユーザーの作成
- Quarkus アプリケーションにモジュールを追加
- 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_ID
と AWS_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 のリクエスト数も送ってくれるようです。どのエンドポイントでエラーが発生しやすいか調査できそうです。