Skip to content

Commit

Permalink
OTel metrics for Microprofile 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
brunobat committed Oct 15, 2024
1 parent 8f670e6 commit 7853d67
Show file tree
Hide file tree
Showing 25 changed files with 705 additions and 66 deletions.
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 @@ We only show 1 of 3 exemplars for brevity.
=== Resource
See the main xref:opentelemetry.adoc#resource[OpenTelemetry Guide resources] section.

== Additional instrumentation
== Automatic instrumentation

We provide automatic instrumentation for JVM metrics and HTTP server requests metrics 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,11 +58,6 @@ quarkus.otel.metrics.enabled=true
----
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"}
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 @@ -126,6 +126,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

0 comments on commit 7853d67

Please sign in to comment.