Add `jdk.httpserver` instrumentation (#13243)

Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
This commit is contained in:
Josiah Noel 2025-02-14 11:24:35 -05:00 committed by GitHub
parent f2e51a0cb2
commit 8d057a2a86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 936 additions and 1 deletions

View File

@ -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'

View File

@ -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]&nbsp;[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] |

View File

@ -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"))
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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() {}
}

View File

@ -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";
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
```

View File

@ -0,0 +1,8 @@
plugins {
id("otel.library-instrumentation")
id("otel.nullaway-conventions")
}
dependencies {
testImplementation(project(":instrumentation:java-http-server:testing"))
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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";
}
}

View File

@ -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() {}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -0,0 +1,7 @@
plugins {
id("otel.java-conventions")
}
dependencies {
api(project(":testing-common"))
}

View File

@ -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);
});
}
}

View File

@ -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.");

View File

@ -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")