CLOVER🍀

That was when it all began.

Infinispan Server 14.0でメトリクスの収集方法がMicrometerに移行されたので、試してみた

これは、なにをしたくて書いたもの?

Infinispan 14.0.0.Finalで、メトリクスの生成にMicrometerを使うようになったみたいです。

Integration with Micrometer to produce Prometheus and OpenMetrics metrics

Infinispan 14.0.0.Final

こちらを簡単に確認したいと思います。

Infinispan Serverでのメトリクス収集方法の変遷

Infinispan(Infinispan Server)でのメトリクスの収集方法は、過去にいろいろ変わってきました。

自分も過去に少し試しています。

Infinispan 9.4.1.Finalで追加された、Prometheus Exporterを試す - CLOVER🍀

Infinispan Server 13.0とHot Rod Clientでメトリクスを取得する - CLOVER🍀

Infinispan Serverのメトリクスに関するドキュメントは、こちら。

Guide to Infinispan Server / Enabling and configuring Infinispan statistics and JMX monitoring

JMX MBeanは以前からずっとあります。

変わっているのは、HTTPでのメトリクス取得方法ですね。ちょっと見返してみたのですが、こんな感じで変わってきたようです。

Infinispan 9.2.0.FinalでJolokia。

ISPN-7599 Jolokia support by slaskawi · Pull Request #5267 · infinispan/infinispan · GitHub

Infinispan 9.2.0.Final

Infinispan 9.4.1.FinalでPrometheus Exporter。

ISPN-9558 Export Data Grid stats to Prometheus by vblagoje · Pull Request #6290 · infinispan/infinispan · GitHub

Infinispan 9.4.1.Final and Infinispan Spring Boot Starter 2.1.0.Final are out!

Infinispan 10.0.0.FinalでSmallRye Metrics。

ISPN-10244 Smallrye metrics: gauges by anistor · Pull Request #7407 · infinispan/infinispan · GitHub

[ISPN-10244] Observability: use smallrye metrics - Red Hat Issue Tracker

Infinispan 10.0 “Chupacabra”

そして、Infinispan 14.0.0.FinalでMicrometer。

ISPN-12991 Replace Smallrye metrics with Micrometer by fax4ever · Pull Request #9933 · infinispan/infinispan · GitHub

[ISPN-12991] Replace Smallrye metrics with Micrometer - Red Hat Issue Tracker

Infinispan 14.0.0.Final

この時にSmallRye Metricsを使ったバージョンで試していたので、Micrometerに変わるまで短かったような感覚になりましたが、
単に触っていなかっただけみたいです…。

Infinispan Server 13.0とHot Rod Clientでメトリクスを取得する - CLOVER🍀

Hot Rod ClientはずっとJMX MBeanですね。

Guide to Infinispan Server / Enabling and configuring Infinispan statistics and JMX monitoring / Enabling Hot Rod client statistics

Infinispan Serverのメトリクスに関するドキュメントを確認する

Infinispan Serverに関するメトリクスについて、もう1度確認してみます。

Guide to Infinispan Server / Enabling and configuring Infinispan statistics and JMX monitoring

どちらも、明示的に有効化(statisticstrueにする)必要があります。

ちなみに、Embeddedの方も含めるとこちらにまるっと同じ内容が書かれているようです。

Configuring Infinispan caches / Enabling and configuring Infinispan statistics and JMX monitoring

Micrometerが使われているということでしたが、実装としてはMicrometer Prometeusが利用されているようです。

Micrometer Prometheus

バージョンとしては、1.9.2が使われているようです。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/configuration/pom.xml#L202

CacheManagerおよびCacheに関するXML Schema定義はこちら。

urn:infinispan:config:14.0

メトリクスに関する記述を抜粋しておきましょう。

CacheManagerレベル。

Cacheレベル。

今回、SmallRye MetricsからMicrometerに移行されたので、メトリクスの名称が変わったりするのかなと思ったのですが、basevendorといった
スコープは引き続き使われるようです。

Infinispan metrics are provided at the vendor scope. Metrics related to the JVM are provided in the base scope.

Guide to Infinispan Server / Enabling and configuring Infinispan statistics and JMX monitoring / Configuring Infinispan metrics

メトリクスの収集を有効にすると、HTTPで/metricsエンドポイントでメトリクスを取得できるようになります。

メトリクスのフォーマットは以下の2つです。

  • Prometheus Format
  • OpenMetrics Format

Exposition formats | Prometheus

SmallRye Metricsを使っていた頃はJSON Formatもありましたが、こちらは廃止になったようです。

Infinispan no longer provides metrics in MicroProfile JSON format.

フォーマットは、Micrometer側で対応しているみたいですね。このあたりは、あとで少し追ってみます。

OpenMetrics scrape support by shakuzen · Pull Request #2486 · micrometer-metrics/micrometer · GitHub

取得対象のメトリクスは、CacheManagerCacheについては主にJMX MBeanの情報を参照することになるのですが、完全に同じでは
ありません。

JMX Components

このあたりもあとで少し書くとして、Infinispan ServerやHot Rod Clientからメトリクスを取得するように試してみましょう。
最終的には、Prometheusで収集するようにしてみます。

流れとしては、SmallRye Metricsの時に書いたエントリーの焼き直しなのですが。最後にソースコードを追う部分以外は、そんなに変わりませんね。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu120.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-132-generic", arch: "amd64", family: "unix"

Infinispan Serverは、172.18.0.2〜172.18.0.4の3つのノードで動作し、クラスターを構成しているものとします。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment Temurin-17.0.5+8 (build 17.0.5+8)
OpenJDK 64-Bit Server VM Temurin-17.0.5+8 (build 17.0.5+8, mixed mode, sharing)


$ bin/server.sh --version

Infinispan Server 14.0.2.Final (Flying Saucer)
Copyright (C) Red Hat Inc. and/or its affiliates and other contributors
License Apache License, v. 2.0. http://www.apache.org/licenses/LICENSE-2.0

起動コマンドはこちら。

$ bin/server.sh \
    -b 0.0.0.0 \
    -Djgroups.tcp.address=$(hostname -i)

Prometheusは、172.18.0.5で動作しているものとします。

$ ./prometheus --version
prometheus, version 2.40.3 (branch: HEAD, revision: 84e95d8cbc51b89f1a69b25dd239cae2a44cb6c1)
  build user:       root@72aff466572b
  build date:       20221124-09:08:44
  go version:       go1.19.3
  platform:         linux/amd64

準備

テストコードで使う、Maven依存関係など。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>14.0.2.Final</version>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>14.0.2.Final</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.23.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

各Infinispan Serverには、それぞれ管理用ユーザー、アプリケーションユーザーを作成しておきます。

$ bin/cli.sh user create -g admin -p password ispn-admin
$ bin/cli.sh user create -g application -p password ispn-user

また、メトリクスを取得するためにInfinispan Serverにアクセスする際には、REST Connector経由になります。この時に認証の設定をどうするか
決めておく必要がありますが、今回は認証設定を行うことにします。

デフォルトではこのようになっているendpointsの設定を

      <endpoints socket-binding="default" security-realm="default" />

以下のように変更。

      <endpoints socket-binding="default" security-realm="default">
         <endpoint>
            <hotrod-connector>
               <authentication>
                  <sasl mechanisms="SCRAM-SHA-512 SCRAM-SHA-384 SCRAM-SHA-256
                                  SCRAM-SHA-1 DIGEST-SHA-512 DIGEST-SHA-384
                                  DIGEST-SHA-256 DIGEST-SHA DIGEST-MD5 PLAIN"
                        server-name="infinispan"
                        qop="auth"/>
               </authentication>
            </hotrod-connector>
            <rest-connector>
               <authentication mechanisms="DIGEST BASIC"/>
            </rest-connector>
         </endpoint>
      </endpoints>

REST Connector自体はデフォルトで有効で、Digest認証が使えるようになっているのですが、今回はPrometheusからアクセスするので
Basic認証も追加する必要があります。

このように設定すると、REST Connector、Hot Rod Connectorともに設定を明示的に行う必要があります。

Infinispan Serverからメトリクスを取得してみる

まずは、起動しただけのInfinispan Serverからメトリクスを取得してみましょう。

server/conf/infinispan.xmlはデフォルトで以下の状態になっているため、このままでメトリクスを取得できます。

   <cache-container name="default" statistics="true">

Prometheus Formatで取得。

## Digest認証
$ curl --digest -u ispn-user:password 172.18.0.2:11222/metrics


## Basic認証
$ curl -u ispn-user:password 172.18.0.2:11222/metrics

アクセスは、アプリケーション用のユーザーで大丈夫です。

OpenMetrics Formatで取得。

## Digest認証
$ curl --digest -u ispn-user:password -H 'Accept: application/openmetrics-text' 172.18.0.2:11222/metrics


## Basic認証
$ curl -u ispn-user:password -H 'Accept: application/openmetrics-text' 172.18.0.2:11222/metrics

特に操作していない状態でも、このくらいのメトリクスを取得できます。

## Prometheus Format
$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep -v '#' | wc -l
172


## OpenMetrics Format
$ curl -s -u ispn-user:password -H 'Accept: application/openmetrics-text' 172.18.0.2:11222/metrics | grep -v '#' | wc -l
172

取得したメトリクスの例。

$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep -v '#' | head -n 30
base_classloader_loadedClasses_total 11384.0
base_thread_totalStarted 56.0
vendor_jgroups_cluster_fd_sock2_get_actual_bind_port{node="infinispan-server-18034",} 57800.0
base_memory_committedNonHeap_bytes 8.0019456E7
vendor_jgroups_cluster_tcp_get_num_msgs_received{node="infinispan-server-18034",} 197.0
vendor_jgroups_cluster_tcp_get_thread_pool_size{node="infinispan-server-18034",} 4.0
vendor_cache_manager_default_cache_container_stats_evictions{node="infinispan-server-18034",} 0.0
vendor_jgroups_cluster_ufc_get_number_of_credit_responses_sent{node="infinispan-server-18034",} 0.0
vendor_jgroups_cluster_nakack2_get_xmit_table_num_moves{node="infinispan-server-18034",} 0.0
vendor_jgroups_cluster_tcp_get_num_ucast_bytes_received{node="infinispan-server-18034",} 0.0
vendor_jgroups_cluster_unicast3_get_num_send_connections{node="infinispan-server-18034",} 2.0
vendor_cache_manager_default_cache_container_stats_required_minimum_number_of_nodes{node="infinispan-server-18034",} -1.0
vendor_cache_manager_default_server_single_port_11222_transport_pending_tasks{node="infinispan-server-18034",} 0.0
vendor_memoryPool_CodeHeap__non_profiled_nmethods__usage_max_bytes 2761344.0
vendor_jgroups_cluster_gms_get_view_handler_size{node="infinispan-server-18034",} 0.0
vendor_cache_manager_default_cluster_size{node="infinispan-server-18034",} 3.0
vendor_memoryPool_CodeHeap__profiled_nmethods__usage_max_bytes 1.1169152E7
vendor_cache_manager_default_cache_container_health_number_of_nodes{node="infinispan-server-18034",} 3.0
vendor_cache_manager_default_cache_container_stats_remove_hits{node="infinispan-server-18034",} 0.0
vendor_jgroups_cluster_stable_get_stability_received{node="infinispan-server-18034",} 59.0
vendor_jgroups_cluster_unicast3_get_num_xmits{node="infinispan-server-18034",} 0.0
vendor_jgroups_cluster_frag4_get_number_of_received_fragments{node="infinispan-server-18034",} 0.0
base_cpu_systemLoadAverage 0.89
vendor_jgroups_cluster_unicast3_get_xmit_table_undelivered_messages{node="infinispan-server-18034",} 0.0
vendor_cache_manager_default_local_container_stats_memory_max{node="infinispan-server-18034",} 5.36870912E8
vendor_cache_manager_default_cache_container_stats_time_since_reset{node="infinispan-server-18034",} 399.0
vendor_jgroups_cluster_tcp_get_num_ucast_msgs_received{node="infinispan-server-18034",} 0.0
base_cpu_processCpuLoad 6.451612903225806E-4
vendor_jgroups_cluster_unicast3_get_num_connections{node="infinispan-server-18034",} 4.0
vendor_cache_manager_default_local_container_stats_time_since_reset{node="infinispan-server-18034",} 399.0

〜省略〜

以降は、Prometheus FormatでBasic認証を使ってアクセスすることにします。

スコープはbasevendorの2種類があるということでしたが、この時点では以下のような分布になっています。

$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep -v '#' | perl -wp -e 's!^([^_]+).+!$1!' | sort | uniq -c
     24 base
    148 vendor

少し挙動を確認してみましょう。statisticsfalseにしてInfinispan Serverを再起動。

   <cache-container name="default" statistics="false">

すると、CacheManagerに関するメトリクスが取得できなくなります。

$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep -v '#' | wc -l
127

メトリクス自体が全く取得できなくなるわけではなさそうですね。

では、CacheManagerに関するメトリクスは?というと名前にcache_managercache_containerが入っているものが該当しそうです。

$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep -v '#' | grep cache_manager | grep cache_container
vendor_cache_manager_default_cache_container_stats_number_of_entries{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_health_number_of_nodes{node="infinispan-server-33644",} 3.0
vendor_cache_manager_default_cache_container_health_number_of_cpus{node="infinispan-server-33644",} 8.0
vendor_cache_manager_default_cache_container_stats_average_write_time{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_misses{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_average_read_time{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_off_heap_memory_used{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_hits{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_average_remove_time{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_average_write_time_nanos{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_health_total_memory_kb{node="infinispan-server-33644",} 67584.0
vendor_cache_manager_default_cache_container_stats_remove_misses{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_average_remove_time_nanos{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_time_since_reset{node="infinispan-server-33644",} 20.0
vendor_cache_manager_default_cache_container_stats_current_number_of_entries_in_memory{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_health_free_memory_kb{node="infinispan-server-33644",} 22656.0
vendor_cache_manager_default_cache_container_stats_required_minimum_number_of_nodes{node="infinispan-server-33644",} -1.0
vendor_cache_manager_default_cache_container_stats_remove_hits{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_time_since_start{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_data_memory_used{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_hit_ratio{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_stores{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_evictions{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_read_write_ratio{node="infinispan-server-33644",} 0.0
vendor_cache_manager_default_cache_container_stats_average_read_time_nanos{node="infinispan-server-33644",} 0.0

デフォルトでは取得できるメトリクスはgauseのみです。
※一部、FunctionCounterも混じっているようですが

$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep gauge| head -n 10
# TYPE base_thread_totalStarted gauge
# TYPE vendor_jgroups_cluster_unicast3_get_num_send_connections gauge
# TYPE base_memory_committedNonHeap_bytes gauge
# TYPE vendor_jgroups_cluster_nakack2_get_xmit_table_num_compactions gauge
# TYPE vendor_jgroups_cluster_nakack2_get_non_member_messages gauge
# TYPE vendor_cache_manager_default_cache_container_stats_number_of_entries gauge
# TYPE vendor_cache_manager_default_cache_container_health_number_of_nodes gauge
# TYPE vendor_jgroups_cluster_unicast3_get_xmit_table_num_compactions gauge
# TYPE vendor_jgroups_cluster_red_get_total_messages gauge
# TYPE vendor_jgroups_cluster_mfc_get_number_of_credit_requests_sent gauge

〜省略〜

histogramを有効にする場合は、以下のように設定します。

   <cache-container name="default" statistics="true">
      <metrics gauges="true"
               histograms="true" />

起動しただけだと、histogramによるメトリクスは取得できないのですが…。

Cacheを作成、利用してからメトリクスを取得する

次に、Cacheを作成してからメトリクスを取得してみます。

こちらは、テストコードで行います。まずは以下のような雛形を用意。

src/test/java/org/littlewings/infinispan/remote/metrics/MetricsTest.java

package org.littlewings.infinispan.remote.metrics;

import java.util.stream.IntStream;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.infinispan.client.hotrod.ServerStatistics;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.client.hotrod.jmx.RemoteCacheClientStatisticsMXBean;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class MetricsTest {
    static String createUri(String userName, String password) {
        return String.format("hotrod://%s:%s@172.18.0.2:11222,172.18.0.3:11222,172.18.0.4:11222", userName, password);
    }

    @BeforeAll
    static void setUpAll() {
        String uri = createUri("ispn-admin", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration distCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .statistics().enable()
                            .build();

            admin.getOrCreateCache("distCache", distCacheConfiguration);

            org.infinispan.configuration.cache.Configuration replCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.REPL_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .statistics().enable()
                            .build();

            admin.getOrCreateCache("replCache", replCacheConfiguration);
        }
    }

    // ここに、テストを書く!!
}

Cacheは、テスト実行時に作成することにします。今回はDistributed CacheとReplicated Cacheの2つを作成。

デフォルトではCacheのメトリクスの記録は無効になっているので、明示的に有効化する必要があります。

これでInfinispan Server側に作成されるCacheの定義は、以下のようになります。

server/data/caches.xml

<?xml version="1.0"?>
<infinispan xmlns="urn:infinispan:config:14.0">
    <cache-container>
        <caches>
            <replicated-cache name="replCache" mode="SYNC" remote-timeout="17500" statistics="true">
                <encoding>
                    <key media-type="application/x-protostream"/>
                    <value media-type="application/x-protostream"/>
                </encoding>
                <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
                <state-transfer timeout="60000"/>
            </replicated-cache>
            <distributed-cache name="distCache" mode="SYNC" remote-timeout="17500" statistics="true">
                <encoding>
                    <key media-type="application/x-protostream"/>
                    <value media-type="application/x-protostream"/>
                </encoding>
                <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
                <state-transfer timeout="60000"/>
            </distributed-cache>
        </caches>
    </cache-container>
</infinispan>

これらのCacheを使う、テストコード。

    @Test
    public void distributedCache() {
        String uri = createUri("ispn-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCache<String, String> cache = manager.getCache("distCache");

            IntStream
                    .rangeClosed(1, 100)
                    .forEach(i -> cache.put("key" + i, "value" + i));

            IntStream
                    .rangeClosed(1, 50)
                    .forEach(i -> assertThat(cache.get("key" + i)).isNotNull());

            IntStream
                    .rangeClosed(101, 125)
                    .forEach(i -> assertThat(cache.get("key" + i)).isNull());
        }
    }

    @Test
    public void replicatedCache() {
        String uri = createUri("ispn-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCache<String, String> cache = manager.getCache("replCache");

            IntStream
                    .rangeClosed(1, 100)
                    .forEach(i -> cache.put("key" + i, "value" + i));

            IntStream
                    .rangeClosed(1, 50)
                    .forEach(i -> assertThat(cache.get("key" + i)).isNotNull());

            IntStream
                    .rangeClosed(101, 125)
                    .forEach(i -> assertThat(cache.get("key" + i)).isNull());
        }
    }

これらのテストを実行した後に、メトリクスを取得してみます。

$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep -v '#' | wc -l
346

一気に倍くらいになりました。

vendorスコープのものが増えていますね。

$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep -v '#' | perl
-wp -e 's!^([^_]+).+!$1!' | sort | uniq -c
     24 base
    322 vendor

cache_managerで絞り込んで、ソートしてみます。

$ curl -s -u ispn-user:password 172.18.0.2:11222/metrics | grep -v '#' | grep cache_manager | sort
vendor_cache_manager_default_cache_container_health_free_memory_kb{node="infinispan-server-63198",} 13067.0
vendor_cache_manager_default_cache_container_health_number_of_cpus{node="infinispan-server-63198",} 8.0
vendor_cache_manager_default_cache_container_health_number_of_nodes{node="infinispan-server-63198",} 3.0
vendor_cache_manager_default_cache_container_health_total_memory_kb{node="infinispan-server-63198",} 67584.0
vendor_cache_manager_default_cache_container_stats_average_read_time_nanos{node="infinispan-server-63198",} 55291.0
vendor_cache_manager_default_cache_container_stats_average_read_time{node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_container_stats_average_remove_time_nanos{node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_container_stats_average_remove_time{node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_container_stats_average_write_time_nanos{node="infinispan-server-63198",} 1567124.0
vendor_cache_manager_default_cache_container_stats_average_write_time{node="infinispan-server-63198",} 1.0

〜省略〜

vendor_cache_manager_default_cache_distCache_cluster_cache_stats_activations{cache="distCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_approximate_entries_in_memory{cache="distCache",node="infinispan-server-63198",} 200.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_approximate_entries_unique{cache="distCache",node="infinispan-server-63198",} 100.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_approximate_entries{cache="distCache",node="infinispan-server-63198",} 200.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_read_time_nanos{cache="distCache",node="infinispan-server-63198",} 68779.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_read_time{cache="distCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_remove_time_nanos{cache="distCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_remove_time{cache="distCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_write_time_nanos{cache="distCache",node="infinispan-server-63198",} 1756830.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_write_time{cache="distCache",node="infinispan-server-63198",} 1.0
〜省略〜

vendor_cache_manager_default_cache_replCache_cluster_cache_stats_activations{cache="replCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_approximate_entries_in_memory{cache="replCache",node="infinispan-server-63198",} 300.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_approximate_entries_unique{cache="replCache",node="infinispan-server-63198",} 100.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_approximate_entries{cache="replCache",node="infinispan-server-63198",} 300.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_read_time_nanos{cache="replCache",node="infinispan-server-63198",} 50003.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_read_time{cache="replCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_remove_time_nanos{cache="replCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_remove_time{cache="replCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_write_time_nanos{cache="replCache",node="infinispan-server-63198",} 1553920.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_write_time{cache="replCache",node="infinispan-server-63198",} 1.0

〜省略〜

vendor_cache_manager_default_cluster_container_stats_memory_available{node="infinispan-server-63198",} 3.2327152E7
vendor_cache_manager_default_cluster_container_stats_memory_max{node="infinispan-server-63198",} 1.073741824E9
vendor_cache_manager_default_cluster_container_stats_memory_total{node="infinispan-server-63198",} 1.38412032E8
vendor_cache_manager_default_cluster_container_stats_memory_used{node="infinispan-server-63198",} 1.0608488E8
vendor_cache_manager_default_cluster_container_stats_time_since_reset{node="infinispan-server-63198",} 41190.0
vendor_cache_manager_default_cluster_size{node="infinispan-server-63198",} 3.0
vendor_cache_manager_default_local_container_stats_memory_available{node="infinispan-server-63198",} 1.3747984E7
vendor_cache_manager_default_local_container_stats_memory_max{node="infinispan-server-63198",} 5.36870912E8
vendor_cache_manager_default_local_container_stats_memory_total{node="infinispan-server-63198",} 6.9206016E7
vendor_cache_manager_default_local_container_stats_memory_used{node="infinispan-server-63198",} 5.5458032E7
vendor_cache_manager_default_local_container_stats_time_since_reset{node="infinispan-server-63198",} 130.0
vendor_cache_manager_default_number_of_cache_configurations{node="infinispan-server-63198",} 13.0
vendor_cache_manager_default_number_of_created_caches{node="infinispan-server-63198",} 2.0
vendor_cache_manager_default_number_of_running_caches{node="infinispan-server-63198",} 2.0
vendor_cache_manager_default_server_single_port_11222_transport_number_iothreads{node="infinispan-server-63198",} 16.0
vendor_cache_manager_default_server_single_port_11222_transport_number_of_global_connections{node="infinispan-server-63198",} 1.0
vendor_cache_manager_default_server_single_port_11222_transport_number_of_local_connections{node="infinispan-server-63198",} 1.0
vendor_cache_manager_default_server_single_port_11222_transport_pending_tasks{node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_server_single_port_11222_transport_total_bytes_read{node="infinispan-server-63198",} 7559.0
vendor_cache_manager_default_server_single_port_11222_transport_total_bytes_written{node="infinispan-server-63198",} 645939.0

CacheManagerレベルのものと

vendor_cache_manager_default_cache_container_health_free_memory_kb{node="infinispan-server-63198",} 13067.0
vendor_cache_manager_default_cache_container_health_number_of_cpus{node="infinispan-server-63198",} 8.0
vendor_cache_manager_default_cache_container_health_number_of_nodes{node="infinispan-server-63198",} 3.0
vendor_cache_manager_default_cache_container_health_total_memory_kb{node="infinispan-server-63198",} 67584.0
vendor_cache_manager_default_cache_container_stats_average_read_time_nanos{node="infinispan-server-63198",} 55291.0
vendor_cache_manager_default_cache_container_stats_average_read_time{node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_container_stats_average_remove_time_nanos{node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_container_stats_average_remove_time{node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_container_stats_average_write_time_nanos{node="infinispan-server-63198",} 1567124.0
vendor_cache_manager_default_cache_container_stats_average_write_time{node="infinispan-server-63198",} 1.0

〜省略〜

Cacheレベルのものは、それぞれ名前が入っているのでわかりやすいですね。

vendor_cache_manager_default_cache_distCache_cluster_cache_stats_activations{cache="distCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_approximate_entries_in_memory{cache="distCache",node="infinispan-server-63198",} 200.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_approximate_entries_unique{cache="distCache",node="infinispan-server-63198",} 100.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_approximate_entries{cache="distCache",node="infinispan-server-63198",} 200.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_read_time_nanos{cache="distCache",node="infinispan-server-63198",} 68779.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_read_time{cache="distCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_remove_time_nanos{cache="distCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_remove_time{cache="distCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_write_time_nanos{cache="distCache",node="infinispan-server-63198",} 1756830.0
vendor_cache_manager_default_cache_distCache_cluster_cache_stats_average_write_time{cache="distCache",node="infinispan-server-63198",} 1.0
〜省略〜

vendor_cache_manager_default_cache_replCache_cluster_cache_stats_activations{cache="replCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_approximate_entries_in_memory{cache="replCache",node="infinispan-server-63198",} 300.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_approximate_entries_unique{cache="replCache",node="infinispan-server-63198",} 100.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_approximate_entries{cache="replCache",node="infinispan-server-63198",} 300.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_read_time_nanos{cache="replCache",node="infinispan-server-63198",} 50003.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_read_time{cache="replCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_remove_time_nanos{cache="replCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_remove_time{cache="replCache",node="infinispan-server-63198",} 0.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_write_time_nanos{cache="replCache",node="infinispan-server-63198",} 1553920.0
vendor_cache_manager_default_cache_replCache_cluster_cache_stats_average_write_time{cache="replCache",node="infinispan-server-63198",} 1.0

〜省略〜

なお、CacheManagerの名前は今回はdefaultです。これがメトリクスの名前に反映されています。

   <cache-container name="default" statistics="true">

https://github.com/infinispan/infinispan/blob/14.0.2.Final/core/src/main/java/org/infinispan/metrics/impl/AbstractMetricsRegistration.java#L71-L76

Hot Rod Clientでメトリクスを取得する

次は、Hot Rod Clientでメトリクスを取得してみます。

テストコードはこちら。

    @Test
    public void clientMetrics() {
        String uri = createUri("ispn-user", "password");

        Configuration configuration =
                new ConfigurationBuilder()
                        .uri(uri)
                        .statistics().enable() // .jmxEnable() JMX MBeanも有効にする場合
                        .build();

        try (RemoteCacheManager manager = new RemoteCacheManager(configuration)) {
            RemoteCache<String, String> cache = manager.getCache("distCache");

            IntStream
                    .rangeClosed(1, 100)
                    .forEach(i -> cache.put("key" + i, "value" + i));

            IntStream
                    .rangeClosed(1, 50)
                    .forEach(i -> assertThat(cache.get("key" + i)).isNotNull());

            IntStream
                    .rangeClosed(101, 125)
                    .forEach(i -> assertThat(cache.get("key" + i)).isNull());

            ServerStatistics serverStatistics = cache.serverStatistics();

            serverStatistics
                    .getStatsMap()
                    .entrySet().forEach(entry -> System.out.printf("%s = %s%n", entry.getKey(), entry.getValue()));

            RemoteCacheClientStatisticsMXBean clientStatistics = cache.clientStatistics();
            assertThat(clientStatistics.getRemoteStores()).isEqualTo(100);
            assertThat(clientStatistics.getRemoteHits()).isEqualTo(50);
            assertThat(clientStatistics.getRemoteMisses()).isEqualTo(25);
        }
    }

今回は、メトリクスのみ有効にしました。JMX MBeanでアクセスしたい場合は、jmxEnableも呼び出してください。

標準出力に書き出している部分の値は、こんな感じになります。

currentNumberOfEntries = -1
globalRemoveHits = 0
removeMisses = 0
approximateEntries = 76
approximateEntriesUnique = 45
globalRetrievals = 150
stores = 90
retrievals = 58
globalHits = 100
globalRemoveMisses = 0
hits = 40
removeHits = 0
timeSinceStart = 1
globalCurrentNumberOfEntries = -1
totalNumberOfEntries = 90
misses = 18
globalMisses = 50
globalApproximateEntriesUnique = 100
globalApproximateEntries = 200
globalStores = 200

これで、Hot Rod Client側も確認できました。

Prometheusでメトリクスを取得する

最後に、PrometheusでInfinispan Serverからメトリクスを取得してみます。

Prometheusの設定は、このようにしました。

prometheus.yml

global:
  scrape_interval: 5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: "infinispan-server"
    static_configs:
      - targets:
        - 172.18.0.2:11222
        - 172.18.0.3:11222
        - 172.18.0.4:11222
    basic_auth:
      username: ispn-user
      password: password
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]

Infinispan Serverの各ノードを指定しつつ、Basic認証も設定。

Prometheusを起動したら、Web UIで確認。

Targetとして認識していることを確認。

メトリクスも認識できています。

OKですね。

ソースコードを見てみる

気になる範囲で、ソースコードを見てみます。Infinispan 13.0の時にまとめたものですが、モジュールが移動していたり

JMX MBeanに関する話

メトリクスに関するインターフェースやクラスはこちら。

https://github.com/infinispan/infinispan/tree/14.0.2.Final/core/src/main/java/org/infinispan/stats

https://github.com/infinispan/infinispan/tree/14.0.2.Final/core/src/main/java/org/infinispan/stats/impl

ドキュメントとしては、こちらですね。

JMX Components

JMX MBeanの属性や操作は、以下のアノテーションで指定されているものが対象のようです。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-annotations/src/main/java/org/infinispan/jmx/annotations/ManagedAttribute.java

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-annotations/src/main/java/org/infinispan/jmx/annotations/ManagedOperation.java

これを、ビルド時にPluggable Annotation Processing APIで処理してソースコードを生成します。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-processor/src/main/java/org/infinispan/component/processor/ComponentAnnotationProcessor.java#L384-L407

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-processor/src/main/java/org/infinispan/component/processor/ComponentAnnotationProcessor.java#L409-L424

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-processor/src/main/java/org/infinispan/component/processor/Generator.java

というのが、JMX MBeanに関する話。

Micrometerで取得できるメトリクス

Infinispanのドキュメントでは、JMX MBeanとして扱える対象は以下ということになっています。

JMX Components

どういうことかというと。

メトリクスを登録している箇所は、以下になります。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/core/src/main/java/org/infinispan/metrics/impl/MetricsCollector.java#L140-L179

gauseとhistogramのみを対象にしていることがわかります。

これには、@ManagedAttributeアノテーションdataType属性が関係します。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-annotations/src/main/java/org/infinispan/jmx/annotations/ManagedAttribute.java#L44

gaugeに対応するものはMEASUREMENTで、かつ整数・小数のものみたいです。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-annotations/src/main/java/org/infinispan/jmx/annotations/DataType.java#L15-L20

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-processor/src/main/java/org/infinispan/component/processor/Generator.java#L263-L274

histogramはHISTOGRAMおよびTIMERが対応します。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-processor/src/main/java/org/infinispan/component/processor/Generator.java#L276-L283

https://github.com/infinispan/infinispan/blob/14.0.2.Final/build/component-annotations/src/main/java/org/infinispan/jmx/annotations/DataType.java#L22-L30

使われているのは、TIMERのみでここくらいのようですけどね。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheMgmtInterceptor.java#L107-L125

JMX MBeanよりも取得できるメトリクスが少ないことはわかるのですが、実際どのようなメトリクスが取得できるかは実際に動かして
確認した方が早そうですね。

ちなみに、vendorという文字列はここで付与しているようです。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/core/src/main/java/org/infinispan/metrics/impl/MetricsCollector.java#L145

baseスコープのメトリクス

baseスコープのメトリクスは、こちらで登録しているようです。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/core/src/main/java/org/infinispan/metrics/impl/BaseAdditionalMetrics.java

https://github.com/infinispan/infinispan/blob/14.0.2.Final/core/src/main/java/org/infinispan/metrics/impl/BaseMemoryAdditionalMetrics.java

https://github.com/infinispan/infinispan/blob/14.0.2.Final/core/src/main/java/org/infinispan/metrics/impl/BaseOperatingSystemAdditionalMetrics.java

vendor分も一部追加であるようですけどね。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/core/src/main/java/org/infinispan/metrics/impl/VendorAdditionalMetrics.java

このあたりは、SmallRye Metricsを使っていた時にはなかった部分ですね。

Micrometerのエンドポイント

Micrometerを使った、メトリクスを返却するエンドポイントはこちら。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/server/rest/src/main/java/org/infinispan/rest/resources/MetricsResource.java

Content-TypeでPrometheus FormatとOpenMetrics Formatの出し分けを行っているのは、こちらのようです。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/server/rest/src/main/java/org/infinispan/rest/resources/MetricsResource.java#L55-L57

Hot Rod Clientのメトリクスに関するインターフェースとクラス

Hot Rod Clientのメトリクスに関するインターフェースとクラスの組み合わせは、こちら。

インターフェース。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/jmx/RemoteCacheClientStatisticsMXBean.java

https://github.com/infinispan/infinispan/blob/14.0.2.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/jmx/RemoteCacheManagerMXBean.java

https://github.com/infinispan/infinispan/blob/14.0.2.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/ServerStatistics.java

実装。

https://github.com/infinispan/infinispan/blob/14.0.2.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/ClientStatistics.java

https://github.com/infinispan/infinispan/blob/14.0.2.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/RemoteCacheManager.java

https://github.com/infinispan/infinispan/blob/14.0.2.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/ServerStatisticsImpl.java

Hot Rod Clientのメトリクスに関する、これ以上の情報はなさそうです。

まとめ

メトリクス取得の仕組みがMicrometerになった、Infinispan Serverのメトリクス取得を試してみました。Hot Rod Clientはオマケですが。

メトリクス名とか変わったのかなと思ったのですが、それほど変更はなさそうでした。

ソースコードもどう変わったか見直せましたし、今回はこんなところでしょう。

今回作成したソースコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-cache-metrics-micrometer

Serverless Esbuildを使って、TypeScriptのServerless Frameworkサービスを作る

これは、なにをしたくて書いたもの?

前に、Serverless FrameworkをTypeScript(とLocalStack)で使ってみました。

Serverless FrameworkをLocalStack+TypeScriptで使ってみる(Amazon API Gateway+AWS Lambda) - CLOVER🍀

この時、テンプレートにaws-nodejs-typescriptを使用したのですが、Serverless Frameworkの設定ファイルがTypeScriptのファイルで
できるのが微妙に思い、ちょっと他の方法を探してみることにしました。

TypeScriptで書かれていても読めないことはないのですが、リファレンスはYAMLで書かれていますからね。

Serverless Framework - AWS Lambda Guide - Serverless.yml Reference

合わせていた方がいいのかな、と。

結論を書くと、JavaScript向けに作成したサービスにServerless Esbuildをプラグインとして追加する方法で良いかなと思います。

Serverless Plugin TypeScript

Serverless FrameworkとTypeScriptで検索すると、割と最初の方に見つかるのがServerss Plugin TypeScriptな気がします。

Serverless Framework: Plugins

これは、Prisma Labsが作成して現在はServerlss Incがメンテナンスしているようです。

Originally developed by Prisma Labs, now maintained in scope of Serverless, Inc

なのですが、実際に使ってみるとエラーになります。

Error:
Cannot access package artifact at ".esbuild/.serverless/hello.zip" (for "hello"): ENOENT: no such file or directory, access '/path/to/.build/.esbuild/.serverless/hello.zip'

こちらでも動作しないという話題が挙がっていて、今回はスルーすることにしました。

Serverless compose not working with typescript · Discussion #11218 · serverless/serverless · GitHub

Serverless Esbuild

今回使うのは、Serverless Esbuildです。

Serverless Framework: Plugins

文字通り、esbuildでトランスパイルしてくれるプラグインです。前のエントリーでaws-nodejs-typescriptテンプレートでサービスを
作成した時にも、実は含まれていたものになります。

Serverless FrameworkをLocalStack+TypeScriptで使ってみる(Amazon API Gateway+AWS Lambda) - CLOVER🍀

今回は、こちらを使ってAmazon API GatewayAWS Lambda関数を、LocalStackにデプロイするシナリオで試してみます。

環境

今回の環境は、こちら。

$ python3 -V
Python 3.8.10


$ localstack --version
1.2.0

LocalStackを起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

Node.jsのバージョン。

$ node --version
v16.18.1


$ npm --version
8.19.2

最後に動作確認でAWS CLIも使います。

$ aws --version
aws-cli/2.9.1 Python/3.9.11 Linux/5.4.0-132-generic exe/x86_64.ubuntu.20 prompt/off


$ awslocal --version
aws-cli/2.9.1 Python/3.9.11 Linux/5.4.0-132-generic exe/x86_64.ubuntu.20 prompt/off

Serverless Frameworkのサービスを作成する

まずは、Serverless Frameworkのサービスを作成しましょう。

Serverless Framework Commands - AWS Lambda - Create

Serverless Frameworkをインストール。

$ npm i -D serverless


$ npx serverless --version
Framework Core: 3.25.0 (local)
Plugin: 6.2.2
SDK: 4.3.2

aws-nodejsテンプレートを使用して、サービスを作成します。

$ npx serverless create --template aws-nodejs

作成されたファイル。

$ ll
合計 340
drwxrwxr-x   3 xxxxx xxxxx   4096 11月 26 20:14 ./
drwxrwxr-x  24 xxxxx xxxxx   4096 11月 26 20:11 ../
-rw-r--r--   1 xxxxx xxxxx     86 10月 12 11:50 .gitignore
-rw-r--r--   1 xxxxx xxxxx    444 10月 12 11:50 handler.js
drwxrwxr-x 346 xxxxx xxxxx  12288 11月 26 20:13 node_modules/
-rw-rw-r--   1 xxxxx xxxxx 309469 11月 26 20:14 package-lock.json
-rw-rw-r--   1 xxxxx xxxxx    105 11月 26 20:14 package.json
-rw-r--r--   1 xxxxx xxxxx   3282 11月 26 20:14 serverless.yml

主要なファイルの中身を見てみましょう。

serverless.yml

# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: serverless-plugin-esbuild-example
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs12.x

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# you can add statements to the Lambda function's IAM Role here
#  iam:
#    role:
#      statements:
#        - Effect: "Allow"
#          Action:
#            - "s3:ListBucket"
#          Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#        - Effect: "Allow"
#          Action:
#            - "s3:PutObject"
#          Resource:
#            Fn::Join:
#              - ""
#              - - "arn:aws:s3:::"
#                - "Ref" : "ServerlessDeploymentBucket"
#                - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

# you can add packaging information here
#package:
#  patterns:
#    - '!exclude-me.js'
#    - '!exclude-me-dir/**'
#    - include-me.js
#    - include-me-dir/**

functions:
  hello:
    handler: handler.hello
#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - httpApi:
#          path: /users/create
#          method: get
#      - websocket: $connect
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
#      - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp
#      - alb:
#          listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/
#          priority: 1
#          conditions:
#            host: example.com
#            path: /hello

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"

handler.js

'use strict';

module.exports.hello = async (event) => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};

ここから、TypeScriptに変更しつつ、Serverless Esbuildを導入していきます。

TypeScriptとServerless Esbuildの導入

TypeScriptや、Node.jsとAWS Lambdaに関する型定義を追加。

$ npm i -D typescript
$ npm i -D prettier
$ npm i -D @types/node@v16 @types/aws-lambda

Serverless Esbuildとesbuild、LocalStack Serverless Pluginを追加。

$ npm i -D serverless-esbuild esbuild
$ npm i -D serverless-localstack

Serverless Esbuild / Install

GitHub - localstack/serverless-localstack: ⚡ Serverless plugin for running against LocalStack

この時の依存関係は、このようになりました。

  "devDependencies": {
    "@types/aws-lambda": "^8.10.109",
    "@types/node": "^16.18.3",
    "esbuild": "^0.15.15",
    "prettier": "^2.8.0",
    "serverless": "^3.25.0",
    "serverless-esbuild": "^1.33.2",
    "serverless-localstack": "^1.0.1",
    "typescript": "^4.9.3"
  },

package.jsonscriptsは、このようにしておきました。

  "scripts": {
    "typecheck": "tsc --project .",
    "typecheck:watch": "tsc --project . --watch",
    "format": "prettier --write ./**/*.{js,ts}"
  }

TypeScriptの設定。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "noEmit": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": ["./**/*.ts"],
  "exclude": [
    "node_modules/**/*",
    ".serverless/**/*"
  ]
}

Prettierの設定。

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

ソースコードは、TypeScriptで書き直しました。

handler.ts

import { APIGatewayEvent, APIGatewayProxyResult, ProxyResult } from 'aws-lambda';

export const hello = async (event: APIGatewayEvent): Promise<ProxyResult> => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };
};

もともと生成されていたJavaScriptソースコードは削除。

$ rm handler.js

serverless.ymlは、以下のように修正。

serverless.yml

service: serverless-plugin-esbuild-example

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs16.x
  stage: dev
  region: us-east-1

package:
  individually: true

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: /hello
          method: get

custom:
  esbuild:
    bundle: true
    minify: false
    target: node16
    platform: node

plugins:
  - serverless-esbuild
  - serverless-localstack

少し、説明を加えておきます。

今回利用するプラグイン

plugins:
  - serverless-esbuild
  - serverless-localstack

AWS Lambda関数の定義。httpをイベントとして受け取るようにしたので、Amazon API Gatewayと連携します。

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: /hello
          method: get

以下は、AWS Lambda関数を個別にパッケージングしてデプロイする設定です。今回はAWS Lambda関数はひとつだけなんですが。

package:
  individually: true

Packaging / Package Configuration / Packaging functions separately

あとは、esbuildの設定です。

custom:
  esbuild:
    bundle: true
    minify: false
    target: node16
    platform: node

こちらは、Serverless Esbuildのドキュメントにある程度記載があります。

Serverless Esbuild / Configuration / Options

ただ、最終的にはesbuildのドキュメントを見ることになるのではないかな、と思います。

esbuild - API

動作確認

準備ができたので、LocalStackにデプロイしましょう。

$ npx serverless deploy

デプロイが終わりました。

Using serverless-localstack

Deploying serverless-plugin-esbuild-example to stage dev (us-east-1)
Skipping template validation: Unsupported in Localstack

✔ Service deployed to stack serverless-plugin-esbuild-example-dev (13s)

endpoint: http://localhost:4566/restapis/6t31uk9dr3/dev/_user_request_
functions:
  hello: serverless-plugin-esbuild-example-dev-hello (744 B)

Need a better logging experience than CloudWatch? Try our Dev Mode in console: run "serverless --console"

エンドポイントの情報は、出力されていますね。

endpoint: http://localhost:4566/restapis/6t31uk9dr3/dev/_user_request_

Amazon API GatewayREST APIのIDは、コマンドで取得した方が汎用的かなという気がするので、こちらで取得。

$ REST_API_ID=$(awslocal apigateway get-rest-apis --query 'reverse(sort_by(items[], &createdDate))[0].id' --output text)

確認。

$ curl -H 'Content-Type: application/json' http://localhost:4566/restapis/$REST_API_ID/dev/_user_request_/hello
{
  "message": "Go Serverless v1.0! Your function executed successfully!",
  "input": {
    "path": "/hello",
    "headers": {
      "Host": "localhost:4566",
      "User-Agent": "curl/7.68.0",
      "accept": "*/*",
      "Content-Type": "application/json",
      "x-localstack-tgt-api": "apigateway",
      "X-Forwarded-For": "172.17.0.1, localhost:4566",
      "x-localstack-edge": "http://localhost:4566"
    },
    "multiValueHeaders": {
      "Host": [
        "localhost:4566"
      ],
      "User-Agent": [
        "curl/7.68.0"
      ],
      "accept": [
        "*/*"
      ],
      "Content-Type": [
        "application/json"
      ],
      "x-localstack-tgt-api": [
        "apigateway"
      ],
      "X-Forwarded-For": [
        "172.17.0.1, localhost:4566"
      ],
      "x-localstack-edge": [
        "http://localhost:4566"
      ]
    },
    "body": "",
    "isBase64Encoded": false,
    "httpMethod": "GET",
    "queryStringParameters": null,
    "multiValueQueryStringParameters": null,
    "pathParameters": {},
    "resource": "/hello",
    "requestContext": {
      "accountId": "000000000000",
      "apiId": "6t31uk9dr3",
      "resourcePath": "/hello",
      "domainPrefix": "localhost",
      "domainName": "localhost",
      "resourceId": "wyvlcgsfpz",
      "requestId": "88c77cbd-55ac-45bb-a531-c667f29cab02",
      "identity": {
        "accountId": "000000000000",
        "sourceIp": "172.17.0.1",
        "userAgent": "curl/7.68.0"
      },
      "httpMethod": "GET",
      "protocol": "HTTP/1.1",
      "requestTime": "26/Nov/2022:12:50:59 +0000",
      "requestTimeEpoch": 1669467059223,
      "authorizer": {},
      "path": "/dev/hello",
      "stage": "dev"
    },
    "stageVariables": {}
  }
}

OKですね。

まとめ

Serverless Esbuildを使って、TypeScriptのServerless Frameworkサービスを作ってみました。

そもそもの目的はserverless.tsではなく、serverless.ymlで設定を行いたい、だったのですが。aws-nodejs-typescriptテンプレートを
使わずにServerless FrameworkでTypeScriptを扱うにはどうすればよいのか、なかなか調べるのに苦労しました。

落ち着き先を振り返るとふつうの結果な気がしますが、自分の情報整理という点では意味があったかなと思います。