diff --git a/.fossa.yml b/.fossa.yml index 669600a2e600..d0f8da960f6a 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -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' diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 8a66681358f0..fb4c4f762325 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -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),
[opentelemetry-runtime-telemetry-java17](../instrumentation/runtime-telemetry/runtime-telemetry-java17/library),
[opentelemetry-resources](../instrumentation/resources/library) | [JVM Runtime Metrics] | | [Javalin](https://javalin.io/) | 5.0+ | N/A | Provides `http.route` [2] | diff --git a/instrumentation/java-http-server/javaagent/build.gradle.kts b/instrumentation/java-http-server/javaagent/build.gradle.kts new file mode 100644 index 000000000000..1c8be721a562 --- /dev/null +++ b/instrumentation/java-http-server/javaagent/build.gradle.kts @@ -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")) +} diff --git a/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/HttpServerInstrumentation.java b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/HttpServerInstrumentation.java new file mode 100644 index 000000000000..f1dc68a5914f --- /dev/null +++ b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/HttpServerInstrumentation.java @@ -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 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); + } + } +} diff --git a/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerInstrumentationModule.java b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerInstrumentationModule.java new file mode 100644 index 000000000000..9d25ca807c3a --- /dev/null +++ b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerInstrumentationModule.java @@ -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 typeInstrumentations() { + return singletonList(new HttpServerInstrumentation()); + } +} diff --git a/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerResponseMutator.java b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerResponseMutator.java new file mode 100644 index 000000000000..c212eaeadec7 --- /dev/null +++ b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerResponseMutator.java @@ -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 { + INSTANCE; + + @Override + public void appendHeader(Headers response, String name, String value) { + response.add(name, value); + } +} diff --git a/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerSingletons.java b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerSingletons.java new file mode 100644 index 000000000000..997b325f6410 --- /dev/null +++ b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerSingletons.java @@ -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 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() {} +} diff --git a/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/ResponseCustomizingFilter.java b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/ResponseCustomizingFilter.java new file mode 100644 index 000000000000..41db8046c129 --- /dev/null +++ b/instrumentation/java-http-server/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/ResponseCustomizingFilter.java @@ -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"; + } +} diff --git a/instrumentation/java-http-server/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerTest.java b/instrumentation/java-http-server/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerTest.java new file mode 100644 index 000000000000..445b4d0bc07a --- /dev/null +++ b/instrumentation/java-http-server/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javahttpserver/JavaHttpServerTest.java @@ -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); + } +} diff --git a/instrumentation/java-http-server/library/README.md b/instrumentation/java-http-server/library/README.md new file mode 100644 index 000000000000..c4d6d9382dc8 --- /dev/null +++ b/instrumentation/java-http-server/library/README.md @@ -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 + + + io.opentelemetry.instrumentation + opentelemetry-java-http-server + OPENTELEMETRY_VERSION + + +``` + +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); + } +} +``` diff --git a/instrumentation/java-http-server/library/build.gradle.kts b/instrumentation/java-http-server/library/build.gradle.kts new file mode 100644 index 000000000000..34f1618868e8 --- /dev/null +++ b/instrumentation/java-http-server/library/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("otel.library-instrumentation") + id("otel.nullaway-conventions") +} + +dependencies { + testImplementation(project(":instrumentation:java-http-server:testing")) +} diff --git a/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerAttributesGetter.java b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerAttributesGetter.java new file mode 100644 index 000000000000..c742fe99f857 --- /dev/null +++ b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerAttributesGetter.java @@ -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 { + 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 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 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(); + } +} diff --git a/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerExchangeGetter.java b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerExchangeGetter.java new file mode 100644 index 000000000000..1a573b60f1d5 --- /dev/null +++ b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerExchangeGetter.java @@ -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 { + INSTANCE; + + @Override + public Iterable 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 list = carrier.getRequestHeaders().get(key); + return list != null ? list.get(0) : null; + } + + @Override + public Iterator getAll(@Nullable HttpExchange carrier, String key) { + if (carrier == null) { + return emptyIterator(); + } + + List list = carrier.getRequestHeaders().get(key); + return list != null ? list.iterator() : emptyIterator(); + } +} diff --git a/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTelemetry.java b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTelemetry.java new file mode 100644 index 000000000000..6595f553d8cf --- /dev/null +++ b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTelemetry.java @@ -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 instrumenter; + + JavaHttpServerTelemetry(Instrumenter 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()); + } +} diff --git a/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTelemetryBuilder.java b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTelemetryBuilder.java new file mode 100644 index 000000000000..ec2462e6c59b --- /dev/null +++ b/instrumentation/java-http-server/library/src/main/java/io/opentelemetry/instrumentation/javahttpserver/JavaHttpServerTelemetryBuilder.java @@ -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 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, + ? extends SpanStatusExtractor> + 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 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 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 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, + ? extends SpanNameExtractor> + 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")