Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OTel metrics for Microprofile Telemetry 2.0 #43678

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions docs/src/main/asciidoc/opentelemetry-metrics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -529,9 +529,23 @@
=== Resource
See the main xref:opentelemetry.adoc#resource[OpenTelemetry Guide resources] section.

== Additional instrumentation
== Automatic instrumentation

We provide automatic instrumentation JVM Metrics and HTTP server requests according to the https://github.com/eclipse/microprofile-telemetry/blob/2.0/spec/src/main/asciidoc/metrics.adoc[Microprofile Metrics 2.0 specification].

These metrics can be disabled by setting the following properties to `false`:

[source,properties]
----
quarkus.otel.instrument.jvm-metrics=false
quarkus.otel.instrument.http-server-metrics=false
----

[NOTE]
====
- It is recommended to disable these instrumentations if you are using the Micrometer extension as well.

Check warning on line 546 in docs/src/main/asciidoc/opentelemetry-metrics.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'instrumentations'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'instrumentations'?", "location": {"path": "docs/src/main/asciidoc/opentelemetry-metrics.adoc", "range": {"start": {"line": 546, "column": 38}}}, "severity": "WARNING"}

Check warning on line 546 in docs/src/main/asciidoc/opentelemetry-metrics.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/opentelemetry-metrics.adoc", "range": {"start": {"line": 546, "column": 65}}}, "severity": "INFO"}

Check warning on line 546 in docs/src/main/asciidoc/opentelemetry-metrics.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/opentelemetry-metrics.adoc", "range": {"start": {"line": 546, "column": 97}}}, "severity": "INFO"}
====

Automatic metrics are not yet provided by the Quarkus OpenTelemetry extension.
We plan to bridge the existing Quarkus Micrometer extension metrics to OpenTelemetry in the future.

== Exporters
Expand Down
5 changes: 0 additions & 5 deletions docs/src/main/asciidoc/opentelemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,9 @@
----
At build time on your `application.properties` file.

==== Manual instrumentation only
For now only manual instrumentation is supported. You can use the OpenTelemetry API to create and record metrics but Quarkus is not yet providing automatic instrumentation for metrics.

In the future, we plan to bridge current Micrometer metrics to OpenTelemetry and maintain compatibility when possible.

=== xref:opentelemetry-logging.adoc[OpenTelemetry Logging Guide]

==== Enable Logs

Check warning on line 63 in docs/src/main/asciidoc/opentelemetry.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Enable Logs'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Enable Logs'.", "location": {"path": "docs/src/main/asciidoc/opentelemetry.adoc", "range": {"start": {"line": 63, "column": 6}}}, "severity": "INFO"}
The logging functionality is experimental and *off* by default. You will need to activate it by setting:

[source,properties]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ void createOpenTelemetry(
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void setupVertx(InstrumentationRecorder recorder, BeanContainerBuildItem beanContainerBuildItem,
Capabilities capabilities) {
Capabilities capabilities, OTelBuildConfig config) {
boolean sqlClientAvailable = capabilities.isPresent(Capability.REACTIVE_DB2_CLIENT)
|| capabilities.isPresent(Capability.REACTIVE_MSSQL_CLIENT)
|| capabilities.isPresent(Capability.REACTIVE_MYSQL_CLIENT)
Expand All @@ -283,7 +283,8 @@ void setupVertx(InstrumentationRecorder recorder, BeanContainerBuildItem beanCon
boolean redisClientAvailable = capabilities.isPresent(Capability.REDIS_CLIENT);
recorder.setupVertxTracer(beanContainerBuildItem.getValue(),
sqlClientAvailable,
redisClientAvailable);
redisClientAvailable,
config);
}

@BuildStep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem;
import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig;
import io.quarkus.opentelemetry.runtime.metrics.cdi.MetricsProducer;
import io.quarkus.opentelemetry.runtime.metrics.instrumentation.JvmMetricsService;

@BuildSteps(onlyIf = MetricProcessor.MetricEnabled.class)
public class MetricProcessor {
Expand All @@ -39,6 +41,7 @@ UnremovableBeanBuildItem ensureProducersAreRetained(
additionalBeans.produce(AdditionalBeanBuildItem.builder()
.setUnremovable()
.addBeanClass(MetricsProducer.class)
.addBeanClass(JvmMetricsService.class)
.build());

IndexView index = indexBuildItem.getIndex();
Expand Down Expand Up @@ -84,6 +87,13 @@ UnremovableBeanBuildItem ensureProducersAreRetained(
return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanClassNamesExclusion(retainProducers));
}

@BuildStep
void runtimeInit(BuildProducer<RuntimeReinitializedClassBuildItem> runtimeReinitialized) {
runtimeReinitialized.produce(
new RuntimeReinitializedClassBuildItem(
"io.opentelemetry.instrumentation.runtimemetrics.java8.internal.CpuMethods"));
}

public static class MetricEnabled implements BooleanSupplier {
OTelBuildConfig otelBuildConfig;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig;
import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig;
import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor;
import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.HttpInstrumenterVertxTracer;
Expand All @@ -33,6 +34,9 @@ public class OpenTelemetryDisabledSdkTest {
@Inject
OTelRuntimeConfig runtimeConfig;

@Inject
OTelBuildConfig buildConfig;

@Test
void testNoTracer() {
// The OTel API doesn't provide a clear way to check if a tracer is an effective NOOP tracer.
Expand All @@ -41,7 +45,7 @@ void testNoTracer() {

@Test
void noReceiveRequestInstrumenter() {
HttpInstrumenterVertxTracer instrumenter = new HttpInstrumenterVertxTracer(openTelemetry, runtimeConfig);
HttpInstrumenterVertxTracer instrumenter = new HttpInstrumenterVertxTracer(openTelemetry, runtimeConfig, buildConfig);

Instrumenter<HttpRequest, HttpResponse> receiveRequestInstrumenter = instrumenter.getReceiveRequestInstrumenter();
assertFalse(receiveRequestInstrumenter.shouldStart(null, null),
Expand All @@ -50,7 +54,7 @@ void noReceiveRequestInstrumenter() {

@Test
void noReceiveResponseInstrumenter() {
HttpInstrumenterVertxTracer instrumenter = new HttpInstrumenterVertxTracer(openTelemetry, runtimeConfig);
HttpInstrumenterVertxTracer instrumenter = new HttpInstrumenterVertxTracer(openTelemetry, runtimeConfig, buildConfig);

Instrumenter<HttpRequest, HttpResponse> receiveRequestInstrumenter = instrumenter.getReceiveResponseInstrumenter();
assertFalse(receiveRequestInstrumenter.shouldStart(null, null),
Expand All @@ -59,7 +63,7 @@ void noReceiveResponseInstrumenter() {

@Test
void noSendRequestInstrumenter() {
HttpInstrumenterVertxTracer instrumenter = new HttpInstrumenterVertxTracer(openTelemetry, runtimeConfig);
HttpInstrumenterVertxTracer instrumenter = new HttpInstrumenterVertxTracer(openTelemetry, runtimeConfig, buildConfig);

Instrumenter<HttpRequest, HttpResponse> receiveRequestInstrumenter = instrumenter.getSendRequestInstrumenter();
assertFalse(receiveRequestInstrumenter.shouldStart(null, null),
Expand All @@ -68,7 +72,7 @@ void noSendRequestInstrumenter() {

@Test
void noSendResponseInstrumenter() {
HttpInstrumenterVertxTracer instrumenter = new HttpInstrumenterVertxTracer(openTelemetry, runtimeConfig);
HttpInstrumenterVertxTracer instrumenter = new HttpInstrumenterVertxTracer(openTelemetry, runtimeConfig, buildConfig);

Instrumenter<HttpRequest, HttpResponse> receiveRequestInstrumenter = instrumenter.getSendResponseInstrumenter();
assertFalse(receiveRequestInstrumenter.shouldStart(null, null),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ public void assertCountAtLeast(final String name, final String target, final int
.untilAsserted(() -> Assertions.assertTrue(count < getFinishedMetricItems(name, target).size()));
}

public void assertCountPointsAtLeast(final String name, final String target, final int countPoints) {
Awaitility.await().atMost(5, SECONDS)
.untilAsserted(() -> {
List<MetricData> metricData = getFinishedMetricItems(name, target);
Assertions.assertTrue(1 <= metricData.size());
Assertions.assertTrue(countPoints <= metricData.get(0).getData().getPoints().size());
});
}

/**
* Returns a {@code List} of the finished {@code Metric}s, represented by {@code MetricData}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.quarkus.opentelemetry.deployment.metrics;

import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD;
import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE;
import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE;
import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME;
import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
import static org.hamcrest.Matchers.is;

import java.net.URL;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.opentelemetry.sdk.metrics.data.MetricData;
import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporter;
import io.quarkus.opentelemetry.deployment.common.exporter.InMemoryMetricExporterProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.restassured.RestAssured;

public class HttpServerMetricsTest {

@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
.setArchiveProducer(
() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class)
.addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()),
"META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")
.add(new StringAsset(
"quarkus.otel.metrics.enabled=true\n" +
"quarkus.otel.traces.exporter=none\n" +
"quarkus.otel.logs.exporter=none\n" +
"quarkus.otel.metrics.exporter=in-memory\n" +
"quarkus.otel.metric.export.interval=300ms\n"),
"application.properties"));

@Inject
protected InMemoryMetricExporter metricExporter;

@TestHTTPResource
URL url;

@AfterEach
void tearDown() {
metricExporter.reset();
}

@Test
void collectsHttpRouteFromEndAttributes() {
RestAssured.when()
.get("/span").then()
.statusCode(200)
.body(is("hello"));

RestAssured.when()
.get("/fail").then()
.statusCode(INTERNAL_SERVER_ERROR.getStatusCode());

metricExporter.assertCountPointsAtLeast("http.server.request.duration", null, 2);
MetricData metric = metricExporter.getFinishedMetricItems("http.server.request.duration", null).get(0);

assertThat(metric)
.hasName("http.server.request.duration")
.hasDescription("Duration of HTTP server requests.")
.hasUnit("s")
.hasHistogramSatisfying(histogram -> histogram.isCumulative()
.hasPointsSatisfying(
point -> point.hasCount(1)
.hasAttributesSatisfying(
equalTo(HTTP_REQUEST_METHOD, "GET"),
equalTo(URL_SCHEME, "http"),
equalTo(HTTP_RESPONSE_STATUS_CODE, 200),
equalTo(HTTP_ROUTE, url.getPath() + "span")),
point -> point.hasCount(1)
.hasAttributesSatisfying(
equalTo(HTTP_REQUEST_METHOD, "GET"),
equalTo(URL_SCHEME, "http"),
equalTo(HTTP_RESPONSE_STATUS_CODE, 500),
equalTo(HTTP_ROUTE, url.getPath() + "fail"))));
}

@Path("/")
public static class SpanResource {
@GET
@Path("/span")
public Response span() {
return Response.ok("hello").build();
}

@GET
@Path("/fail")
public Response fail() {
return Response.status(INTERNAL_SERVER_ERROR).build();
}
}
}
Loading
Loading