From 473f16fa68c3db1e486241e47aa81d45e5088d34 Mon Sep 17 00:00:00 2001 From: Anna Nosek Date: Tue, 14 Sep 2021 07:39:25 +0200 Subject: [PATCH] Restlet instrumentation (#3946) * server instrumentation with first tests * migrate to instrumenter API, move TracingFilter to library, rename module, other review refinements * change name to 1.0 and rebase * review, add ServerSpanNaming, create RestletTracing * codenarc fix * review * fix TracingFilter behaviour on exception, inline HeadersAdapter's methods * move instrumentation to doHandle, add StatusFilter in library test --- .../restlet-1.0/javaagent/build.gradle.kts | 28 +++ .../v1_0/RestletInstrumentationModule.java | 25 +++ .../restlet/v1_0/RestletSingletons.java | 24 +++ .../restlet/v1_0/RouteInstrumentation.java | 51 ++++++ .../restlet/v1_0/ServerInstrumentation.java | 85 +++++++++ .../restlet/v1_0/RestletServerTest.groovy | 19 ++ .../restlet-1.0/library/build.gradle.kts | 20 ++ .../restlet/v1_0/RestletHeadersGetter.java | 35 ++++ .../v1_0/RestletHttpAttributesExtractor.java | 99 ++++++++++ .../v1_0/RestletNetAttributesExtractor.java | 34 ++++ .../restlet/v1_0/RestletTracing.java | 47 +++++ .../restlet/v1_0/RestletTracingBuilder.java | 69 +++++++ .../restlet/v1_0/TracingFilter.java | 66 +++++++ .../internal/RestletServerSpanNaming.java | 23 +++ .../restlet/v1_0/RestletServerTest.groovy | 30 +++ .../restlet-1.0/testing/build.gradle.kts | 20 ++ .../v1_0/AbstractRestletServerTest.groovy | 172 ++++++++++++++++++ settings.gradle.kts | 3 + 18 files changed, 850 insertions(+) create mode 100644 instrumentation/restlet/restlet-1.0/javaagent/build.gradle.kts create mode 100644 instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletInstrumentationModule.java create mode 100644 instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletSingletons.java create mode 100644 instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RouteInstrumentation.java create mode 100644 instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/ServerInstrumentation.java create mode 100644 instrumentation/restlet/restlet-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletServerTest.groovy create mode 100644 instrumentation/restlet/restlet-1.0/library/build.gradle.kts create mode 100644 instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletHeadersGetter.java create mode 100644 instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletHttpAttributesExtractor.java create mode 100644 instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletNetAttributesExtractor.java create mode 100644 instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletTracing.java create mode 100644 instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletTracingBuilder.java create mode 100644 instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/TracingFilter.java create mode 100644 instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/internal/RestletServerSpanNaming.java create mode 100644 instrumentation/restlet/restlet-1.0/library/src/test/groovy/io/opententelemetry/instrumentation/restlet/v1_0/RestletServerTest.groovy create mode 100644 instrumentation/restlet/restlet-1.0/testing/build.gradle.kts create mode 100644 instrumentation/restlet/restlet-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_0/AbstractRestletServerTest.groovy diff --git a/instrumentation/restlet/restlet-1.0/javaagent/build.gradle.kts b/instrumentation/restlet/restlet-1.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..7a0122a031cd --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/javaagent/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.restlet") + module.set("org.restlet") + versions.set("[1.0.0, 1.2-M1)") + assertInverse.set(true) + } +} + +repositories { + mavenCentral() + maven("https://maven.restlet.talend.com/") + mavenLocal() +} + +dependencies { + api(project(":instrumentation:restlet:restlet-1.0:library")) + + library("org.restlet:org.restlet:1.1.5") + library("com.noelios.restlet:com.noelios.restlet:1.1.5") + + implementation(project(":instrumentation:restlet:restlet-1.0:library")) + testImplementation(project(":instrumentation:restlet:restlet-1.0:testing")) +} diff --git a/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletInstrumentationModule.java b/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletInstrumentationModule.java new file mode 100644 index 000000000000..af81c0990294 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.restlet.v1_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RestletInstrumentationModule extends InstrumentationModule { + + public RestletInstrumentationModule() { + super("restlet", "restlet-1.0"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList(new ServerInstrumentation(), new RouteInstrumentation()); + } +} diff --git a/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletSingletons.java b/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletSingletons.java new file mode 100644 index 000000000000..c1f5c8bbfd43 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletSingletons.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.restlet.v1_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.restlet.v1_0.RestletTracing; +import org.restlet.data.Request; +import org.restlet.data.Response; + +public final class RestletSingletons { + + private static final Instrumenter INSTRUMENTER = + RestletTracing.create(GlobalOpenTelemetry.get()).getServerInstrumenter(); + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private RestletSingletons() {} +} diff --git a/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RouteInstrumentation.java b/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RouteInstrumentation.java new file mode 100644 index 000000000000..63a5cbeaf898 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RouteInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.restlet.v1_0; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTROLLER; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.restlet.v1_0.internal.RestletServerSpanNaming; +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; +import org.restlet.Route; +import org.restlet.data.Request; + +public class RouteInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.restlet.Route"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("beforeHandle")) + .and(takesArgument(0, named("org.restlet.data.Request"))) + .and(takesArgument(1, named("org.restlet.data.Response"))), + this.getClass().getName() + "$RouteBeforeHandleAdvice"); + } + + @SuppressWarnings("unused") + public static class RouteBeforeHandleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void getRouteInfo(@Advice.This Route route, @Advice.Argument(0) Request request) { + String pattern = route.getTemplate().getPattern(); + + ServerSpanNaming.updateServerSpanName( + currentContext(), CONTROLLER, RestletServerSpanNaming.SERVER_SPAN_NAME, pattern); + } + } +} diff --git a/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/ServerInstrumentation.java b/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/ServerInstrumentation.java new file mode 100644 index 000000000000..32fdc3e7179b --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/ServerInstrumentation.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.restlet.v1_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.restlet.v1_0.RestletSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +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; +import org.restlet.data.Request; +import org.restlet.data.Response; + +public class ServerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.restlet.Server"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("handle")) + .and(takesArgument(0, named("org.restlet.data.Request"))) + .and(takesArgument(1, named("org.restlet.data.Response"))), + this.getClass().getName() + "$ServerHandleAdvice"); + } + + @SuppressWarnings("unused") + public static class ServerHandleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void beginRequest( + @Advice.Argument(0) Request request, + @Advice.Argument(1) Response response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = currentContext(); + + 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 finishRequest( + @Advice.Argument(0) Request request, + @Advice.Argument(1) Response response, + @Advice.Thrown Throwable exception, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope == null) { + return; + } + + scope.close(); + + if (exception != null) { + instrumenter().end(context, request, response, exception); + return; + } + + // Restlet suppresses exceptions and sets the throwable in status + Throwable statusThrowable = response.getStatus().getThrowable(); + + instrumenter().end(context, request, response, statusThrowable); + } + } +} diff --git a/instrumentation/restlet/restlet-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletServerTest.groovy b/instrumentation/restlet/restlet-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletServerTest.groovy new file mode 100644 index 000000000000..5a811b6ccdc9 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v1_0/RestletServerTest.groovy @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.restlet.v1_0 + +import io.opentelemetry.instrumentation.restlet.v1_0.AbstractRestletServerTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import org.restlet.Restlet + +class RestletServerTest extends AbstractRestletServerTest implements AgentTestTrait { + + @Override + Restlet wrapRestlet(Restlet restlet, String path){ + return restlet + } + +} diff --git a/instrumentation/restlet/restlet-1.0/library/build.gradle.kts b/instrumentation/restlet/restlet-1.0/library/build.gradle.kts new file mode 100644 index 000000000000..569dbcfedf0d --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("otel.library-instrumentation") +} + +repositories { + mavenCentral() + maven("https://maven.restlet.talend.com/") + mavenLocal() +} + +dependencies { + + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + library("org.restlet:org.restlet:1.1.5") + library("com.noelios.restlet:com.noelios.restlet:1.1.5") + + testImplementation(project(":instrumentation:restlet:restlet-1.0:testing")) +} diff --git a/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletHeadersGetter.java b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletHeadersGetter.java new file mode 100644 index 000000000000..2df5e3d7503d --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletHeadersGetter.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.restlet.v1_0; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Locale; +import org.restlet.data.Form; +import org.restlet.data.Request; + +final class RestletHeadersGetter implements TextMapGetter { + + @Override + public Iterable keys(Request carrier) { + return getHeaders(carrier).getNames(); + } + + @Override + public String get(Request carrier, String key) { + + Form headers = getHeaders(carrier); + + String value = headers.getFirstValue(key); + if (value != null) { + return value; + } + return headers.getFirstValue(key.toLowerCase(Locale.ROOT)); + } + + private static Form getHeaders(Request carrier) { + return (Form) carrier.getAttributes().get("org.restlet.http.headers"); + } +} diff --git a/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletHttpAttributesExtractor.java b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletHttpAttributesExtractor.java new file mode 100644 index 000000000000..ad800158c161 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletHttpAttributesExtractor.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.restlet.v1_0; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.restlet.data.Reference; +import org.restlet.data.Request; +import org.restlet.data.Response; + +final class RestletHttpAttributesExtractor extends HttpAttributesExtractor { + @Override + protected String method(Request request) { + return request.getMethod().toString(); + } + + @Override + protected String url(Request request) { + return request.getOriginalRef().toString(); + } + + @Override + protected @Nullable String target(Request request) { + Reference ref = request.getOriginalRef(); + String path = ref.getPath(); + return ref.hasQuery() ? path + "?" + ref.getQuery() : path; + } + + @Override + protected @Nullable String host(Request request) { + return null; + } + + @Override + protected @Nullable String route(Request request) { + return null; + } + + @Override + protected @Nullable String scheme(Request request) { + return request.getOriginalRef().getScheme(); + } + + @Override + protected @Nullable String userAgent(Request request) { + return request.getClientInfo().getAgent(); + } + + @Override + protected @Nullable Long requestContentLength(Request request, @Nullable Response response) { + return null; + } + + @Override + protected @Nullable Long requestContentLengthUncompressed( + Request request, @Nullable Response response) { + return null; + } + + @Override + protected @Nullable String flavor(Request request, @Nullable Response response) { + String version = (String) request.getAttributes().get("org.restlet.http.version"); + switch (version) { + case "HTTP/1.0": + return SemanticAttributes.HttpFlavorValues.HTTP_1_0; + case "HTTP/1.1": + return SemanticAttributes.HttpFlavorValues.HTTP_1_1; + case "HTTP/2.0": + return SemanticAttributes.HttpFlavorValues.HTTP_2_0; + default: + // fall through + } + return null; + } + + @Override + protected @Nullable String serverName(Request request, @Nullable Response response) { + return null; + } + + @Override + protected Integer statusCode(Request request, Response response) { + return response.getStatus().getCode(); + } + + @Override + protected @Nullable Long responseContentLength(Request request, Response response) { + return null; + } + + @Override + protected @Nullable Long responseContentLengthUncompressed(Request request, Response response) { + return null; + } +} diff --git a/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletNetAttributesExtractor.java b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletNetAttributesExtractor.java new file mode 100644 index 000000000000..cc4a6f1ff277 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletNetAttributesExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.restlet.v1_0; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.restlet.data.Request; +import org.restlet.data.Response; + +final class RestletNetAttributesExtractor extends NetAttributesExtractor { + @Override + public String transport(Request request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + public @Nullable String peerName(Request request, @Nullable Response response) { + return null; + } + + @Override + public Integer peerPort(Request request, @Nullable Response response) { + return request.getClientInfo().getPort(); + } + + @Override + public @Nullable String peerIp(Request request, @Nullable Response response) { + return request.getClientInfo().getAddress(); + } +} diff --git a/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletTracing.java b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletTracing.java new file mode 100644 index 000000000000..cd1fdd14f050 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletTracing.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.restlet.v1_0; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import org.restlet.Filter; +import org.restlet.data.Request; +import org.restlet.data.Response; + +/** Entrypoint for tracing Restlet servers. */ +public final class RestletTracing { + + /** Returns a new {@link RestletTracing} configured with the given {@link OpenTelemetry}. */ + public static RestletTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + /** + * Returns a new {@link RestletTracingBuilder} configured with the given {@link OpenTelemetry}. + */ + public static RestletTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new RestletTracingBuilder(openTelemetry); + } + + private final Instrumenter serverInstrumenter; + + RestletTracing(Instrumenter serverInstrumenter) { + this.serverInstrumenter = serverInstrumenter; + } + + /** + * Returns a new {@link Filter} which can be used to wrap {@link org.restlet.Restlet} + * implementations. + */ + public Filter newFilter(String path) { + return new TracingFilter(serverInstrumenter, path); + } + + /** Returns a server {@link Instrumenter}. */ + public Instrumenter getServerInstrumenter() { + return serverInstrumenter; + } +} diff --git a/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletTracingBuilder.java b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletTracingBuilder.java new file mode 100644 index 000000000000..330e7b961666 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/RestletTracingBuilder.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.restlet.v1_0; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import java.util.ArrayList; +import java.util.List; +import org.restlet.data.Request; +import org.restlet.data.Response; + +/** A builder of {@link RestletTracing}. */ +public final class RestletTracingBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.restlet-1.0"; + + private final OpenTelemetry openTelemetry; + private final List> additionalExtractors = + new ArrayList<>(); + + RestletTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. + */ + public RestletTracingBuilder addAttributesExtractor( + AttributesExtractor attributesExtractor) { + additionalExtractors.add(attributesExtractor); + return this; + } + + /** + * Returns a new {@link RestletTracing} with the settings of this {@link RestletTracingBuilder}. + */ + public RestletTracing build() { + HttpAttributesExtractor httpAttributesExtractor = + new RestletHttpAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor spanStatusExtractor = + HttpSpanStatusExtractor.create(httpAttributesExtractor); + NetAttributesExtractor netAttributesExtractor = + new RestletNetAttributesExtractor(); + + Instrumenter instrumenter = + Instrumenter.newBuilder( + openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractors(additionalExtractors) + .newServerInstrumenter(new RestletHeadersGetter()); + + return new RestletTracing(instrumenter); + } +} diff --git a/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/TracingFilter.java b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/TracingFilter.java new file mode 100644 index 000000000000..72865eaa7600 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/TracingFilter.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.restlet.v1_0; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTROLLER; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.restlet.v1_0.internal.RestletServerSpanNaming; +import org.restlet.Filter; +import org.restlet.data.Request; +import org.restlet.data.Response; + +final class TracingFilter extends Filter { + + private final Instrumenter instrumenter; + private final String path; + + public TracingFilter(Instrumenter instrumenter, String path) { + this.instrumenter = instrumenter; + this.path = path; + } + + @Override + public int doHandle(Request request, Response response) { + + Context parentContext = Context.current(); + Context context = parentContext; + + Scope scope = null; + + if (instrumenter.shouldStart(parentContext, request)) { + context = instrumenter.start(parentContext, request); + scope = context.makeCurrent(); + } + + ServerSpanNaming.updateServerSpanName( + context, CONTROLLER, RestletServerSpanNaming.SERVER_SPAN_NAME, path); + + Throwable statusThrowable = null; + try { + super.doHandle(request, response); + } catch (Throwable t) { + statusThrowable = t; + } + + if (scope == null) { + return CONTINUE; + } + + scope.close(); + + if (response.getStatus() != null && response.getStatus().isError()) { + statusThrowable = response.getStatus().getThrowable(); + } + + instrumenter.end(context, request, response, statusThrowable); + + return CONTINUE; + } +} diff --git a/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/internal/RestletServerSpanNaming.java b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/internal/RestletServerSpanNaming.java new file mode 100644 index 000000000000..8db5ed94710b --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_0/internal/RestletServerSpanNaming.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.restlet.v1_0.internal; + +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNameSupplier; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; + +public final class RestletServerSpanNaming { + + public static final ServerSpanNameSupplier SERVER_SPAN_NAME = + (context, pattern) -> { + if (pattern == null || pattern.equals("")) { + return null; + } + + return ServletContextPath.prepend(context, pattern); + }; + + private RestletServerSpanNaming() {} +} diff --git a/instrumentation/restlet/restlet-1.0/library/src/test/groovy/io/opententelemetry/instrumentation/restlet/v1_0/RestletServerTest.groovy b/instrumentation/restlet/restlet-1.0/library/src/test/groovy/io/opententelemetry/instrumentation/restlet/v1_0/RestletServerTest.groovy new file mode 100644 index 000000000000..18efd7bfbd7d --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/library/src/test/groovy/io/opententelemetry/instrumentation/restlet/v1_0/RestletServerTest.groovy @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opententelemetry.instrumentation.restlet.v1_0 + +import com.noelios.restlet.StatusFilter +import io.opentelemetry.instrumentation.restlet.v1_0.AbstractRestletServerTest +import io.opentelemetry.instrumentation.restlet.v1_0.RestletTracing +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import org.restlet.Restlet + +class RestletServerTest extends AbstractRestletServerTest implements LibraryTestTrait { + + @Override + Restlet wrapRestlet(Restlet restlet, String path){ + + RestletTracing tracing = RestletTracing.create(openTelemetry) + + def tracingFilter = tracing.newFilter(path) + def statusFilter = new StatusFilter(component.getContext(), false, null, null) + + tracingFilter.setNext(statusFilter) + statusFilter.setNext(restlet) + + return tracingFilter + } + +} diff --git a/instrumentation/restlet/restlet-1.0/testing/build.gradle.kts b/instrumentation/restlet/restlet-1.0/testing/build.gradle.kts new file mode 100644 index 000000000000..06d1f438a1a5 --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/testing/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("otel.java-conventions") +} + +repositories { + mavenCentral() + maven("https://maven.restlet.talend.com/") + mavenLocal() +} + +dependencies { + api(project(":testing-common")) + + implementation("org.restlet:org.restlet:1.1.5") + implementation("com.noelios.restlet:com.noelios.restlet:1.1.5") + + implementation("org.codehaus.groovy:groovy-all") + implementation("io.opentelemetry:opentelemetry-api") + implementation("org.spockframework:spock-core") +} diff --git a/instrumentation/restlet/restlet-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_0/AbstractRestletServerTest.groovy b/instrumentation/restlet/restlet-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_0/AbstractRestletServerTest.groovy new file mode 100644 index 000000000000..438cefabdfae --- /dev/null +++ b/instrumentation/restlet/restlet-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_0/AbstractRestletServerTest.groovy @@ -0,0 +1,172 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.restlet.v1_0 + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.restlet.Component +import org.restlet.Context +import org.restlet.Redirector +import org.restlet.Restlet +import org.restlet.Server +import org.restlet.VirtualHost +import org.restlet.data.MediaType +import org.restlet.data.Protocol +import org.restlet.data.Request +import org.restlet.data.Response +import org.restlet.data.Status + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +abstract class AbstractRestletServerTest extends HttpServerTest { + + Component component + VirtualHost host + + @Override + Server startServer(int port) { + + component = new Component() + def server = component.getServers().add(Protocol.HTTP, port) + + host = component.getDefaultHost() + attachRestlets() + component.start() + + return server + } + + @Override + void stopServer(Server server) { + component.stop() + } + + def attachAndWrap(path, restlet){ + host.attach(path, wrapRestlet(restlet, path)) + } + + def attachRestlets(){ + attachAndWrap(SUCCESS.path, new Restlet() { + @Override + void handle(Request request, Response response) { + controller(SUCCESS) { + response.setEntity(SUCCESS.body, MediaType.TEXT_PLAIN) + response.setStatus(Status.valueOf(SUCCESS.status), SUCCESS.body) + } + } + }) + + attachAndWrap(REDIRECT.path, new Redirector(Context.getCurrent(), REDIRECT.body, Redirector.MODE_CLIENT_FOUND) { + @Override + void handle(Request request, Response response){ + super.handle(request, response) + controller(REDIRECT){ + } //TODO: check why handle fails inside controller + } + }) + + attachAndWrap(ERROR.path, new Restlet(){ + @Override + void handle(Request request, Response response){ + controller(ERROR){ + response.setStatus(Status.valueOf(ERROR.getStatus()), ERROR.getBody()) + } + } + }) + + attachAndWrap(EXCEPTION.path, new Restlet(){ + @Override + void handle(Request request, Response response){ + controller(EXCEPTION){ + throw new Exception(EXCEPTION.getBody()) + } + } + }) + + attachAndWrap(QUERY_PARAM.path, new Restlet() { + @Override + void handle(Request request, Response response){ + controller(QUERY_PARAM){ + response.setEntity(QUERY_PARAM.getBody(), MediaType.TEXT_PLAIN) + response.setStatus(Status.valueOf(QUERY_PARAM.getStatus()), QUERY_PARAM.getBody()) + } + } + }) + + attachAndWrap(NOT_FOUND.path, new Restlet() { + @Override + void handle(Request request, Response response){ + controller(NOT_FOUND){ + response.setEntity(NOT_FOUND.getBody(), MediaType.TEXT_PLAIN) + response.setStatus(Status.valueOf(NOT_FOUND.getStatus()), NOT_FOUND.getBody()) + } + } + }) + + attachAndWrap("/path/{id}/param", new Restlet(){ + @Override + void handle(Request request, Response response) { + controller(PATH_PARAM) { + response.setEntity(PATH_PARAM.getBody(), MediaType.TEXT_PLAIN) + response.setStatus(Status.valueOf(PATH_PARAM.getStatus()), PATH_PARAM.getBody()) + } + } + }) + + attachAndWrap(INDEXED_CHILD.path, new Restlet() { + @Override + void handle(Request request, Response response) { + controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes {request.getOriginalRef().getQueryAsForm().getFirst(it).getValue() } + response.setStatus(Status.valueOf(INDEXED_CHILD.status)) + } + } + }) + + } + + @Override + List> extraAttributes() { + [ + SemanticAttributes.HTTP_TARGET, + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.NET_TRANSPORT, + ] + } + + @Override + boolean testPathParam() { + true + } + + @Override + boolean testConcurrency() { + true + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return getContextPath() + "/path/{id}/param" + case NOT_FOUND: + return getContextPath() + "/notFound" + default: + return endpoint.resolvePath(address).path + } + } + + abstract Restlet wrapRestlet(Restlet restlet, String path) + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 75053c1f4e4d..5363573733ff 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -279,6 +279,9 @@ include(":instrumentation:reactor-netty:reactor-netty-0.9:javaagent") include(":instrumentation:reactor-netty:reactor-netty-1.0:javaagent") include(":instrumentation:rediscala-1.8:javaagent") include(":instrumentation:redisson-3.0:javaagent") +include(":instrumentation:restlet:restlet-1.0:javaagent") +include(":instrumentation:restlet:restlet-1.0:library") +include(":instrumentation:restlet:restlet-1.0:testing") include(":instrumentation:rmi:bootstrap") include(":instrumentation:rmi:javaagent") include(":instrumentation:rocketmq-client-4.8:javaagent")