具有 Grafana OTel LGTM 的可观察性 Dev Services
此开发服务提供 Grafana OTel-LGTM,这是一个 all-in-one
Docker 镜像,包含一个 OpenTelemetry Collector,用于接收并将遥测数据转发到 Prometheus(指标)、Tempo(追踪)和 Loki(日志)。然后可以通过 Grafana 可视化这些数据。LGTM 缩写代表
配置您的项目
将 Quarkus Grafana OTel LGTM sink(数据去向)扩展添加到您的构建文件
<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 开发服务中的抓取器将抓取它(从服务中拉取数据)。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
implementation("io.quarkus:quarkus-micrometer-registry-prometheus")
使用 Micrometer OTLP 注册表
Quarkiverse Micrometer OTLP 注册表将使用 OpenTelemetry OTLP 协议将数据输出到 Grafana LGTM 开发服务。这将把数据推送到服务之外
<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 时,quarkus.micrometer.export.otlp.url
属性会自动设置为从 Docker 容器外部看到的 OTel 收集器端点。
OpenTelemetry
使用 OpenTelemetry,可以创建指标、追踪和日志并将其发送到 Grafana LGTM 开发服务。
默认情况下,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=https://:42797, quarkus.otel.exporter.otlp.endpoint=https://:34711, otel-collector.url=localhost:34711, quarkus.micrometer.export.otlp.url=https://:34711/v1/metrics, quarkus.otel.exporter.otlp.protocol=http/protobuf}
请记住,Grafana 可通过临时端口访问,因此您需要检查日志以查看正在使用哪个端口。在此示例中,grafana 端点为 grafana.endpoint=https://:42797
。
另一种选择是使用 Dev UI (https://:8080/q/dev-ui/extensions),因为 Grafana URL 链接将可用,如果选择它,它将直接在新浏览器选项卡中打开正在运行的 Grafana 实例

仪表板
开发服务包括一组仪表板。

每个仪表板都针对特定的应用程序设置进行了调整。可用的仪表板是
-
Quarkus Micrometer OpenTelemetry:与 Micrometer 和 OpenTelemetry 扩展一起使用。
-
Quarkus Micrometer OTLP registry:与 Micrometer OTLP 注册表扩展一起使用。
-
Quarkus Micrometer Prometheus registry:与 Micrometer Prometheus 注册表扩展一起使用。
-
Quarkus OpenTelemetry Logging:用于查看来自 OpenTelemetry 扩展的日志。
当仪表板中的某些面板的值是在滑动时间窗口内计算时,可能需要几分钟才能显示准确的数据。 |
其他配置
此扩展将配置您的 quarkus-opentelemetry
和 quarkus-micrometer-registry-otlp
扩展以将数据发送到与 Grafana OTel LGTM 镜像捆绑在一起的 OTel Collector。
如果您不希望所有使用开发服务的麻烦(例如,查找和重用现有的运行容器等),您可以简单地禁用开发服务,而只启用开发资源使用
quarkus.observability.enabled=false
quarkus.observability.dev-resources=true
测试
对于测试中最小的“自动魔法”用法,只需禁用两者(默认情况下已禁用开发资源)
quarkus.observability.enabled=false
然后,在测试中将 LGTM 开发资源显式列为 @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();
}
}