Observability Dev Services with Grafana OTel LGTM
include::_attributes.adoc[]:iokays-category: quarkus:iokays-path: modules/ROOT/pages/observability-devservices-lgtm.adoc:categories: observability,devservices,telemetry,metrics,tracing,logging, opentelemetry, micrometer, prometheus, tempo, loki, grafana:summary: 有关如何使用 Grafana Otel LGTM 的说明:topics: observability,grafana,lgtm,otlp,opentelemetry,devservices,micrometer:extensions: io.quarkus:quarkus-observability-devservices
OTel-LGTM 是 all-in-one
Docker 映像,其中包含 OpenTelemetry 的 OTLP 作为将度量标准、跟踪和日志记录数据传输到 OpenTelemetry Collector 的协议,然后将信号数据存储到 Prometheus(度量标准)、 Tempo(跟踪)和 Loki(日志),仅供 Grafana 可视化。Quarkus Observability 使用它来提供 Grafana OTel LGTM Dev 资源。
Configuring your project
将 Quarkus Grafana OTel LGTM 接收器(数据去向)扩展添加到您的构建文件中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-observability-devservices-lgtm</artifactId>
<scope>provided</scope>
</dependency>
implementation("quarkus-observability-devservices-lgtm")
Metrics
如果您需要指标,请将 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")
在使用 MicroMeter’s Quarkiverse OTLP 注册表将度量标准推送到 Grafana OTel LGTM 时,quarkus.micrometer.export.otlp.url
属性会自动设置为 OTel 收集器端点,从 docker 容器外部可以看到。
Tracing
对于跟踪,请将 quarkus-opentelemetry
扩展添加到您的构建文件:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
implementation("io.quarkus:quarkus-opentelemetry")
quarkus.otel.exporter.otlp.endpoint
属性会自动设置为 OTel 收集器端点,从 docker 容器外部可以看到。
quarkus.otel.exporter.otlp.protocol
设置为 http/protobuf
。
Access Grafana
在开发模式下启动您的应用程序后:
Unresolved directive in observability-devservices-lgtm.adoc - include::{includes}/devtools/dev.adoc[]
您会看到这样的日志条目:
[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
。
如果您错过了消息,您始终可以使用此 Docker 命令检查端口:
docker ps | grep grafana
Additional configuration
此扩展将配置您的 quarkus-opentelemetry
和 quarkus-micrometer-registry-otlp
扩展,以便将数据发送到与 Grafana OTel LGTM 映像捆绑的 OTel 收集器。
如果您不想使用 Dev Services(例如查找和重新使用现有的运行容器等)的麻烦,您可以简单地禁用 Dev Services 并仅启用 Dev Resource 使用:
quarkus.observability.enabled=false
quarkus.observability.dev-resources=true
Tests
对于测试中最不“自动魔幻”的使用,简单地禁用两者(Dev Resource 默认情况下已禁用):
quarkus.observability.enabled=false
然后在测试中明确地将 LGTM Dev Resource 列为 @WithTestResource
资源:
@QuarkusTest
@WithTestResource(LgtmResource.class)
@TestProfile(QuarkusTestResourceTestProfile.class)
public class LgtmLifecycleTest extends LgtmTestBase {
}
Testing full Grafana OTel LGTM stack - example
使用现有的 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();
}
}