diff --git a/instrumentation/armeria-1.3/javaagent/build.gradle.kts b/instrumentation/armeria-1.3/javaagent/build.gradle.kts index 89e62ac51c38..e2d2e222aeb5 100644 --- a/instrumentation/armeria-1.3/javaagent/build.gradle.kts +++ b/instrumentation/armeria-1.3/javaagent/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) library("com.linecorp.armeria:armeria:1.3.0") + testLibrary("com.linecorp.armeria:armeria-junit5:1.3.0") testImplementation(project(":instrumentation:armeria-1.3:testing")) } diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpResponseMutator.java b/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpResponseMutator.java new file mode 100644 index 000000000000..a9380503da0a --- /dev/null +++ b/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpResponseMutator.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.common.ResponseHeadersBuilder; +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseMutator; + +enum ArmeriaHttpResponseMutator implements HttpServerResponseMutator { + INSTANCE; + + @Override + public void appendHeader(ResponseHeadersBuilder response, String name, String value) { + response.add(name, value); + } +} diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java b/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java index 260d9f60763a..b94af56e76eb 100644 --- a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java +++ b/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java @@ -38,7 +38,11 @@ public final class ArmeriaSingletons { .build(); CLIENT_DECORATOR = telemetry.newClientDecorator(); - SERVER_DECORATOR = service -> new ServerDecorator(service); + Function libraryDecorator = + telemetry + .newServiceDecorator() + .compose(service -> new ResponseCustomizingDecorator(service)); + SERVER_DECORATOR = service -> new ServerDecorator(service, libraryDecorator.apply(service)); } private ArmeriaSingletons() {} diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ResponseCustomizingDecorator.java b/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ResponseCustomizingDecorator.java new file mode 100644 index 000000000000..35a60207e89e --- /dev/null +++ b/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ResponseCustomizingDecorator.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.common.FilteredHttpResponse; +import com.linecorp.armeria.common.HttpObject; +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.ResponseHeaders; +import com.linecorp.armeria.common.ResponseHeadersBuilder; +import com.linecorp.armeria.server.HttpService; +import com.linecorp.armeria.server.ServiceRequestContext; +import com.linecorp.armeria.server.SimpleDecoratingHttpService; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; + +class ResponseCustomizingDecorator extends SimpleDecoratingHttpService { + + ResponseCustomizingDecorator(HttpService delegate) { + super(delegate); + } + + @Override + public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { + HttpResponse response = unwrap().serve(ctx, req); + Context context = Context.current(); + return new FilteredHttpResponse(response) { + @Override + public HttpObject filter(HttpObject obj) { + // Ignore other objects like HttpData. + if (!(obj instanceof ResponseHeaders)) { + return obj; + } + + ResponseHeaders headers = (ResponseHeaders) obj; + ResponseHeadersBuilder headersBuilder = headers.toBuilder(); + HttpServerResponseCustomizerHolder.getCustomizer() + .customize(context, headersBuilder, ArmeriaHttpResponseMutator.INSTANCE); + + return headersBuilder.build(); + } + }; + } +} diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java b/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java index fb3b7b0a3956..a6eac353a1db 100644 --- a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java +++ b/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java @@ -14,17 +14,27 @@ import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerRoute; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerRouteSource; class ServerDecorator extends SimpleDecoratingHttpService { + private final HttpService libraryDelegate; - ServerDecorator(HttpService delegate) { + ServerDecorator(HttpService delegate, HttpService libraryDelegate) { super(delegate); + this.libraryDelegate = libraryDelegate; } @Override public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { + // If there is no server span fall back to armeria liberary instrumentation. Server span is + // usually created by netty instrumentation, it can be missing when netty instrumentation is + // disabled or when http2 is used (netty instrumentation does not support http2). + if (!LocalRootSpan.current().getSpanContext().isValid()) { + return libraryDelegate.serve(ctx, req); + } + String matchedRoute = ctx.config().route().patternString(); if (matchedRoute == null || matchedRoute.isEmpty()) { matchedRoute = "/"; diff --git a/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2Test.java b/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2Test.java new file mode 100644 index 000000000000..9025514ae7fd --- /dev/null +++ b/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2Test.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ArmeriaHttp2Test { + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension + static ServerExtension server1 = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service("/", (ctx, req) -> HttpResponse.of("hello")); + } + }; + + @RegisterExtension + static ServerExtension server2 = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service("/", (ctx, req) -> createWebClient(server1).execute(req)); + } + }; + + private static WebClient createWebClient(ServerExtension server) { + return WebClient.builder(server.httpUri()).build(); + } + + @Test + void testHello() throws Exception { + // verify that spans are created and context is propagated + AggregatedHttpResponse result = createWebClient(server2).get("/").aggregate().get(); + assertThat(result.contentAscii()).isEqualTo("hello"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("GET").hasKind(SpanKind.CLIENT).hasNoParent(), + span -> span.hasName("GET /").hasKind(SpanKind.SERVER).hasParent(trace.getSpan(0)), + span -> span.hasName("GET").hasKind(SpanKind.CLIENT).hasParent(trace.getSpan(1)), + span -> + span.hasName("GET /").hasKind(SpanKind.SERVER).hasParent(trace.getSpan(2)))); + } +}