-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New rest observation instrumentation
- Loading branch information
Showing
8 changed files
with
492 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
190 changes: 190 additions & 0 deletions
190
...va/io/smallrye/opentelemetry/implementation/rest.observation/ObservationClientFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
} |
Oops, something went wrong.