The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
このページを編集

Observability Dev Services と Grafana OTel LGTM

この Dev Service は、 Grafana OTel-LGTM を提供します。これは、テレメトリーデータを受信して Prometheus (メトリクス)、Tempo (トレース)、および Loki (ログ) に転送する OpenTelemetry Collector を含む all-in-one Docker イメージです。 このデータは、 Grafana で視覚化できます。LGTM という略語は次の意味です。

  • L → Loki (ログ)

  • G → Grafana (メトリクスの可視化)

  • T → Tempo (トレース)

  • M → Mimir (Prometheus の長期保存)

プロジェクトの設定

Quarkus Grafana OTel LGTM シンク (データが送信される場所) エクステンションをビルドファイルに追加します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-observability-devservices-lgtm</artifactId>
    <scope>provided</scope>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-observability-devservices-lgtm")

Micrometer

Micrometer Quarkus エクステンション は、Quarkus とそのエクステンションに実装された自動計装からのメトリクスを提供します。

Micrometer メトリクスを出力する方法は複数あります。次にいくつか例を示します。

Micrometer Prometheus レジストリーの使用

これは、Micrometer からメトリクスを出力する最も一般的な方法であり、Quarkus のデフォルトの方法です。Micrometer Prometheus レジストリーは、 /q/metrics エンドポイントでデータを公開し、Grafana LGTM Dev Service 内のスクレイパーがデータを取得します (サービスからデータを プル します)。

pom.xml
<dependency>
    <groupId>io.quarkiverse.micrometer.registry</groupId>
    <artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
build.gradle
implementation("io.quarkiverse.micrometer.registry:quarkus-micrometer-registry-prometheus")

Micrometer OTLP レジストリーの使用

Quarkiverse Micrometer OTLP レジストリー は、OpenTelemetry OTLP プロトコルを使用してデータを Grafana LGTM Dev Service に出力します。これにより、データがサービスから プッシュ されます。

pom.xml
<dependency>
    <groupId>io.quarkiverse.micrometer.registry</groupId>
    <artifactId>quarkus-micrometer-registry-otlp</artifactId>
</dependency>
build.gradle
implementation("io.quarkiverse.micrometer.registry:quarkus-micrometer-registry-otlp")

Micrometer の Quarkiverse OTLP レジストリーを使用してメトリクスを Grafana OTel LGTM にプッシュする場合、Docker コンテナーの外部から見える OTel コレクターエンドポイントに quarkus.micrometer.export.otlp.url プロパティーが自動的に設定されます。

OpenTelemetry

OpenTelemetry を使用すると、メトリクス、トレース、ログを作成し、Grafana LGTM Dev Service に送信できます。

デフォルトでは、 OpenTelemetry エクステンショントレース を生成します。 メトリクスログ は個別に有効にする必要があります。

quarkus-opentelemetry エクステンションは、次のようにビルドファイルに追加できます。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-opentelemetry</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-opentelemetry")

quarkus.otel.exporter.otlp.endpoint プロパティーは、Docker コンテナーの外部から見える OTel コレクターエンドポイントに自動的に設定されます。

quarkus.otel.exporter.otlp.protocolhttp/protobuf に設定されています。

Micrometer から OpenTelemetry へのブリッジ

このエクステンションは、Micrometer メトリクスと OpenTelemetry メトリクス、トレース、およびログを提供します。データはすべて OpenTelemetry エクステンションによって管理され、送信されます。

すべてのシグナルがデフォルトで有効になります。

エクステンションは次のようにビルドファイルに追加できます。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-micrometer-opentelemetry</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-micrometer-opentelemetry")

Grafana

Grafana UI へのアクセス

アプリケーションを開発モードで起動します。

コマンドラインインタフェース
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

次のようなログエントリーが表示されます。

[io.qu.ob.de.ObservabilityDevServiceProcessor] (build-35) Dev Service Lgtm started, config: {grafana.endpoint=http://localhost:42797, quarkus.otel.exporter.otlp.endpoint=http://localhost:34711, otel-collector.url=localhost:34711, quarkus.micrometer.export.otlp.url=http://localhost:34711/v1/metrics, quarkus.otel.exporter.otlp.protocol=http/protobuf}

Grafana はエフェメラルポートでアクセス可能であるため、どのポートが使用されているかを確認するにはログを確認する必要があります。この例では、Grafana エンドポイントは grafana.endpoint=http://localhost:42797 です。

もう 1 つの方法は、Dev UI (http://localhost:8080/q/dev-ui/extensions) を使用することです。Grafana URL リンクが使用可能になり、選択すると、実行中の Grafana インスタンスに直接新しいブラウザータブが開きます。

Dev UI LGTM

探索

Explore セクションでは、すべてのデータソースのデータをクエリーできます。

トレースを表示するには、 tempo データソースを選択し、データをクエリーします。

Dev UI LGTM

ログの場合は、 loki データソースを選択し、データをクエリーします。

Dev UI LGTM

ダッシュボード

Dev Service にはダッシュボードのセットが含まれています。

Dev UI LGTM

各ダッシュボードは、特定のアプリケーション設定に合わせて調整されています。使用可能なダッシュボードは次のとおりです。

  • Quarkus Micrometer OpenTelemetry: Micrometer および OpenTelemetry エクステンションで使用されます。

  • Quarkus Micrometer OTLP registry: Micrometer OTLP レジストリーエクステンションで使用されます。

  • Quarkus Micrometer Prometheus registry: Micrometer Prometheus レジストリーエクステンションで使用されます。

  • Quarkus OpenTelemetry Logging: OpenTelemetry エクステンションからのログを表示します。

ダッシュボードの一部のパネルでは、値がスライディングタイムウィンドウにわたって計算される場合、正確なデータが表示されるまでに数分かかることがあります。

追加設定

このエクステンションは、Grafana OTel LGTM イメージにバンドルされている OTel Collector にデータを送信するために、 quarkus-opentelemetry および quarkus-micrometer-registry-otlp エクステンションを設定します。

Dev Services に関する面倒な作業 (既存の実行中のコンテナーの検索や再利用など) を避ける場合は、Dev Services を無効にして、Dev Resource の使用のみを有効にすることができます。

quarkus.observability.enabled=false
quarkus.observability.dev-resources=true

テスト

テストにおいて '魔法のような自動処理' の使用を最小限に抑えるには、両方を無効します (Dev Resources はデフォルトですでに無効になっています)。

quarkus.observability.enabled=false

そして、テスト内の LGTM Dev Resource を @QuarkusTestResource リソースとして明示的にリストします。

@QuarkusTest
@QuarkusTestResource(value = LgtmResource.class, restrictToAnnotatedClass = true)
@TestProfile(QuarkusTestResourceTestProfile.class)
public class LgtmLifecycleTest extends LgtmTestBase {
}

完全な Grafana OTel LGTM スタックのテスト - 例

既存の Quarkus MicroMeter OTLP レジストリーの使用

pom.xml
<dependency>
    <groupId>io.quarkiverse.micrometer.registry</groupId>
    <artifactId>quarkus-micrometer-registry-otlp</artifactId>
</dependency>
build.gradle
implementation("io.quarkiverse.micrometer.registry:quarkus-micrometer-registry-otlp")

Meter レジストリーをコードに注入するだけで、メトリクスが Grafana LGTM の OTLP HTTP エンドポイントに定期的にプッシュされるようになります。

@Path("/api")
public class SimpleEndpoint {
    private static final Logger log = Logger.getLogger(SimpleEndpoint.class);

    @Inject
    MeterRegistry registry;

    @PostConstruct
    public void start() {
        Gauge.builder("xvalue", arr, a -> arr[0])
                .baseUnit("X")
                .description("Some random x")
                .tag("my_key", "x")
                .register(registry);
    }

    // ...
}

その後、Grafana のデータソース API で既存のメトリクスデータを確認できます。

public class LgtmTestBase {

    @ConfigProperty(name = "grafana.endpoint")
    String endpoint; // NOTE -- injected Grafana endpoint!

    @Test
    public void testTracing() {
        String response = RestAssured.get("/api/poke?f=100").body().asString();
        System.out.println(response);
        GrafanaClient client = new GrafanaClient(endpoint, "admin", "admin");
        Awaitility.await().atMost(61, TimeUnit.SECONDS).until(
                client::user,
                u -> "admin".equals(u.login));
        Awaitility.await().atMost(61, TimeUnit.SECONDS).until(
                () -> client.query("xvalue_X"),
                result -> !result.data.result.isEmpty());
    }

}

// simple Grafana HTTP client

public class GrafanaClient {
    private static final ObjectMapper MAPPER = new ObjectMapper();

    private final String url;
    private final String username;
    private final String password;

    public GrafanaClient(String url, String username, String password) {
        this.url = url;
        this.username = username;
        this.password = password;
    }

    private <T> void handle(
            String path,
            Function<HttpRequest.Builder, HttpRequest.Builder> method,
            HttpResponse.BodyHandler<T> bodyHandler,
            BiConsumer<HttpResponse<T>, T> consumer) {
        try {
            String credentials = username + ":" + password;
            String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());

            HttpClient httpClient = HttpClient.newHttpClient();
            HttpRequest.Builder builder = HttpRequest.newBuilder()
                    .uri(URI.create(url + path))
                    .header("Authorization", "Basic " + encodedCredentials);
            HttpRequest request = method.apply(builder).build();

            HttpResponse<T> response = httpClient.send(request, bodyHandler);
            int code = response.statusCode();
            if (code < 200 || code > 299) {
                throw new IllegalStateException("Bad response: " + code + " >> " + response.body());
            }
            consumer.accept(response, response.body());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }

    public User user() {
        AtomicReference<User> ref = new AtomicReference<>();
        handle(
                "/api/user",
                HttpRequest.Builder::GET,
                HttpResponse.BodyHandlers.ofString(),
                (r, b) -> {
                    try {
                        User user = MAPPER.readValue(b, User.class);
                        ref.set(user);
                    } catch (JsonProcessingException e) {
                        throw new UncheckedIOException(e);
                    }
                });
        return ref.get();
    }

    public QueryResult query(String query) {
        AtomicReference<QueryResult> ref = new AtomicReference<>();
        handle(
                "/api/datasources/proxy/1/api/v1/query?query=" + query,
                HttpRequest.Builder::GET,
                HttpResponse.BodyHandlers.ofString(),
                (r, b) -> {
                    try {
                        QueryResult result = MAPPER.readValue(b, QueryResult.class);
                        ref.set(result);
                    } catch (JsonProcessingException e) {
                        throw new UncheckedIOException(e);
                    }
                });
        return ref.get();
    }
}

関連コンテンツ