diff --git a/.fossa.yml b/.fossa.yml index d0f8da960f6a..b8327cee5a7d 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -49,6 +49,9 @@ targets: - type: gradle path: ./ target: ':testing:agent-for-testing' + - type: gradle + path: ./ + target: ':instrumentation:activej-http-6.0:javaagent' - type: gradle path: ./ target: ':instrumentation:alibaba-druid-1.0:javaagent' diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index fb4c4f762325..c120ff44cde2 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -19,6 +19,7 @@ These are the supported libraries and frameworks: | Library/Framework | Auto-instrumented versions | Standalone Library Instrumentation [1] | Semantic Conventions | |---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| [ActiveJ](https://activej.io/) | 6.0+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | | [Akka Actors](https://doc.akka.io/docs/akka/current/typed/index.html) | 2.3+ | N/A | Context propagation | | [Akka HTTP](https://doc.akka.io/docs/akka-http/current/index.html) | 10.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics], Provides `http.route` [2] | | [Alibaba Druid](https://github.com/alibaba/druid) | 1.0+ | [opentelemetry-alibaba-druid-1.0](../instrumentation/alibaba-druid-1.0/library) | [Database Pool Metrics] | diff --git a/instrumentation/activej-http-6.0/javaagent/build.gradle.kts b/instrumentation/activej-http-6.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..afe8d68968e3 --- /dev/null +++ b/instrumentation/activej-http-6.0/javaagent/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("io.activej") + module.set("activej-http") + versions.set("[6.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("io.activej:activej-http:6.0-rc2") + latestDepTestLibrary("io.activej:activej-http:6.+") // documented limitation, can be removed when there is a non rc version in 6.x series +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionInstrumentation.java b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionInstrumentation.java new file mode 100644 index 000000000000..30208b67a927 --- /dev/null +++ b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionInstrumentation.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.activejhttp; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static io.opentelemetry.javaagent.instrumentation.activejhttp.ActivejHttpServerConnectionSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.activej.http.AsyncServlet; +import io.activej.http.HttpRequest; +import io.activej.http.HttpResponse; +import io.activej.promise.Promise; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +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 ActivejHttpServerConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("io.activej.http.AsyncServlet")).and(not(isInterface())); + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.activej.http.AsyncServlet"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("serve")) + .and(takesArguments(1).and(takesArgument(0, named("io.activej.http.HttpRequest")))), + this.getClass().getName() + "$ServeAdvice"); + } + + @SuppressWarnings("unused") + public static class ServeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.This AsyncServlet asyncServlet, + @Advice.Argument(0) HttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("httpRequest") HttpRequest httpRequest) { + Context parentContext = currentContext(); + httpRequest = request; + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.This AsyncServlet asyncServlet, + @Advice.Return(readOnly = false) Promise responsePromise, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("httpRequest") HttpRequest httpRequest) { + if (scope == null) { + return; + } + scope.close(); + if (throwable != null) { + instrumenter().end(context, httpRequest, null, throwable); + } else { + responsePromise = PromiseWrapper.wrap(responsePromise, httpRequest, context); + } + } + } +} diff --git a/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionInstrumentationModule.java b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionInstrumentationModule.java new file mode 100644 index 000000000000..37f5f10fd76b --- /dev/null +++ b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionInstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.activejhttp; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +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; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class ActivejHttpServerConnectionInstrumentationModule extends InstrumentationModule { + + public ActivejHttpServerConnectionInstrumentationModule() { + super("activej-http", "activej-http-6.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ActivejHttpServerConnectionInstrumentation()); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // class which was added in 6.0, the minimum version we support. + return hasClassesNamed("io.activej.http.HttpServer"); + } +} diff --git a/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionSingletons.java b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionSingletons.java new file mode 100644 index 000000000000..daf774ff5a9f --- /dev/null +++ b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionSingletons.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.activejhttp; + +import io.activej.http.HttpRequest; +import io.activej.http.HttpResponse; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpServerInstrumenters; + +public final class ActivejHttpServerConnectionSingletons { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.activej-http-6.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + INSTRUMENTER = + JavaagentHttpServerInstrumenters.create( + INSTRUMENTATION_NAME, + new ActivejHttpServerHttpAttributesGetter(), + ActivejHttpServerRequestGetter.INSTANCE); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private ActivejHttpServerConnectionSingletons() {} +} diff --git a/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerHttpAttributesGetter.java b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerHttpAttributesGetter.java new file mode 100644 index 000000000000..032ce14988b4 --- /dev/null +++ b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerHttpAttributesGetter.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.activejhttp; + +import io.activej.http.HttpHeader; +import io.activej.http.HttpHeaderValue; +import io.activej.http.HttpHeaders; +import io.activej.http.HttpRequest; +import io.activej.http.HttpResponse; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +final class ActivejHttpServerHttpAttributesGetter + implements HttpServerAttributesGetter { + + @Override + public String getHttpRequestMethod(HttpRequest request) { + return request.getMethod().name(); + } + + @Override + public List getHttpRequestHeader(HttpRequest request, String name) { + HttpHeader httpHeader = HttpHeaders.of(name); + List values = new ArrayList<>(); + for (Map.Entry entry : request.getHeaders()) { + if (httpHeader.equals(entry.getKey())) { + values.add(entry.getValue().toString()); + } + } + + return values; + } + + @Override + public Integer getHttpResponseStatusCode( + HttpRequest request, HttpResponse httpResponse, @Nullable Throwable error) { + return httpResponse.getCode(); + } + + @Override + public List getHttpResponseHeader( + HttpRequest request, HttpResponse httpResponse, String name) { + HttpHeader httpHeader = HttpHeaders.of(name); + List values = new ArrayList<>(); + for (Map.Entry entry : httpResponse.getHeaders()) { + if (httpHeader.equals(entry.getKey())) { + values.add(entry.getValue().toString()); + } + } + + return values; + } + + @Override + public String getUrlScheme(HttpRequest request) { + return request.getProtocol().lowercase(); + } + + @Override + public String getUrlPath(HttpRequest request) { + return request.getPath(); + } + + @Override + public String getUrlQuery(HttpRequest request) { + return request.getQuery(); + } + + @Override + public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse httpResponse) { + return switch (request.getVersion()) { + case HTTP_0_9, HTTP_1_0, HTTP_1_1, HTTP_2_0 -> "http"; + default -> null; + }; + } + + @Override + public String getNetworkProtocolVersion( + HttpRequest request, @Nullable HttpResponse httpResponse) { + return switch (request.getVersion()) { + case HTTP_0_9 -> "0.9"; + case HTTP_1_0 -> "1.0"; + case HTTP_1_1 -> "1.1"; + case HTTP_2_0 -> "2"; + default -> null; + }; + } + + @Nullable + @Override + public String getNetworkPeerAddress(HttpRequest request, @Nullable HttpResponse httpResponse) { + InetAddress remoteAddress = request.getConnection().getRemoteAddress(); + return remoteAddress != null ? remoteAddress.getHostAddress() : null; + } +} diff --git a/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerRequestGetter.java b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerRequestGetter.java new file mode 100644 index 000000000000..a60628599870 --- /dev/null +++ b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerRequestGetter.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.activejhttp; + +import io.activej.http.HttpHeader; +import io.activej.http.HttpHeaderValue; +import io.activej.http.HttpHeaders; +import io.activej.http.HttpRequest; +import io.opentelemetry.context.propagation.internal.ExtendedTextMapGetter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +enum ActivejHttpServerRequestGetter implements ExtendedTextMapGetter { + INSTANCE; + + @Override + public Iterable keys(HttpRequest httpRequest) { + return httpRequest.getHeaders().stream().map(h -> h.getKey().toString()).toList(); + } + + @Override + public String get(HttpRequest carrier, String key) { + if (carrier == null) { + return null; + } + + return carrier.getHeader(HttpHeaders.of(key)); + } + + @Override + public Iterator getAll(HttpRequest carrier, String key) { + if (carrier == null) { + return Collections.emptyIterator(); + } + + HttpHeader httpHeader = HttpHeaders.of(key); + List values = new ArrayList<>(); + for (Map.Entry entry : carrier.getHeaders()) { + if (httpHeader.equals(entry.getKey())) { + values.add(entry.getValue().toString()); + } + } + return values.iterator(); + } +} diff --git a/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/PromiseWrapper.java b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/PromiseWrapper.java new file mode 100644 index 000000000000..d45349f3cc8b --- /dev/null +++ b/instrumentation/activej-http-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/PromiseWrapper.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.activejhttp; + +import static io.opentelemetry.javaagent.instrumentation.activejhttp.ActivejHttpServerConnectionSingletons.instrumenter; + +import io.activej.http.HttpRequest; +import io.activej.http.HttpResponse; +import io.activej.promise.Promise; +import io.opentelemetry.context.Context; + +public final class PromiseWrapper { + + public static Promise wrap( + Promise promise, HttpRequest httpRequest, Context context) { + return promise.whenComplete( + (httpResponse, exception) -> + instrumenter().end(context, httpRequest, httpResponse, exception)); + } + + private PromiseWrapper() {} +} diff --git a/instrumentation/activej-http-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerTest.java b/instrumentation/activej-http-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerTest.java new file mode 100644 index 000000000000..6c316e51fab9 --- /dev/null +++ b/instrumentation/activej-http-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerTest.java @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.activejhttp; + +import static io.activej.common.exception.FatalErrorHandlers.logging; +import static io.activej.http.HttpMethod.GET; +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.ID_PARAMETER_NAME; +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 static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT; + +import io.activej.eventloop.Eventloop; +import io.activej.http.AsyncServlet; +import io.activej.http.HttpHeaderValue; +import io.activej.http.HttpHeaders; +import io.activej.http.HttpResponse; +import io.activej.http.HttpServer; +import io.activej.http.RoutingServlet; +import io.activej.promise.Promise; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.testing.internal.armeria.internal.shaded.guava.collect.ImmutableSet; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ActivejHttpServerTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + private static final Eventloop eventloop = + Eventloop.builder().withCurrentThread().withFatalErrorHandler(logging()).build(); + private Thread thread; + + @Override + protected HttpServer setupServer() throws Exception { + AsyncServlet captureHttpHeadersAsyncServlet = + request -> { + HttpResponse httpResponse = + HttpResponse.builder() + .withBody(CAPTURE_HEADERS.getBody()) + .withCode(CAPTURE_HEADERS.getStatus()) + .withHeader(HttpHeaders.of(TEST_RESPONSE_HEADER), HttpHeaderValue.of("test")) + .build(); + controller(CAPTURE_HEADERS, () -> httpResponse); + return httpResponse.toPromise(); + }; + AsyncServlet indexChildAsyncServlet = + request -> { + HttpResponse httpResponse = + HttpResponse.builder() + .withBody(INDEXED_CHILD.getBody()) + .withCode(INDEXED_CHILD.getStatus()) + .build(); + controller( + INDEXED_CHILD, + () -> { + INDEXED_CHILD.collectSpanAttributes( + id -> + id.equals(ID_PARAMETER_NAME) + ? request.getQueryParameter(ID_PARAMETER_NAME) + : null); + return httpResponse; + }); + return httpResponse.toPromise(); + }; + + RoutingServlet routingServlet = + RoutingServlet.builder(eventloop) + .with(GET, SUCCESS.getPath(), request -> prepareResponse(SUCCESS)) + .with(GET, QUERY_PARAM.getPath(), request -> prepareResponse(QUERY_PARAM)) + .with(GET, ERROR.getPath(), request -> prepareResponse(ERROR)) + .with(GET, NOT_FOUND.getPath(), request -> prepareResponse(NOT_FOUND)) + .with( + GET, + EXCEPTION.getPath(), + request -> + controller( + EXCEPTION, + () -> { + throw new IllegalStateException(EXCEPTION.getBody()); + })) + .with( + GET, + REDIRECT.getPath(), + request -> + controller( + REDIRECT, () -> HttpResponse.redirect302(REDIRECT.getBody()).toPromise())) + .with(GET, CAPTURE_HEADERS.getPath(), captureHttpHeadersAsyncServlet) + .with(GET, INDEXED_CHILD.getPath(), indexChildAsyncServlet) + .build(); + + HttpServer server = HttpServer.builder(eventloop, routingServlet).withListenPort(port).build(); + + server.listen(); + thread = new Thread(eventloop); + thread.start(); + return server; + } + + @Override + protected void stopServer(HttpServer server) throws Exception { + server.closeFuture().get(); + thread.join(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setTestException(false); + options.disableTestNonStandardHttpMethod(); + options.setHttpAttributes(endpoint -> ImmutableSet.of(NETWORK_PEER_PORT)); + } + + private static Promise prepareResponse(ServerEndpoint endpoint) { + HttpResponse httpResponse = + HttpResponse.builder().withBody(endpoint.getBody()).withCode(endpoint.getStatus()).build(); + controller(endpoint, () -> httpResponse); + return httpResponse.toPromise(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7455272696b3..d0b0eb823140 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -128,6 +128,7 @@ include(":smoke-tests-otel-starter:spring-boot-reactive-2") include(":smoke-tests-otel-starter:spring-boot-reactive-3") include(":smoke-tests-otel-starter:spring-boot-reactive-common") +include(":instrumentation:activej-http-6.0:javaagent") include(":instrumentation:akka:akka-actor-2.3:javaagent") include(":instrumentation:akka:akka-actor-fork-join-2.5:javaagent") include(":instrumentation:akka:akka-http-10.0:javaagent")