Observability Dev Services と Grafana OTel LGTM
この Dev Service は、 Grafana OTel-LGTM を提供します。これは、テレメトリーデータを受信して Prometheus (メトリクス)、Tempo (トレース)、および Loki (ログ) に転送する OpenTelemetry Collector を含む all-in-one
Docker イメージです。
このデータは、 Grafana で視覚化できます。LGTM という略語は次の意味です。
プロジェクトの設定
Quarkus Grafana OTel LGTM シンク (データが送信される場所) エクステンションをビルドファイルに追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-observability-devservices-lgtm</artifactId>
<scope>provided</scope>
</dependency>
implementation("io.quarkus:quarkus-observability-devservices-lgtm")
Micrometer
Micrometer Quarkus エクステンション は、Quarkus とそのエクステンションに実装された自動計装からのメトリクスを提供します。
Micrometer メトリクスを出力する方法は複数あります。次にいくつか例を示します。
Micrometer Prometheus レジストリーの使用
これは、Micrometer からメトリクスを出力する最も一般的な方法であり、Quarkus のデフォルトの方法です。Micrometer Prometheus レジストリーは、 /q/metrics
エンドポイントでデータを公開し、Grafana LGTM Dev Service 内のスクレイパーがデータを取得します (サービスからデータを プル します)。
<dependency>
<groupId>io.quarkiverse.micrometer.registry</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
implementation("io.quarkiverse.micrometer.registry:quarkus-micrometer-registry-prometheus")
Micrometer OTLP レジストリーの使用
Quarkiverse Micrometer OTLP レジストリー は、OpenTelemetry OTLP プロトコルを使用してデータを Grafana LGTM Dev Service に出力します。これにより、データがサービスから プッシュ されます。
<dependency>
<groupId>io.quarkiverse.micrometer.registry</groupId>
<artifactId>quarkus-micrometer-registry-otlp</artifactId>
</dependency>
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
エクステンションは、次のようにビルドファイルに追加できます。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
implementation("io.quarkus:quarkus-opentelemetry")
quarkus.otel.exporter.otlp.endpoint
プロパティーは、Docker コンテナーの外部から見える OTel コレクターエンドポイントに自動的に設定されます。
quarkus.otel.exporter.otlp.protocol
は http/protobuf
に設定されています。
Micrometer から OpenTelemetry へのブリッジ
このエクステンションは、Micrometer メトリクスと OpenTelemetry メトリクス、トレース、およびログを提供します。データはすべて OpenTelemetry エクステンションによって管理され、送信されます。
すべてのシグナルがデフォルトで有効になります。
エクステンションは次のようにビルドファイルに追加できます。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-opentelemetry</artifactId>
</dependency>
implementation("io.quarkus:quarkus-micrometer-opentelemetry")
Grafana
Grafana UI へのアクセス
アプリケーションを開発モードで起動します。
quarkus dev
./mvnw quarkus:dev
./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 インスタンスに直接新しいブラウザータブが開きます。

探索
Explore セクションでは、すべてのデータソースのデータをクエリーできます。
トレースを表示するには、 tempo
データソースを選択し、データをクエリーします。

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

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

各ダッシュボードは、特定のアプリケーション設定に合わせて調整されています。使用可能なダッシュボードは次のとおりです。
-
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 レジストリーの使用
<dependency>
<groupId>io.quarkiverse.micrometer.registry</groupId>
<artifactId>quarkus-micrometer-registry-otlp</artifactId>
</dependency>
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();
}
}