diff --git a/.fossa.yml b/.fossa.yml index 5b1f524cc4f9..c3645f0f7512 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -136,6 +136,12 @@ targets: - type: gradle path: ./ target: ':instrumentation:gwt-2.0:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:helidon-4.3:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:helidon-4.3:library' - type: gradle path: ./ target: ':instrumentation:hikaricp-3.0:javaagent' diff --git a/.github/config/lychee.toml b/.github/config/lychee.toml index 1f8e51a4d5ba..4c5992769dfa 100644 --- a/.github/config/lychee.toml +++ b/.github/config/lychee.toml @@ -21,4 +21,6 @@ exclude = [ '^http://code.google.com/p/concurrentlinkedhashmap$', '^https://softwareengineering.stackexchange.com/questions/29727.*', '^https://central.sonatype.com/service/rest/repository/browse/maven-snapshots/io/opentelemetry/$', + # new artifact, remove after 2.21.0 release + '^https://central.sonatype.com/artifact/io.opentelemetry.instrumentation/opentelemetry-helidon-4.3$', ] diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 4b9d839b0414..373b52ed4898 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -76,6 +76,7 @@ These are the supported libraries and frameworks: | [gRPC](https://github.com/grpc/grpc-java) | 1.6+ | [opentelemetry-grpc-1.6](../instrumentation/grpc-1.6/library) | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] | | [Guava ListenableFuture](https://guava.dev/releases/snapshot/api/docs/com/google/common/util/concurrent/ListenableFuture.html) | 10.0+ | [opentelemetry-guava-10.0](../instrumentation/guava-10.0/library) | Context propagation | | [GWT](https://www.gwtproject.org/) | 2.0+ | N/A | [RPC Server Spans] | +| [Helidon](https://helidon.io/) | 4.3+ | [opentelemetry-helidon-4.3](../instrumentation/helidon-4.3/library) | [HTTP Server Spans], [HTTP Server Metrics], Provides `http.route` [2] | | [Hibernate](https://github.com/hibernate/hibernate-orm) | 3.3+ | N/A | none | | [Hibernate Reactive](https://hibernate.org/reactive) | 1.0+ | N/A | none | | [HikariCP](https://github.com/brettwooldridge/HikariCP) | 3.0+ | [opentelemetry-hikaricp-3.0](../instrumentation/hikaricp-3.0/library) | [Database Pool Metrics] | diff --git a/instrumentation/helidon-4.3/javaagent/build.gradle.kts b/instrumentation/helidon-4.3/javaagent/build.gradle.kts new file mode 100644 index 000000000000..6ffe73c6c849 --- /dev/null +++ b/instrumentation/helidon-4.3/javaagent/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("io.helidon.webserver") + module.set("helidon-webserver") + versions.set("[4.3.0,)") + assertInverse.set(true) + } +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_21) +} + +dependencies { + library("io.helidon.webserver:helidon-webserver:4.3.0") + implementation(project(":instrumentation:helidon-4.3:library")) + testImplementation(project(":instrumentation:helidon-4.3:testing")) +} diff --git a/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonInstrumentation.java b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonInstrumentation.java new file mode 100644 index 000000000000..6cc609914d41 --- /dev/null +++ b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.helidon.v4_3; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.helidon.webserver.http.HttpRouting; +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 HelidonInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.helidon.webserver.http.HttpRouting"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(isStatic()).and(named("builder")), + HelidonInstrumentation.class.getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return HttpRouting.Builder httpContext) { + HelidonSingletons.FILTERS.forEach(httpContext::addFilter); + } + } +} diff --git a/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonInstrumentationModule.java b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonInstrumentationModule.java new file mode 100644 index 000000000000..756282b5bcf3 --- /dev/null +++ b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.helidon.v4_3; + +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 HelidonInstrumentationModule extends InstrumentationModule { + public HelidonInstrumentationModule() { + super("helidon", "helidon-4.3"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HelidonInstrumentation()); + } +} diff --git a/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonServerResponseMutator.java b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonServerResponseMutator.java new file mode 100644 index 000000000000..e13a0a934f75 --- /dev/null +++ b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonServerResponseMutator.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.helidon.v4_3; + +import io.helidon.webserver.http.ServerResponse; +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseMutator; + +enum HelidonServerResponseMutator implements HttpServerResponseMutator { + INSTANCE; + + @Override + public void appendHeader(ServerResponse res, String name, String value) { + res.header(name, value); + } +} diff --git a/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonSingletons.java b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonSingletons.java new file mode 100644 index 000000000000..c5ad2275ddde --- /dev/null +++ b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonSingletons.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.helidon.v4_3; + +import io.helidon.webserver.http.Filter; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.helidon.v4_3.HelidonTelemetry; +import io.opentelemetry.instrumentation.helidon.v4_3.internal.HelidonInstrumenterBuilderUtil; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import java.util.List; + +public final class HelidonSingletons { + + public static final List FILTERS; + + static { + var serverBuilder = HelidonTelemetry.builder(GlobalOpenTelemetry.get()); + HelidonInstrumenterBuilderUtil.getServerBuilderExtractor() + .apply(serverBuilder) + .configure(AgentCommonConfig.get()); + var serverTelemetry = serverBuilder.build(); + + FILTERS = List.of(serverTelemetry.createFilter(), new ResponseCustomizingFilter()); + } + + private HelidonSingletons() {} +} diff --git a/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/ResponseCustomizingFilter.java b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/ResponseCustomizingFilter.java new file mode 100644 index 000000000000..1bcb5ef07673 --- /dev/null +++ b/instrumentation/helidon-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/ResponseCustomizingFilter.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.helidon.v4_3; + +import io.helidon.webserver.http.Filter; +import io.helidon.webserver.http.FilterChain; +import io.helidon.webserver.http.RoutingRequest; +import io.helidon.webserver.http.RoutingResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; + +final class ResponseCustomizingFilter implements Filter { + + ResponseCustomizingFilter() {} + + @Override + public void filter(FilterChain chain, RoutingRequest req, RoutingResponse res) { + + var context = Context.current(); + HttpServerResponseCustomizerHolder.getCustomizer() + .customize(context, res, HelidonServerResponseMutator.INSTANCE); + chain.proceed(); + } +} diff --git a/instrumentation/helidon-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonServerTest.java b/instrumentation/helidon-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonServerTest.java new file mode 100644 index 000000000000..a9b0287c8131 --- /dev/null +++ b/instrumentation/helidon-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/helidon/v4_3/HelidonServerTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.helidon.v4_3; + +import io.opentelemetry.instrumentation.helidon.v4_3.AbstractHelidonTest; +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 HelidonServerTest extends AbstractHelidonTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setHasResponseCustomizer(serverEndpoint -> true); + + options.setTestNotFound(false); + } +} diff --git a/instrumentation/helidon-4.3/library/README.md b/instrumentation/helidon-4.3/library/README.md new file mode 100644 index 000000000000..1114c94dbc72 --- /dev/null +++ b/instrumentation/helidon-4.3/library/README.md @@ -0,0 +1,56 @@ +# Library Instrumentation for Helidon + +Provides OpenTelemetry instrumentation for [Helidon](https://helidon.io/). + +## Quickstart + +### Add these dependencies to your project + +Replace `OPENTELEMETRY_VERSION` with the [latest +release](https://central.sonatype.com/artifact/io.opentelemetry.instrumentation/opentelemetry-helidon-4.3). + +For Maven, add to your `pom.xml` dependencies: + +```xml + + + io.opentelemetry.instrumentation + opentelemetry-helidon-4.3 + OPENTELEMETRY_VERSION + + +``` + +For Gradle, add to your dependencies: + +```groovy +implementation("io.opentelemetry.instrumentation:opentelemetry-helidon-4.3:OPENTELEMETRY_VERSION") +``` + +### Usage + +The instrumentation library contains an `HttpFeature` that provides OpenTelemetry-based spans +and context propagation. + +```java +import java.io.IOException; + +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.helidon.v4_3.HelidonTelemetry; + +public class Application { + + static void main(String args) throws IOException { + + OpenTelemetry openTelemetry = // ... + WebServer.builder() + .addRouting( + HttpRouting.builder() + .addFeature(HelidonTelemetry.create(openTelemetry)) + .get("/greet", (req, res) -> res.send("Hello World!"))) + .build(); + } +} +``` diff --git a/instrumentation/helidon-4.3/library/build.gradle.kts b/instrumentation/helidon-4.3/library/build.gradle.kts new file mode 100644 index 000000000000..518d425eaac8 --- /dev/null +++ b/instrumentation/helidon-4.3/library/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("otel.library-instrumentation") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_21) +} + +dependencies { + library("io.helidon.webserver:helidon-webserver:4.3.0") + testImplementation(project(":instrumentation:helidon-4.3:testing")) +} diff --git a/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonAttributesGetter.java b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonAttributesGetter.java new file mode 100644 index 000000000000..a3ba78a4e8ad --- /dev/null +++ b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonAttributesGetter.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3; + +import io.helidon.http.HeaderNames; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.net.InetSocketAddress; +import java.util.List; +import javax.annotation.Nullable; + +enum HelidonAttributesGetter implements HttpServerAttributesGetter { + INSTANCE; + + @Override + public String getHttpRequestMethod(ServerRequest req) { + return req.prologue().method().text(); + } + + @Override + public String getUrlScheme(ServerRequest req) { + return req.requestedUri().scheme(); + } + + @Override + public String getUrlPath(ServerRequest req) { + return req.path().rawPath(); + } + + @Nullable + @Override + public String getUrlQuery(ServerRequest req) { + return req.query().rawValue(); + } + + @Override + public List getHttpRequestHeader(ServerRequest req, String name) { + return req.headers().values(HeaderNames.create(name)); + } + + @Override + public Integer getHttpResponseStatusCode( + ServerRequest req, ServerResponse res, @Nullable Throwable error) { + return res.status().code(); + } + + @Override + public List getHttpResponseHeader(ServerRequest req, ServerResponse res, String name) { + return res.headers().values(HeaderNames.create(name)); + } + + @Override + public String getNetworkProtocolName(ServerRequest req, @Nullable ServerResponse res) { + return req.prologue().protocol(); + } + + @Override + public String getNetworkProtocolVersion(ServerRequest req, @Nullable ServerResponse res) { + return req.prologue().protocolVersion(); + } + + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + ServerRequest req, @Nullable ServerResponse res) { + var address = req.remotePeer().address(); + return address instanceof InetSocketAddress s ? s : null; + } + + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + ServerRequest req, @Nullable ServerResponse res) { + var address = req.localPeer().address(); + return address instanceof InetSocketAddress s ? s : null; + } +} diff --git a/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonRequestGetter.java b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonRequestGetter.java new file mode 100644 index 000000000000..0f56c702184d --- /dev/null +++ b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonRequestGetter.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3; + +import static java.util.Collections.emptyIterator; +import static java.util.Collections.emptyList; + +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.webserver.http.ServerRequest; +import io.opentelemetry.context.propagation.internal.ExtendedTextMapGetter; +import java.util.Iterator; +import javax.annotation.Nullable; + +enum HelidonRequestGetter implements ExtendedTextMapGetter { + INSTANCE; + + @Override + public Iterable keys(@Nullable ServerRequest req) { + if (req == null) { + return emptyList(); + } + return () -> req.headers().stream().map(Header::name).iterator(); + } + + @Override + public String get(@Nullable ServerRequest carrier, String key) { + if (carrier == null) { + return null; + } + + return carrier.headers().first(HeaderNames.create(key)).orElse(null); + } + + @Override + public Iterator getAll(@Nullable ServerRequest carrier, String key) { + if (carrier == null) { + return emptyIterator(); + } + + return carrier.headers().values(HeaderNames.create(key)).iterator(); + } +} diff --git a/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonTelemetry.java b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonTelemetry.java new file mode 100644 index 000000000000..d9da6b16bb71 --- /dev/null +++ b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonTelemetry.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3; + +import io.helidon.webserver.http.Filter; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +/** Entrypoint for instrumenting Helidon services. */ +public final class HelidonTelemetry { + + /** Returns a new {@link HelidonTelemetry} configured with the given {@link OpenTelemetry}. */ + public static HelidonTelemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + public static HelidonTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new HelidonTelemetryBuilder(openTelemetry); + } + + private final Instrumenter instrumenter; + + HelidonTelemetry(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + /** + * Construct a new OpenTelemetry enabled {@link Filter}. Add it with {@link + * HttpRouting.Builder#addFilter(Filter)} to start capturing telemetry. + */ + public Filter createFilter() { + return new OpenTelemetryFilter(instrumenter); + } +} diff --git a/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonTelemetryBuilder.java b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonTelemetryBuilder.java new file mode 100644 index 000000000000..abb032841919 --- /dev/null +++ b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonTelemetryBuilder.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +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.helidon.v4_3.internal.Experimental; +import io.opentelemetry.instrumentation.helidon.v4_3.internal.HelidonInstrumenterBuilderUtil; +import java.util.Collection; +import java.util.function.UnaryOperator; + +public final class HelidonTelemetryBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.helidon-4.3"; + + private final DefaultHttpServerInstrumenterBuilder builder; + + static { + HelidonInstrumenterBuilderUtil.setServerBuilderExtractor(builder -> builder.builder); + Experimental.internalSetEmitExperimentalTelemetry( + (builder, emit) -> builder.builder.setEmitExperimentalHttpServerTelemetry(emit)); + } + + HelidonTelemetryBuilder(OpenTelemetry openTelemetry) { + builder = + DefaultHttpServerInstrumenterBuilder.create( + INSTRUMENTATION_NAME, + openTelemetry, + HelidonAttributesGetter.INSTANCE, + HelidonRequestGetter.INSTANCE); + } + + /** Sets the status extractor for server spans. */ + @CanIgnoreReturnValue + public HelidonTelemetryBuilder setStatusExtractor( + UnaryOperator> 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 HelidonTelemetryBuilder 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 HelidonTelemetryBuilder 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 HelidonTelemetryBuilder 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 HelidonTelemetryBuilder setKnownMethods(Collection knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** Sets custom server {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public HelidonTelemetryBuilder setSpanNameExtractor( + UnaryOperator> serverSpanNameExtractor) { + builder.setSpanNameExtractor(serverSpanNameExtractor); + return this; + } + + public HelidonTelemetry build() { + return new HelidonTelemetry(builder.build()); + } +} diff --git a/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/OpenTelemetryFilter.java b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/OpenTelemetryFilter.java new file mode 100644 index 000000000000..c05fb727a1d7 --- /dev/null +++ b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/OpenTelemetryFilter.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3; + +import io.helidon.webserver.http.Filter; +import io.helidon.webserver.http.FilterChain; +import io.helidon.webserver.http.RoutingRequest; +import io.helidon.webserver.http.RoutingResponse; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; + +final class OpenTelemetryFilter implements Filter { + + private final Instrumenter instrumenter; + + OpenTelemetryFilter(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + @Override + public void filter(FilterChain chain, RoutingRequest req, RoutingResponse res) { + Context parentContext = Context.current(); + if (!instrumenter.shouldStart(parentContext, req)) { + chain.proceed(); + return; + } + + Context context = instrumenter.start(parentContext, req); + + Throwable error = null; + try (Scope ignored = context.makeCurrent()) { + chain.proceed(); + } catch (Throwable t) { + error = t; + throw t; + } finally { + req.matchingPattern() + .ifPresent(route -> HttpServerRoute.update(context, HttpServerRouteSource.SERVER, route)); + instrumenter.end(context, req, res, error); + } + } +} diff --git a/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/internal/Experimental.java b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/internal/Experimental.java new file mode 100644 index 000000000000..16d1d44d8382 --- /dev/null +++ b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/internal/Experimental.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3.internal; + +import io.opentelemetry.instrumentation.helidon.v4_3.HelidonTelemetryBuilder; +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( + HelidonTelemetryBuilder 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/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/internal/HelidonInstrumenterBuilderUtil.java b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/internal/HelidonInstrumenterBuilderUtil.java new file mode 100644 index 000000000000..6d60802f7768 --- /dev/null +++ b/instrumentation/helidon-4.3/library/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/internal/HelidonInstrumenterBuilderUtil.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3.internal; + +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.helidon.v4_3.HelidonTelemetryBuilder; +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 HelidonInstrumenterBuilderUtil { + private HelidonInstrumenterBuilderUtil() {} + + @Nullable + private static Function< + HelidonTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + serverBuilderExtractor; + + @Nullable + public static Function< + HelidonTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + getServerBuilderExtractor() { + return serverBuilderExtractor; + } + + public static void setServerBuilderExtractor( + Function< + HelidonTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + serverBuilderExtractor) { + HelidonInstrumenterBuilderUtil.serverBuilderExtractor = serverBuilderExtractor; + } +} diff --git a/instrumentation/helidon-4.3/library/src/test/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonServerTest.java b/instrumentation/helidon-4.3/library/src/test/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonServerTest.java new file mode 100644 index 000000000000..1e19625e1da2 --- /dev/null +++ b/instrumentation/helidon-4.3/library/src/test/java/io/opentelemetry/instrumentation/helidon/v4_3/HelidonServerTest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3; + +import io.helidon.webserver.http.HttpRouting; +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 org.junit.jupiter.api.extension.RegisterExtension; + +class HelidonServerTest extends AbstractHelidonTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forLibrary(); + + @Override + protected void configureRoutes(HttpRouting.Builder routing) { + var feature = + HelidonTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedRequestHeaders( + Collections.singletonList(AbstractHttpServerTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders( + Collections.singletonList(AbstractHttpServerTest.TEST_RESPONSE_HEADER)) + .build(); + routing.addFilter(feature.createFilter()); + } +} diff --git a/instrumentation/helidon-4.3/metadata.yaml b/instrumentation/helidon-4.3/metadata.yaml new file mode 100644 index 000000000000..5e8ff68ef74a --- /dev/null +++ b/instrumentation/helidon-4.3/metadata.yaml @@ -0,0 +1,28 @@ +description: This instrumentation enables HTTP server spans and HTTP server metrics for the Helidon HTTP server. +library_link: https://helidon.io/ +configurations: + - name: otel.instrumentation.http.known-methods + description: > + Configures the instrumentation to recognize an alternative set of HTTP request methods. All + other methods will be treated as `_OTHER`. + type: list + default: "CONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE" + - name: otel.instrumentation.http.server.capture-request-headers + description: List of HTTP request headers to capture in HTTP server telemetry. + type: list + default: "" + - name: otel.instrumentation.http.server.capture-response-headers + description: List of HTTP response headers to capture in HTTP server telemetry. + type: list + default: "" + - name: otel.instrumentation.common.peer-service-mapping + description: Used to specify a mapping from host names or IP addresses to peer services. + type: map + default: "" + - name: otel.instrumentation.http.server.emit-experimental-telemetry + description: > + Enable the capture of experimental HTTP server telemetry. Adds the `http.request.body.size` and + `http.response.body.size` attributes to spans, and records `http.server.request.body.size` + and `http.server.response.body.size` metrics. + type: boolean + default: false diff --git a/instrumentation/helidon-4.3/testing/build.gradle.kts b/instrumentation/helidon-4.3/testing/build.gradle.kts new file mode 100644 index 000000000000..56c8d2889546 --- /dev/null +++ b/instrumentation/helidon-4.3/testing/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("otel.java-conventions") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_21) +} + +dependencies { + api(project(":testing-common")) + implementation("io.helidon.webserver:helidon-webserver:4.3.0") +} diff --git a/instrumentation/helidon-4.3/testing/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/AbstractHelidonTest.java b/instrumentation/helidon-4.3/testing/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/AbstractHelidonTest.java new file mode 100644 index 000000000000..db33a78a5394 --- /dev/null +++ b/instrumentation/helidon-4.3/testing/src/main/java/io/opentelemetry/instrumentation/helidon/v4_3/AbstractHelidonTest.java @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.helidon.v4_3; + +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 io.helidon.http.HeaderNames; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +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.util.Collections; +import java.util.Map; + +public abstract class AbstractHelidonTest extends AbstractHttpServerTest { + + protected void configureRoutes(HttpRouting.Builder routing) {} + + static void sendResponse(ServerResponse res, int status, String response) { + sendResponse(res, status, Collections.emptyMap(), response); + } + + static void sendResponse(ServerResponse res, int status, Map headers) { + sendResponse(res, status, headers, ""); + } + + static void sendResponse( + ServerResponse res, int status, Map headers, String response) { + res.header("Content-Type", "text/plain"); + headers.forEach(res::header); + res.status(status).send(response); + } + + private static String getUrlQuery(ServerRequest req) { + return req.query().rawValue(); + } + + @Override + protected WebServer setupServer() { + var server = WebServer.builder().port(port); + var routing = HttpRouting.builder(); + + routing.get( + SUCCESS.getPath(), + (req, res) -> + testing() + .runWithSpan( + "controller", () -> sendResponse(res, SUCCESS.getStatus(), SUCCESS.getBody()))); + + routing.get( + REDIRECT.getPath(), + (req, res) -> + testing() + .runWithSpan( + "controller", + () -> + sendResponse( + res, + REDIRECT.getStatus(), + Collections.singletonMap("Location", REDIRECT.getBody())))); + + routing.get( + ERROR.getPath(), + (req, res) -> + testing() + .runWithSpan( + "controller", () -> sendResponse(res, ERROR.getStatus(), ERROR.getBody()))); + + routing.get( + QUERY_PARAM.getPath(), + (req, res) -> + testing() + .runWithSpan( + "controller", + () -> + sendResponse( + res, + QUERY_PARAM.getStatus(), + "some=" + QueryParams.fromQueryString(getUrlQuery(req)).get("some")))); + + routing.get( + INDEXED_CHILD.getPath(), + (req, res) -> + testing() + .runWithSpan( + "controller", + () -> { + INDEXED_CHILD.collectSpanAttributes( + name -> QueryParams.fromQueryString(getUrlQuery(req)).get(name)); + + sendResponse(res, INDEXED_CHILD.getStatus(), INDEXED_CHILD.getBody()); + })); + + routing.get( + "/captureHeaders", + (req, res) -> + testing() + .runWithSpan( + "controller", + () -> + sendResponse( + res, + CAPTURE_HEADERS.getStatus(), + Collections.singletonMap( + "X-Test-Response", + req.headers().get(HeaderNames.create("X-Test-Request")).get()), + CAPTURE_HEADERS.getBody()))); + + routing.get( + EXCEPTION.getPath(), + (req, res) -> + testing() + .runWithSpan( + "controller", + () -> { + sendResponse(res, EXCEPTION.getStatus(), EXCEPTION.getBody()); + throw new IllegalStateException(EXCEPTION.getBody()); + })); + + routing.get("/", (req, res) -> sendResponse(res, NOT_FOUND.getStatus(), NOT_FOUND.getBody())); + configureRoutes(routing); + + return server.routing(routing).build().start(); + } + + @Override + protected void stopServer(WebServer server) { + server.stop(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + // filter isn't called for non-standard method + options.disableTestNonStandardHttpMethod(); + options.setTestException(false); + options.setTestHttpPipelining(true); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3b6bf858fb56..d675362f0057 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -247,6 +247,9 @@ include(":instrumentation:grpc-1.6:testing") include(":instrumentation:guava-10.0:javaagent") include(":instrumentation:guava-10.0:library") include(":instrumentation:gwt-2.0:javaagent") +include(":instrumentation:helidon-4.3:javaagent") +include(":instrumentation:helidon-4.3:library") +include(":instrumentation:helidon-4.3:testing") include(":instrumentation:hibernate:hibernate-3.3:javaagent") include(":instrumentation:hibernate:hibernate-4.0:javaagent") include(":instrumentation:hibernate:hibernate-6.0:javaagent")