Add `jdk.httpserver` instrumentation (#13243)
Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
This commit is contained in:
parent
f2e51a0cb2
commit
8d057a2a86
|
|
@ -148,6 +148,12 @@ targets:
|
|||
- type: gradle
|
||||
path: ./
|
||||
target: ':instrumentation:java-http-client:library'
|
||||
- type: gradle
|
||||
path: ./
|
||||
target: ':instrumentation:java-http-server:javaagent'
|
||||
- type: gradle
|
||||
path: ./
|
||||
target: ':instrumentation:java-http-server:library'
|
||||
- type: gradle
|
||||
path: ./
|
||||
target: ':instrumentation:java-util-logging:javaagent'
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ These are the supported libraries and frameworks:
|
|||
| [InfluxDB Client](https://github.com/influxdata/influxdb-java) | 2.4+ | N/A | [Database Client Spans], [Database Client Metrics] [6] |
|
||||
| [Java Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) | Java 8+ | N/A | Context propagation |
|
||||
| [Java Http Client](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html) | Java 11+ | [opentelemetry-java-http-client](../instrumentation/java-http-client/library) | [HTTP Client Spans], [HTTP Client Metrics] |
|
||||
| [Java Http Server](https://docs.oracle.com/en/java/javase/21/docs/api/jdk.httpserver/module-summary.html) | Java 8+ | [opentelemetry-java-http-server](../instrumentation/java-http-server/library) | [HTTP Server Spans], [HTTP Server Metrics] |
|
||||
| [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html) | Java 8+ | N/A | none |
|
||||
| [Java Platform](https://docs.oracle.com/javase/8/docs/api/java/lang/management/ManagementFactory.html) | Java 8+ | [opentelemetry-runtime-telemetry-java8](../instrumentation/runtime-telemetry/runtime-telemetry-java8/library),<br>[opentelemetry-runtime-telemetry-java17](../instrumentation/runtime-telemetry/runtime-telemetry-java17/library),<br>[opentelemetry-resources](../instrumentation/resources/library) | [JVM Runtime Metrics] |
|
||||
| [Javalin](https://javalin.io/) | 5.0+ | N/A | Provides `http.route` [2] |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
plugins {
|
||||
id("otel.javaagent-instrumentation")
|
||||
}
|
||||
|
||||
muzzle {
|
||||
pass {
|
||||
coreJdk()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":instrumentation:java-http-server:library"))
|
||||
testImplementation(project(":instrumentation:java-http-server:testing"))
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.javahttpserver;
|
||||
|
||||
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.named;
|
||||
|
||||
import com.sun.net.httpserver.HttpContext;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
|
||||
public class HttpServerInstrumentation implements TypeInstrumentation {
|
||||
|
||||
@Override
|
||||
public ElementMatcher<TypeDescription> typeMatcher() {
|
||||
return extendsClass(named("com.sun.net.httpserver.HttpServer"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transform(TypeTransformer transformer) {
|
||||
transformer.applyAdviceToMethod(
|
||||
isMethod().and(isPublic()).and(named("createContext")),
|
||||
HttpServerInstrumentation.class.getName() + "$BuildAdvice");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class BuildAdvice {
|
||||
|
||||
@Advice.OnMethodExit(suppress = Throwable.class)
|
||||
public static void onExit(@Advice.Return HttpContext httpContext) {
|
||||
httpContext.getFilters().addAll(JavaHttpServerSingletons.FILTERS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.javahttpserver;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
|
||||
import java.util.List;
|
||||
|
||||
@AutoService(InstrumentationModule.class)
|
||||
public class JavaHttpServerInstrumentationModule extends InstrumentationModule {
|
||||
public JavaHttpServerInstrumentationModule() {
|
||||
super("java-http-server");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TypeInstrumentation> typeInstrumentations() {
|
||||
return singletonList(new HttpServerInstrumentation());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.javahttpserver;
|
||||
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseMutator;
|
||||
|
||||
enum JavaHttpServerResponseMutator implements HttpServerResponseMutator<Headers> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public void appendHeader(Headers response, String name, String value) {
|
||||
response.add(name, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.javahttpserver;
|
||||
|
||||
import com.sun.net.httpserver.Filter;
|
||||
import io.opentelemetry.api.GlobalOpenTelemetry;
|
||||
import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig;
|
||||
import io.opentelemetry.instrumentation.javahttpserver.JavaHttpServerTelemetry;
|
||||
import io.opentelemetry.instrumentation.javahttpserver.JavaHttpServerTelemetryBuilder;
|
||||
import io.opentelemetry.instrumentation.javahttpserver.internal.JavaHttpServerInstrumenterBuilderUtil;
|
||||
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class JavaHttpServerSingletons {
|
||||
|
||||
public static final List<Filter> FILTERS;
|
||||
|
||||
static {
|
||||
CommonConfig config = AgentCommonConfig.get();
|
||||
|
||||
JavaHttpServerTelemetryBuilder serverBuilder =
|
||||
JavaHttpServerTelemetry.builder(GlobalOpenTelemetry.get());
|
||||
JavaHttpServerInstrumenterBuilderUtil.getServerBuilderExtractor()
|
||||
.apply(serverBuilder)
|
||||
.configure(config);
|
||||
JavaHttpServerTelemetry serverTelemetry = serverBuilder.build();
|
||||
|
||||
FILTERS = Arrays.asList(serverTelemetry.newFilter(), new ResponseCustomizingFilter());
|
||||
}
|
||||
|
||||
private JavaHttpServerSingletons() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.javahttpserver;
|
||||
|
||||
import com.sun.net.httpserver.Filter;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder;
|
||||
import java.io.IOException;
|
||||
|
||||
final class ResponseCustomizingFilter extends Filter {
|
||||
|
||||
ResponseCustomizingFilter() {}
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
|
||||
Context context = Context.current();
|
||||
HttpServerResponseCustomizerHolder.getCustomizer()
|
||||
.customize(context, exchange.getResponseHeaders(), JavaHttpServerResponseMutator.INSTANCE);
|
||||
chain.doFilter(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "OpenTelemetry response customizing filter";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.javahttpserver;
|
||||
|
||||
import io.opentelemetry.instrumentation.javahttpserver.AbstractJavaHttpServerTest;
|
||||
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension;
|
||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
class JavaHttpServerTest extends AbstractJavaHttpServerTest {
|
||||
|
||||
@RegisterExtension
|
||||
static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent();
|
||||
|
||||
@Override
|
||||
protected void configure(HttpServerTestOptions options) {
|
||||
super.configure(options);
|
||||
|
||||
options.setHasResponseCustomizer(serverEndpoint -> true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
# Library Instrumentation for Java HTTP Server
|
||||
|
||||
Provides OpenTelemetry instrumentation for [Java HTTP Server](https://docs.oracle.com/en/java/javase/21/docs/api/jdk.httpserver/module-summary.html).
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Add these dependencies to your project
|
||||
|
||||
Replace `OPENTELEMETRY_VERSION` with the [latest
|
||||
release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-java-http-server).
|
||||
|
||||
For Maven, add to your `pom.xml` dependencies:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry.instrumentation</groupId>
|
||||
<artifactId>opentelemetry-java-http-server</artifactId>
|
||||
<version>OPENTELEMETRY_VERSION</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
For Gradle, add to your dependencies:
|
||||
|
||||
```groovy
|
||||
implementation("io.opentelemetry.instrumentation:opentelemetry-java-http-server:OPENTELEMETRY_VERSION")
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
The instrumentation library contains a `Filter` wrapper that provides OpenTelemetry-based spans
|
||||
and context propagation.
|
||||
|
||||
```java
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import com.sun.net.httpserver.HttpContext;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.sdk.OpenTelemetrySdk;
|
||||
|
||||
public class Application {
|
||||
|
||||
static void main(String args) throws IOException {
|
||||
|
||||
final HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
||||
final HttpContext context =
|
||||
server.createContext(
|
||||
"/",
|
||||
ctx -> {
|
||||
// http logic
|
||||
});
|
||||
|
||||
OpenTelemetry openTelemetry = //...
|
||||
|
||||
JavaHttpServerTelemetry.create(openTelemetry).configure(context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
plugins {
|
||||
id("otel.library-instrumentation")
|
||||
id("otel.nullaway-conventions")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(project(":instrumentation:java-http-server:testing"))
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpsExchange;
|
||||
import io.opentelemetry.instrumentation.api.internal.HttpProtocolUtil;
|
||||
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
enum JavaHttpServerAttributesGetter
|
||||
implements HttpServerAttributesGetter<HttpExchange, HttpExchange> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public String getHttpRequestMethod(HttpExchange exchange) {
|
||||
return exchange.getRequestMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrlScheme(HttpExchange exchange) {
|
||||
return exchange instanceof HttpsExchange ? "https" : "http";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrlPath(HttpExchange exchange) {
|
||||
return exchange.getRequestURI().getPath();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getUrlQuery(HttpExchange exchange) {
|
||||
return exchange.getRequestURI().getQuery();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getHttpRequestHeader(HttpExchange exchange, String name) {
|
||||
return exchange.getRequestHeaders().getOrDefault(name, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Integer getHttpResponseStatusCode(
|
||||
HttpExchange exchange, @Nullable HttpExchange res, @Nullable Throwable error) {
|
||||
int status = exchange.getResponseCode();
|
||||
return status != -1 ? status : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getHttpResponseHeader(
|
||||
HttpExchange exchange, @Nullable HttpExchange res, String name) {
|
||||
return exchange.getResponseHeaders().getOrDefault(name, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHttpRoute(HttpExchange exchange) {
|
||||
return exchange.getHttpContext().getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNetworkProtocolName(HttpExchange exchange, @Nullable HttpExchange res) {
|
||||
return HttpProtocolUtil.getProtocol(exchange.getProtocol());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNetworkProtocolVersion(HttpExchange exchange, @Nullable HttpExchange res) {
|
||||
return HttpProtocolUtil.getVersion(exchange.getProtocol());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getNetworkPeerInetSocketAddress(
|
||||
HttpExchange exchange, @Nullable HttpExchange res) {
|
||||
return exchange.getRemoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getNetworkLocalInetSocketAddress(
|
||||
HttpExchange exchange, @Nullable HttpExchange res) {
|
||||
return exchange.getLocalAddress();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver;
|
||||
|
||||
import static java.util.Collections.emptyIterator;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.opentelemetry.context.propagation.internal.ExtendedTextMapGetter;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
enum JavaHttpServerExchangeGetter implements ExtendedTextMapGetter<HttpExchange> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Iterable<String> keys(@Nullable HttpExchange exchange) {
|
||||
if (exchange == null) {
|
||||
return emptyList();
|
||||
}
|
||||
return exchange.getRequestHeaders().keySet();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String get(@Nullable HttpExchange carrier, String key) {
|
||||
if (carrier == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> list = carrier.getRequestHeaders().get(key);
|
||||
return list != null ? list.get(0) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> getAll(@Nullable HttpExchange carrier, String key) {
|
||||
if (carrier == null) {
|
||||
return emptyIterator();
|
||||
}
|
||||
|
||||
List<String> list = carrier.getRequestHeaders().get(key);
|
||||
return list != null ? list.iterator() : emptyIterator();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver;
|
||||
|
||||
import com.sun.net.httpserver.Filter;
|
||||
import com.sun.net.httpserver.HttpContext;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
|
||||
|
||||
/** Entrypoint for instrumenting Java HTTP Server services. */
|
||||
public final class JavaHttpServerTelemetry {
|
||||
|
||||
/**
|
||||
* Returns a new {@link JavaHttpServerTelemetry} configured with the given {@link OpenTelemetry}.
|
||||
*/
|
||||
public static JavaHttpServerTelemetry create(OpenTelemetry openTelemetry) {
|
||||
return builder(openTelemetry).build();
|
||||
}
|
||||
|
||||
public static JavaHttpServerTelemetryBuilder builder(OpenTelemetry openTelemetry) {
|
||||
return new JavaHttpServerTelemetryBuilder(openTelemetry);
|
||||
}
|
||||
|
||||
private final Instrumenter<HttpExchange, HttpExchange> instrumenter;
|
||||
|
||||
JavaHttpServerTelemetry(Instrumenter<HttpExchange, HttpExchange> instrumenter) {
|
||||
this.instrumenter = instrumenter;
|
||||
}
|
||||
|
||||
/** Returns a new {@link Filter} for telemetry usage */
|
||||
public Filter newFilter() {
|
||||
return new OpenTelemetryFilter(instrumenter);
|
||||
}
|
||||
|
||||
/** Configures the {@link HttpContext} with OpenTelemetry. */
|
||||
public void configure(HttpContext httpContext) {
|
||||
httpContext.getFilters().add(0, newFilter());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
|
||||
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder;
|
||||
import io.opentelemetry.instrumentation.javahttpserver.internal.Experimental;
|
||||
import io.opentelemetry.instrumentation.javahttpserver.internal.JavaHttpServerInstrumenterBuilderUtil;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class JavaHttpServerTelemetryBuilder {
|
||||
|
||||
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.java-http-server";
|
||||
|
||||
private final DefaultHttpServerInstrumenterBuilder<HttpExchange, HttpExchange> builder;
|
||||
|
||||
static {
|
||||
JavaHttpServerInstrumenterBuilderUtil.setServerBuilderExtractor(builder -> builder.builder);
|
||||
Experimental.internalSetEmitExperimentalTelemetry(
|
||||
(builder, emit) -> builder.builder.setEmitExperimentalHttpServerMetrics(emit));
|
||||
}
|
||||
|
||||
JavaHttpServerTelemetryBuilder(OpenTelemetry openTelemetry) {
|
||||
builder =
|
||||
DefaultHttpServerInstrumenterBuilder.create(
|
||||
INSTRUMENTATION_NAME,
|
||||
openTelemetry,
|
||||
JavaHttpServerAttributesGetter.INSTANCE,
|
||||
JavaHttpServerExchangeGetter.INSTANCE);
|
||||
}
|
||||
|
||||
/** Sets the status extractor for server spans. */
|
||||
@CanIgnoreReturnValue
|
||||
public JavaHttpServerTelemetryBuilder setStatusExtractor(
|
||||
Function<
|
||||
SpanStatusExtractor<? super HttpExchange, ? super HttpExchange>,
|
||||
? extends SpanStatusExtractor<? super HttpExchange, ? super HttpExchange>>
|
||||
statusExtractor) {
|
||||
builder.setStatusExtractor(statusExtractor);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an extra {@link AttributesExtractor} to invoke to set attributes to instrumented items.
|
||||
* The {@link AttributesExtractor} will be executed after all default extractors.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public JavaHttpServerTelemetryBuilder addAttributesExtractor(
|
||||
AttributesExtractor<HttpExchange, HttpExchange> attributesExtractor) {
|
||||
builder.addAttributesExtractor(attributesExtractor);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the HTTP server request headers that will be captured as span attributes.
|
||||
*
|
||||
* @param requestHeaders A list of HTTP header names.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public JavaHttpServerTelemetryBuilder setCapturedRequestHeaders(
|
||||
Collection<String> requestHeaders) {
|
||||
builder.setCapturedRequestHeaders(requestHeaders);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the HTTP server response headers that will be captured as span attributes.
|
||||
*
|
||||
* @param responseHeaders A list of HTTP header names.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public JavaHttpServerTelemetryBuilder setCapturedResponseHeaders(
|
||||
Collection<String> responseHeaders) {
|
||||
builder.setCapturedResponseHeaders(responseHeaders);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the instrumentation to recognize an alternative set of HTTP request methods.
|
||||
*
|
||||
* <p>By default, this instrumentation defines "known" methods as the ones listed in <a
|
||||
* href="https://www.rfc-editor.org/rfc/rfc9110.html#name-methods">RFC9110</a> and the PATCH
|
||||
* method defined in <a href="https://www.rfc-editor.org/rfc/rfc5789.html">RFC5789</a>.
|
||||
*
|
||||
* <p>Note: calling this method <b>overrides</b> the default known method sets completely; it does
|
||||
* not supplement it.
|
||||
*
|
||||
* @param knownMethods A set of recognized HTTP request methods.
|
||||
* @see HttpServerAttributesExtractorBuilder#setKnownMethods(Collection)
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public JavaHttpServerTelemetryBuilder setKnownMethods(Collection<String> knownMethods) {
|
||||
builder.setKnownMethods(knownMethods);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets custom server {@link SpanNameExtractor} via transform function. */
|
||||
@CanIgnoreReturnValue
|
||||
public JavaHttpServerTelemetryBuilder setSpanNameExtractor(
|
||||
Function<
|
||||
SpanNameExtractor<? super HttpExchange>,
|
||||
? extends SpanNameExtractor<? super HttpExchange>>
|
||||
serverSpanNameExtractor) {
|
||||
builder.setSpanNameExtractor(serverSpanNameExtractor);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavaHttpServerTelemetry build() {
|
||||
return new JavaHttpServerTelemetry(builder.build());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver;
|
||||
|
||||
import com.sun.net.httpserver.Filter;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.context.Scope;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Decorates an {@link HttpServer} to trace inbound {@link HttpExchange}s. */
|
||||
final class OpenTelemetryFilter extends Filter {
|
||||
|
||||
private final Instrumenter<HttpExchange, HttpExchange> instrumenter;
|
||||
|
||||
OpenTelemetryFilter(Instrumenter<HttpExchange, HttpExchange> instrumenter) {
|
||||
this.instrumenter = instrumenter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
|
||||
Context parentContext = Context.current();
|
||||
if (!instrumenter.shouldStart(parentContext, exchange)) {
|
||||
chain.doFilter(exchange);
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = instrumenter.start(parentContext, exchange);
|
||||
|
||||
Throwable error = null;
|
||||
try (Scope ignored = context.makeCurrent()) {
|
||||
chain.doFilter(exchange);
|
||||
} catch (Throwable t) {
|
||||
error = t;
|
||||
throw t;
|
||||
} finally {
|
||||
instrumenter.end(context, exchange, exchange, error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "OpenTelemetry tracing filter";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver.internal;
|
||||
|
||||
import io.opentelemetry.instrumentation.javahttpserver.JavaHttpServerTelemetryBuilder;
|
||||
import java.util.function.BiConsumer;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class is internal and experimental. Its APIs are unstable and can change at any time. Its
|
||||
* APIs (or a version of them) may be promoted to the public stable API in the future, but no
|
||||
* guarantees are made.
|
||||
*/
|
||||
public final class Experimental {
|
||||
|
||||
@Nullable
|
||||
private static volatile BiConsumer<JavaHttpServerTelemetryBuilder, Boolean>
|
||||
setEmitExperimentalTelemetry;
|
||||
|
||||
public static void setEmitExperimentalTelemetry(
|
||||
JavaHttpServerTelemetryBuilder builder, boolean emitExperimentalTelemetry) {
|
||||
if (setEmitExperimentalTelemetry != null) {
|
||||
setEmitExperimentalTelemetry.accept(builder, emitExperimentalTelemetry);
|
||||
}
|
||||
}
|
||||
|
||||
public static void internalSetEmitExperimentalTelemetry(
|
||||
BiConsumer<JavaHttpServerTelemetryBuilder, Boolean> setEmitExperimentalTelemetry) {
|
||||
Experimental.setEmitExperimentalTelemetry = setEmitExperimentalTelemetry;
|
||||
}
|
||||
|
||||
private Experimental() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver.internal;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder;
|
||||
import io.opentelemetry.instrumentation.javahttpserver.JavaHttpServerTelemetryBuilder;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
|
||||
* any time.
|
||||
*/
|
||||
public class JavaHttpServerInstrumenterBuilderUtil {
|
||||
private JavaHttpServerInstrumenterBuilderUtil() {}
|
||||
|
||||
@Nullable
|
||||
private static Function<
|
||||
JavaHttpServerTelemetryBuilder,
|
||||
DefaultHttpServerInstrumenterBuilder<HttpExchange, HttpExchange>>
|
||||
serverBuilderExtractor;
|
||||
|
||||
@Nullable
|
||||
public static Function<
|
||||
JavaHttpServerTelemetryBuilder,
|
||||
DefaultHttpServerInstrumenterBuilder<HttpExchange, HttpExchange>>
|
||||
getServerBuilderExtractor() {
|
||||
return serverBuilderExtractor;
|
||||
}
|
||||
|
||||
public static void setServerBuilderExtractor(
|
||||
Function<
|
||||
JavaHttpServerTelemetryBuilder,
|
||||
DefaultHttpServerInstrumenterBuilder<HttpExchange, HttpExchange>>
|
||||
serverBuilderExtractor) {
|
||||
JavaHttpServerInstrumenterBuilderUtil.serverBuilderExtractor = serverBuilderExtractor;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver;
|
||||
|
||||
import com.sun.net.httpserver.Filter;
|
||||
import com.sun.net.httpserver.HttpContext;
|
||||
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest;
|
||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
class JavaHttpServerTest extends AbstractJavaHttpServerTest {
|
||||
|
||||
@RegisterExtension
|
||||
static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forLibrary();
|
||||
|
||||
@Override
|
||||
protected void configureContexts(List<HttpContext> contexts) {
|
||||
Filter filter =
|
||||
JavaHttpServerTelemetry.builder(testing.getOpenTelemetry())
|
||||
.setCapturedRequestHeaders(
|
||||
Collections.singletonList(AbstractHttpServerTest.TEST_REQUEST_HEADER))
|
||||
.setCapturedResponseHeaders(
|
||||
Collections.singletonList(AbstractHttpServerTest.TEST_RESPONSE_HEADER))
|
||||
.build()
|
||||
.newFilter();
|
||||
contexts.forEach(ctx -> ctx.getFilters().add(filter));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("otel.java-conventions")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":testing-common"))
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.javahttpserver;
|
||||
|
||||
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS;
|
||||
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR;
|
||||
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION;
|
||||
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD;
|
||||
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND;
|
||||
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM;
|
||||
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT;
|
||||
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS;
|
||||
|
||||
import com.sun.net.httpserver.HttpContext;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest;
|
||||
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions;
|
||||
import io.opentelemetry.testing.internal.armeria.common.QueryParams;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public abstract class AbstractJavaHttpServerTest extends AbstractHttpServerTest<HttpServer> {
|
||||
|
||||
protected void configureContexts(List<HttpContext> contexts) {}
|
||||
|
||||
static void sendResponse(HttpExchange exchange, int status, String response) throws IOException {
|
||||
sendResponse(exchange, status, Collections.emptyMap(), response);
|
||||
}
|
||||
|
||||
static void sendResponse(HttpExchange exchange, int status, Map<String, String> headers)
|
||||
throws IOException {
|
||||
sendResponse(exchange, status, headers, "");
|
||||
}
|
||||
|
||||
static void sendResponse(
|
||||
HttpExchange exchange, int status, Map<String, String> headers, String response)
|
||||
throws IOException {
|
||||
|
||||
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// -1 means no content, 0 means unknown content length
|
||||
long contentLength = bytes.length == 0 ? -1 : bytes.length;
|
||||
exchange.getResponseHeaders().set("Content-Type", "text/plain");
|
||||
headers.forEach(exchange.getResponseHeaders()::set);
|
||||
exchange.sendResponseHeaders(status, contentLength);
|
||||
if (bytes.length != 0) {
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
} else {
|
||||
exchange.getResponseBody().close();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getUrlQuery(HttpExchange exchange) {
|
||||
return exchange.getRequestURI().getQuery();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpServer setupServer() throws IOException {
|
||||
List<HttpContext> contexts = new ArrayList<>();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||
|
||||
server.setExecutor(Executors.newCachedThreadPool());
|
||||
HttpContext context =
|
||||
server.createContext(
|
||||
SUCCESS.getPath(),
|
||||
ctx ->
|
||||
testing()
|
||||
.runWithSpan(
|
||||
"controller",
|
||||
() -> sendResponse(ctx, SUCCESS.getStatus(), SUCCESS.getBody())));
|
||||
|
||||
contexts.add(context);
|
||||
context =
|
||||
server.createContext(
|
||||
REDIRECT.getPath(),
|
||||
ctx ->
|
||||
testing()
|
||||
.runWithSpan(
|
||||
"controller",
|
||||
() ->
|
||||
sendResponse(
|
||||
ctx,
|
||||
REDIRECT.getStatus(),
|
||||
Collections.singletonMap("Location", REDIRECT.getBody()))));
|
||||
|
||||
contexts.add(context);
|
||||
context =
|
||||
server.createContext(
|
||||
ERROR.getPath(),
|
||||
ctx ->
|
||||
testing()
|
||||
.runWithSpan(
|
||||
"controller", () -> sendResponse(ctx, ERROR.getStatus(), ERROR.getBody())));
|
||||
|
||||
contexts.add(context);
|
||||
context =
|
||||
server.createContext(
|
||||
QUERY_PARAM.getPath(),
|
||||
ctx ->
|
||||
testing()
|
||||
.runWithSpan(
|
||||
"controller",
|
||||
() ->
|
||||
sendResponse(
|
||||
ctx,
|
||||
QUERY_PARAM.getStatus(),
|
||||
"some="
|
||||
+ QueryParams.fromQueryString(getUrlQuery(ctx)).get("some"))));
|
||||
contexts.add(context);
|
||||
context =
|
||||
server.createContext(
|
||||
INDEXED_CHILD.getPath(),
|
||||
ctx ->
|
||||
testing()
|
||||
.runWithSpan(
|
||||
"controller",
|
||||
() -> {
|
||||
INDEXED_CHILD.collectSpanAttributes(
|
||||
name -> QueryParams.fromQueryString(getUrlQuery(ctx)).get(name));
|
||||
|
||||
sendResponse(ctx, INDEXED_CHILD.getStatus(), INDEXED_CHILD.getBody());
|
||||
}));
|
||||
contexts.add(context);
|
||||
context =
|
||||
server.createContext(
|
||||
"/captureHeaders",
|
||||
ctx ->
|
||||
testing()
|
||||
.runWithSpan(
|
||||
"controller",
|
||||
() ->
|
||||
sendResponse(
|
||||
ctx,
|
||||
CAPTURE_HEADERS.getStatus(),
|
||||
Collections.singletonMap(
|
||||
"X-Test-Response",
|
||||
ctx.getRequestHeaders().getFirst("X-Test-Request")),
|
||||
CAPTURE_HEADERS.getBody())));
|
||||
contexts.add(context);
|
||||
context =
|
||||
server.createContext(
|
||||
EXCEPTION.getPath(),
|
||||
ctx ->
|
||||
testing()
|
||||
.runWithSpan(
|
||||
"controller",
|
||||
() -> {
|
||||
sendResponse(ctx, EXCEPTION.getStatus(), EXCEPTION.getBody());
|
||||
throw new IllegalStateException(EXCEPTION.getBody());
|
||||
}));
|
||||
contexts.add(context);
|
||||
context =
|
||||
server.createContext(
|
||||
"/", ctx -> sendResponse(ctx, NOT_FOUND.getStatus(), NOT_FOUND.getBody()));
|
||||
contexts.add(context);
|
||||
|
||||
configureContexts(contexts);
|
||||
server.start();
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopServer(HttpServer server) {
|
||||
server.stop(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpServerTestOptions options) {
|
||||
// filter isn't called for non-standard method
|
||||
options.disableTestNonStandardHttpMethod();
|
||||
options.setTestHttpPipelining(
|
||||
Double.parseDouble(System.getProperty("java.specification.version")) >= 21);
|
||||
options.setExpectedHttpRoute(
|
||||
(endpoint, method) -> {
|
||||
if (NOT_FOUND.equals(endpoint)) {
|
||||
return "/";
|
||||
}
|
||||
return expectedHttpRoute(endpoint, method);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +99,8 @@ public class GlobalIgnoredTypesConfigurer implements IgnoredTypesConfigurer {
|
|||
.allowClass("sun.net.www.protocol.")
|
||||
.allowClass("sun.rmi.server")
|
||||
.allowClass("sun.rmi.transport")
|
||||
.allowClass("sun.net.www.http.HttpClient");
|
||||
.allowClass("sun.net.www.http.HttpClient")
|
||||
.allowClass("sun.net.httpserver.");
|
||||
|
||||
builder.ignoreClass("org.slf4j.");
|
||||
|
||||
|
|
|
|||
|
|
@ -266,6 +266,9 @@ include(":instrumentation:internal:internal-url-class-loader:javaagent-integrati
|
|||
include(":instrumentation:java-http-client:javaagent")
|
||||
include(":instrumentation:java-http-client:library")
|
||||
include(":instrumentation:java-http-client:testing")
|
||||
include(":instrumentation:java-http-server:javaagent")
|
||||
include(":instrumentation:java-http-server:library")
|
||||
include(":instrumentation:java-http-server:testing")
|
||||
include(":instrumentation:java-util-logging:javaagent")
|
||||
include(":instrumentation:java-util-logging:shaded-stub-for-instrumenting")
|
||||
include(":instrumentation:javalin-5.0:javaagent")
|
||||
|
|
|
|||
Loading…
Reference in New Issue