编辑此页面

具有 Grafana OTel LGTM 的可观察性 Dev Services

此开发服务提供 Grafana OTel-LGTM,这是一个 all-in-one Docker 镜像,包含一个 OpenTelemetry Collector,用于接收并将遥测数据转发到 Prometheus(指标)、Tempo(追踪)和 Loki(日志)。然后可以通过 Grafana 可视化这些数据。LGTM 缩写代表

  • L → Loki(日志)

  • G → Grafana(指标可视化)

  • T → Tempo(追踪)

  • M → Mimir(Prometheus 的长期存储)

配置您的项目

将 Quarkus Grafana OTel LGTM sink(数据去向)扩展添加到您的构建文件

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 开发服务中的抓取器将抓取它(从服务中拉取数据)。

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

使用 Micrometer OTLP 注册表

Quarkiverse Micrometer OTLP 注册表将使用 OpenTelemetry OTLP 协议将数据输出到 Grafana LGTM 开发服务。这将把数据推送到服务之外

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 时,quarkus.micrometer.export.otlp.url 属性会自动设置为从 Docker 容器外部看到的 OTel 收集器端点。

OpenTelemetry

使用 OpenTelemetry,可以创建指标、追踪和日志并将其发送到 Grafana LGTM 开发服务。

默认情况下,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.protocol 设置为 http/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 访问

在开发模式下启动应用程序后

CLI
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=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 实例

Dev UI LGTM

探索

在“探索”部分中,您可以查询所有数据源的数据。

要查看追踪,请选择 tempo 数据源并查询数据

Dev UI LGTM

对于日志,请选择 loki 数据源并查询数据

Dev UI LGTM

仪表板

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

Dev UI LGTM

每个仪表板都针对特定的应用程序设置进行了调整。可用的仪表板是

  • Quarkus Micrometer OpenTelemetry:与 Micrometer 和 OpenTelemetry 扩展一起使用。

  • Quarkus Micrometer OTLP registry:与 Micrometer OTLP 注册表扩展一起使用。

  • Quarkus Micrometer Prometheus registry:与 Micrometer Prometheus 注册表扩展一起使用。

  • Quarkus OpenTelemetry Logging:用于查看来自 OpenTelemetry 扩展的日志。

当仪表板中的某些面板的值是在滑动时间窗口内计算时,可能需要几分钟才能显示准确的数据。

其他配置

此扩展将配置您的 quarkus-opentelemetryquarkus-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 注册表

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();
    }
}

相关内容