Skip to content

Commit

Permalink
New rest observation instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
brunobat committed Feb 21, 2024
1 parent d92cb95 commit 71f5aa4
Show file tree
Hide file tree
Showing 8 changed files with 492 additions and 2 deletions.
42 changes: 42 additions & 0 deletions implementation/rest-observation/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.smallrye.opentelemetry</groupId>
<artifactId>smallrye-opentelemetry-parent</artifactId>
<version>2.6.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>smallrye-opentelemetry-rest-observation</artifactId>
<name>SmallRye OpenTelemetry: REST Observation</name>

<dependencies>
<dependency>
<groupId>io.smallrye.opentelemetry</groupId>
<artifactId>smallrye-opentelemetry-api</artifactId>
</dependency>

<dependency>
<groupId>io.smallrye.opentelemetry</groupId>
<artifactId>smallrye-opentelemetry-observation-otel-bridge</artifactId>
</dependency>

<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.opentelemetry.semconv</groupId>
<artifactId>opentelemetry-semconv</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package io.smallrye.opentelemetry.implementation.rest.observation;

import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD;
import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_METHOD;
import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_STATUS_CODE;
import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE;
import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE;
import static io.smallrye.opentelemetry.api.OpenTelemetryConfig.INSTRUMENTATION_NAME;
import static io.smallrye.opentelemetry.api.OpenTelemetryConfig.INSTRUMENTATION_VERSION;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

import java.util.List;

import jakarta.inject.Inject;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.client.ClientResponseContext;
import jakarta.ws.rs.client.ClientResponseFilter;
import jakarta.ws.rs.ext.Provider;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.metrics.LongHistogram;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.TextMapSetter;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientExperimentalMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
import io.opentelemetry.instrumentation.api.internal.SemconvStability;

@Provider
public class ObservationClientFilter implements ClientRequestFilter, ClientResponseFilter {
private Instrumenter<ClientRequestContext, ClientResponseContext> instrumenter;
private LongHistogram durationHistogram;

// RESTEasy requires no-arg constructor for CDI injection: https://issues.redhat.com/browse/RESTEASY-1538
public ObservationClientFilter() {
}

@Inject
public ObservationClientFilter(final OpenTelemetry openTelemetry) {
ClientAttributesExtractor clientAttributesExtractor = new ClientAttributesExtractor();

// TODO - The Client Span name is only "HTTP {METHOD_NAME}": https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name
final InstrumenterBuilder<ClientRequestContext, ClientResponseContext> builder = Instrumenter.builder(
openTelemetry,
INSTRUMENTATION_NAME,
HttpSpanNameExtractor.create(clientAttributesExtractor));
builder.setInstrumentationVersion(INSTRUMENTATION_VERSION);

this.instrumenter = builder
.setSpanStatusExtractor(HttpSpanStatusExtractor.create(clientAttributesExtractor))
.addAttributesExtractor(HttpClientAttributesExtractor.create(clientAttributesExtractor))
// .addOperationMetrics(HttpClientMetrics.get()) // This will include the duration histogram
.addOperationMetrics(HttpClientExperimentalMetrics.get())
.buildClientInstrumenter(new ClientRequestContextTextMapSetter());

final Meter meter = openTelemetry.getMeter(INSTRUMENTATION_NAME);
//fixme use new https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpclientrequestduration
durationHistogram = meter.histogramBuilder("http.client.duration")
.setDescription("The duration of an outbound HTTP request")
.ofLongs()
.setUnit("ms")
.build();
}

@Override
public void filter(final ClientRequestContext request) {
// CDI is not available in some contexts even if this library is available on the CP
if (instrumenter != null) {
Context parentContext = Context.current();
if (instrumenter.shouldStart(parentContext, request)) {
Context spanContext = instrumenter.start(parentContext, request);
Scope scope = spanContext.makeCurrent();
request.setProperty("otel.span.client.context", spanContext);
request.setProperty("otel.span.client.parentContext", parentContext);
request.setProperty("otel.span.client.scope", scope);
}
}
if (durationHistogram != null) {
request.setProperty("otel.metrics.client.start", System.currentTimeMillis());
}
}

@Override
public void filter(final ClientRequestContext request, final ClientResponseContext response) {
// CDI is not available in some contexts even if this library is available on the CP
Context spanContext = (Context) request.getProperty("otel.span.client.context");
if (instrumenter != null) {
Scope scope = (Scope) request.getProperty("otel.span.client.scope");
if (scope == null) {
return;
}

try {
instrumenter.end(spanContext, request, response, null);
} finally {
scope.close();

request.removeProperty("otel.span.client.context");
request.removeProperty("otel.span.client.parentContext");
request.removeProperty("otel.span.client.scope");
}
}
if (durationHistogram != null) {
Long start = (Long) request.getProperty("otel.metrics.client.start");
if (start != null) {
try {
durationHistogram.record(System.currentTimeMillis() - start,
getHistogramAttributes(request, response),
spanContext);
} finally {
request.removeProperty("otel.metrics.client.start");
}
}
}
}

private Attributes getHistogramAttributes(ClientRequestContext request, ClientResponseContext response) {
AttributesBuilder builder = Attributes.builder();
builder.put(HTTP_ROUTE.getKey(), request.getUri().getPath().toString());// Fixme must contain a template /users/:userID?
if (SemconvStability.emitOldHttpSemconv()) {
builder.put(HTTP_METHOD, request.getMethod());// FIXME semantic conventions
builder.put(HTTP_STATUS_CODE, response.getStatus());
} else {
builder.put(HTTP_REQUEST_METHOD, request.getMethod());// FIXME semantic conventions
builder.put(HTTP_RESPONSE_STATUS_CODE, response.getStatus());
}
return builder.build();
}

private static class ClientRequestContextTextMapSetter implements TextMapSetter<ClientRequestContext> {
@Override
public void set(final ClientRequestContext carrier, final String key, final String value) {
if (carrier != null) {
carrier.getHeaders().put(key, singletonList(value));
}
}
}

private static class ClientAttributesExtractor
implements HttpClientAttributesGetter<ClientRequestContext, ClientResponseContext> {

@Override
public String getUrlFull(final ClientRequestContext request) {
return request.getUri().toString();
}

@Override
public String getServerAddress(final ClientRequestContext request) {
return request.getUri().getHost();
}

@Override
public Integer getServerPort(final ClientRequestContext request) {
return request.getUri().getPort();
}

@Override
public String getHttpRequestMethod(final ClientRequestContext request) {
return request.getMethod();
}

@Override
public List<String> getHttpRequestHeader(final ClientRequestContext request, final String name) {
return request.getStringHeaders().getOrDefault(name, emptyList());
}

@Override
public Integer getHttpResponseStatusCode(final ClientRequestContext request, final ClientResponseContext response,
final Throwable throwable) {
return response.getStatus();
}

@Override
public List<String> getHttpResponseHeader(final ClientRequestContext request, final ClientResponseContext response,
final String name) {
return response.getHeaders().getOrDefault(name, emptyList());
}
}
}
Loading

0 comments on commit 71f5aa4

Please sign in to comment.