responseHeaders) {
+ builder.setCapturedResponseHeaders(responseHeaders);
+ return this;
+ }
+
+ /**
+ * Configures the instrumentation to recognize an alternative set of HTTP request methods.
+ *
+ * By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH
+ * method defined in RFC5789.
+ *
+ *
Note: calling this method overrides 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 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());
+ }
+}
diff --git a/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/OpenTelemetryFilter.java b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/OpenTelemetryFilter.java
new file mode 100644
index 000000000000..83d6968e73ca
--- /dev/null
+++ b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/OpenTelemetryFilter.java
@@ -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 instrumenter;
+
+ OpenTelemetryFilter(Instrumenter 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";
+ }
+}
diff --git a/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/internal/Experimental.java b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/internal/Experimental.java
new file mode 100644
index 000000000000..bd6bf037ad24
--- /dev/null
+++ b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/internal/Experimental.java
@@ -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
+ setEmitExperimentalTelemetry;
+
+ public static void setEmitExperimentalTelemetry(
+ JavaHttpServerTelemetryBuilder builder, boolean emitExperimentalTelemetry) {
+ if (setEmitExperimentalTelemetry != null) {
+ setEmitExperimentalTelemetry.accept(builder, emitExperimentalTelemetry);
+ }
+ }
+
+ public static void internalSetEmitExperimentalTelemetry(
+ BiConsumer setEmitExperimentalTelemetry) {
+ Experimental.setEmitExperimentalTelemetry = setEmitExperimentalTelemetry;
+ }
+
+ private Experimental() {}
+}
diff --git a/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/internal/JavaHttpServerInstrumenterBuilderUtil.java b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/internal/JavaHttpServerInstrumenterBuilderUtil.java
new file mode 100644
index 000000000000..3232411599a0
--- /dev/null
+++ b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/internal/JavaHttpServerInstrumenterBuilderUtil.java
@@ -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>
+ serverBuilderExtractor;
+
+ @Nullable
+ public static Function<
+ JavaHttpServerTelemetryBuilder,
+ DefaultHttpServerInstrumenterBuilder>
+ getServerBuilderExtractor() {
+ return serverBuilderExtractor;
+ }
+
+ public static void setServerBuilderExtractor(
+ Function<
+ JavaHttpServerTelemetryBuilder,
+ DefaultHttpServerInstrumenterBuilder>
+ serverBuilderExtractor) {
+ JavaHttpServerInstrumenterBuilderUtil.serverBuilderExtractor = serverBuilderExtractor;
+ }
+}
diff --git a/instrumentation/java-http-server/library/src/test/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTest.java b/instrumentation/java-http-server/library/src/test/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTest.java
new file mode 100644
index 000000000000..92e08aeced2a
--- /dev/null
+++ b/instrumentation/java-http-server/library/src/test/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTest.java
@@ -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 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));
+ }
+}
diff --git a/instrumentation/java-http-server/testing/build.gradle.kts b/instrumentation/java-http-server/testing/build.gradle.kts
new file mode 100644
index 000000000000..484e04028aaa
--- /dev/null
+++ b/instrumentation/java-http-server/testing/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ id("otel.java-conventions")
+}
+
+dependencies {
+ api(project(":testing-common"))
+}
diff --git a/instrumentation/java-http-server/testing/src/main/java/io/opentelemetry/instrumentation/javahttpserver/AbstractJavaHttpServerTest.java b/instrumentation/java-http-server/testing/src/main/java/io/opentelemetry/instrumentation/javahttpserver/AbstractJavaHttpServerTest.java
new file mode 100644
index 000000000000..2876bf7479d8
--- /dev/null
+++ b/instrumentation/java-http-server/testing/src/main/java/io/opentelemetry/instrumentation/javahttpserver/AbstractJavaHttpServerTest.java
@@ -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 {
+
+ protected void configureContexts(List 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 headers)
+ throws IOException {
+ sendResponse(exchange, status, headers, "");
+ }
+
+ static void sendResponse(
+ HttpExchange exchange, int status, Map 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 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);
+ });
+ }
+}
diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java
index 0a8b709b1d11..2b7ec17d8f57 100644
--- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java
+++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java
@@ -99,7 +99,8 @@ private static void configureIgnoredTypes(IgnoredTypesBuilder builder) {
.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.");
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 38bea2a9ef00..7297f7a09484 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -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")