diff --git a/.fossa.yml b/.fossa.yml index 161ceb50792f..53d88b206ce8 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -913,6 +913,9 @@ targets: - type: gradle path: ./ target: ':instrumentation:servlet:servlet-3.0:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:servlet:servlet-3.0:library' - type: gradle path: ./ target: ':instrumentation:servlet:servlet-5.0:javaagent' diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterInitAdvice.java b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterInitAdvice.java index 5bddc729c9e1..3343b4238a5b 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterInitAdvice.java +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterInitAdvice.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; -import static io.opentelemetry.javaagent.instrumentation.servlet.v3_0.Servlet3Singletons.FILTER_MAPPING_RESOLVER_FACTORY; +import static io.opentelemetry.javaagent.instrumentation.servlet.v3_0.Servlet3Singletons.FILTER_MAPPING_RESOLVER; import javax.servlet.Filter; import javax.servlet.FilterConfig; @@ -20,7 +20,6 @@ public static void filterInit( if (filterConfig == null) { return; } - FILTER_MAPPING_RESOLVER_FACTORY.set( - filter, new Servlet3FilterMappingResolverFactory(filterConfig)); + FILTER_MAPPING_RESOLVER.set(filter, new Servlet3FilterMappingResolverFactory(filterConfig)); } } diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InitAdvice.java b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InitAdvice.java index 9c105d27464a..57fa29af91ba 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InitAdvice.java +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InitAdvice.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; -import static io.opentelemetry.javaagent.instrumentation.servlet.v3_0.Servlet3Singletons.SERVLET_MAPPING_RESOLVER_FACTORY; +import static io.opentelemetry.javaagent.instrumentation.servlet.v3_0.Servlet3Singletons.SERVLET_MAPPING_RESOLVER; import javax.servlet.Servlet; import javax.servlet.ServletConfig; @@ -20,7 +20,6 @@ public static void servletInit( if (servletConfig == null) { return; } - SERVLET_MAPPING_RESOLVER_FACTORY.set( - servlet, new Servlet3MappingResolverFactory(servletConfig)); + SERVLET_MAPPING_RESOLVER.set(servlet, new Servlet3MappingResolverFactory(servletConfig)); } } diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Singletons.java b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Singletons.java index 17c7082b6f52..73c3228d149d 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Singletons.java +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Singletons.java @@ -24,14 +24,6 @@ public final class Servlet3Singletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.servlet-3.0"; - public static final VirtualField - SERVLET_MAPPING_RESOLVER_FACTORY = - VirtualField.find(Servlet.class, MappingResolver.Factory.class); - - public static final VirtualField - FILTER_MAPPING_RESOLVER_FACTORY = - VirtualField.find(Filter.class, MappingResolver.Factory.class); - private static final Instrumenter< ServletRequestContext, ServletResponseContext> INSTRUMENTER = @@ -41,9 +33,9 @@ public final class Servlet3Singletons { private static final ServletHelper HELPER = new ServletHelper<>(INSTRUMENTER, Servlet3Accessor.INSTANCE); - private static final VirtualField SERVLET_MAPPING_RESOLVER = + public static final VirtualField SERVLET_MAPPING_RESOLVER = VirtualField.find(Servlet.class, MappingResolver.Factory.class); - private static final VirtualField FILTER_MAPPING_RESOLVER = + public static final VirtualField FILTER_MAPPING_RESOLVER = VirtualField.find(Filter.class, MappingResolver.Factory.class); private static final Instrumenter RESPONSE_INSTRUMENTER = diff --git a/instrumentation/servlet/servlet-3.0/library/README.md b/instrumentation/servlet/servlet-3.0/library/README.md new file mode 100644 index 000000000000..df294859efbe --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/README.md @@ -0,0 +1,52 @@ +# Library Instrumentation for Java Servlet version 3.0 and higher + +Provides OpenTelemetry instrumentation for Java Servlets through a servlet filter. + +## Quickstart + +### Add these dependencies to your project + +Replace `OPENTELEMETRY_VERSION` with +the [latest release](https://central.sonatype.com/artifact/io.opentelemetry.instrumentation/opentelemetry-lettuce-5.1). + +For Maven, add to your `pom.xml` dependencies: + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-servlet-3.0 + OPENTELEMETRY_VERSION + + +``` + +For Gradle, add to your dependencies: + +```kotlin +implementation("io.opentelemetry.instrumentation:opentelemetry-servlet-3.0:OPENTELEMETRY_VERSION") +``` + +### Usage + +Add the filter to your `web.xml` + +```xml + + + OpenTelemetryServletFilter + io.opentelemetry.instrumentation.servlet.v3_0.OpenTelemetryServletFilter + + true + + + OpenTelemetryServletFilter + /* + + +``` + +Note: GlobalOpenTelemetry must be set before filter initialization. If you are unable to ensure it +is set first, consider creating a subclass of `OpenTelemetryServletFilter` that handles +initialization of GlobalOpenTelemetry in a static initializer or constructor. diff --git a/instrumentation/servlet/servlet-3.0/library/build.gradle.kts b/instrumentation/servlet/servlet-3.0/library/build.gradle.kts new file mode 100644 index 000000000000..019c57b6df50 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + compileOnly("javax.servlet:javax.servlet-api:3.0.1") + + testLibrary("org.eclipse.jetty:jetty-server:8.0.0.v20110901") + testLibrary("org.eclipse.jetty:jetty-servlet:8.0.0.v20110901") + testLibrary("org.apache.tomcat.embed:tomcat-embed-core:8.0.41") + testLibrary("org.apache.tomcat.embed:tomcat-embed-jasper:8.0.41") + + latestDepTestLibrary("org.eclipse.jetty:jetty-server:10.+") // see servlet-5.0 module + latestDepTestLibrary("org.eclipse.jetty:jetty-servlet:10.+") // see servlet-5.0 module + + latestDepTestLibrary("org.apache.tomcat.embed:tomcat-embed-core:9.+") // see servlet-5.0 module + latestDepTestLibrary("org.apache.tomcat.embed:tomcat-embed-jasper:9.+") // see servlet-5.0 module +} + +tasks { + withType().configureEach { + // required on jdk17+ to allow tomcat to shutdown properly. + jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + } +} + +// Servlet 3.0 in latest Jetty versions requires Java 11 +// However, projects that depend on this module are still be using Java 8 in testLatestDeps mode +// Therefore, we need a separate project for servlet 3.0 tests +val latestDepTest = findProperty("testLatestDeps") as Boolean + +if (latestDepTest) { + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OpenTelemetryServletFilter.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OpenTelemetryServletFilter.java new file mode 100644 index 000000000000..b854784b0805 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OpenTelemetryServletFilter.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3Singletons.FILTER_MAPPING_RESOLVER; + +import io.opentelemetry.instrumentation.servlet.v3_0.copied.CallDepth; +import io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3FilterMappingResolverFactory; +import io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3RequestAdviceScope; +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * OpenTelemetry Library instrumentation for Java Servlet based applications that can't use a Java + * Agent. Due to inherit limitations in the servlet filter API, instrumenting at the filter level + * will miss anything that happens earlier in the filter stack or problems handled directly by the + * app server. For this reason, Java Agent instrumentation is preferred when possible. + */ +@WebFilter("/*") +public class OpenTelemetryServletFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { + FILTER_MAPPING_RESOLVER.set(this, new Servlet3FilterMappingResolverFactory(filterConfig)); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + // Only HttpServlets are supported. + if (!(request instanceof HttpServletRequest && response instanceof HttpServletResponse)) { + chain.doFilter(request, response); + return; + } + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + Throwable throwable = null; + Servlet3RequestAdviceScope adviceScope = + new Servlet3RequestAdviceScope( + CallDepth.forClass(OpenTelemetryServletFilter.class), httpRequest, httpResponse, this); + try { + chain.doFilter( + new OtelHttpServletRequest(httpRequest), new OtelHttpServletResponse(httpResponse)); + } catch (Throwable e) { + throwable = e; + throw e; + } finally { + adviceScope.exit(throwable, httpRequest, httpResponse); + } + } + + @Override + public void destroy() {} +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelAsyncContext.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelAsyncContext.java new file mode 100644 index 000000000000..f9f09234ccd3 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelAsyncContext.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3Singletons.helper; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncListener; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +/// Delegates all methods except [#start(Runnable) which wraps the [Runnable]. +public class OtelAsyncContext implements AsyncContext { + private final AsyncContext delegate; + + public OtelAsyncContext(AsyncContext delegate) { + this.delegate = delegate; + } + + @Override + public ServletRequest getRequest() { + return delegate.getRequest(); + } + + @Override + public ServletResponse getResponse() { + return delegate.getResponse(); + } + + @Override + public boolean hasOriginalRequestAndResponse() { + return delegate.hasOriginalRequestAndResponse(); + } + + @Override + public void dispatch() { + delegate.dispatch(); + } + + @Override + public void dispatch(String path) { + delegate.dispatch(path); + } + + @Override + public void dispatch(ServletContext context, String path) { + delegate.dispatch(context, path); + } + + @Override + public void complete() { + delegate.complete(); + } + + @Override + public void start(Runnable run) { + delegate.start(helper().wrapAsyncRunnable(run)); + } + + @Override + public void addListener(AsyncListener listener) { + delegate.addListener(listener); + } + + @Override + public void addListener( + AsyncListener listener, ServletRequest servletRequest, ServletResponse servletResponse) { + delegate.addListener(listener, servletRequest, servletResponse); + } + + @Override + public T createListener(Class clazz) throws ServletException { + return delegate.createListener(clazz); + } + + @Override + public void setTimeout(long timeout) { + delegate.setTimeout(timeout); + } + + @Override + public long getTimeout() { + return delegate.getTimeout(); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelHttpServletRequest.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelHttpServletRequest.java new file mode 100644 index 000000000000..6f2fd5d1897a --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelHttpServletRequest.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3Singletons.helper; + +import io.opentelemetry.context.Context; +import javax.servlet.AsyncContext; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +/// Wrapper around [HttpServletRequest] that attaches an async listener if [#startAsync()] is +/// invoked and a wrapper around [#getAsyncContext()] to capture exceptions from async [Runnable]s. +public class OtelHttpServletRequest extends HttpServletRequestWrapper { + + public OtelHttpServletRequest(HttpServletRequest request) { + super(request); + } + + @Override + public AsyncContext getAsyncContext() { + return new OtelAsyncContext(super.getAsyncContext()); + } + + @Override + public AsyncContext startAsync() { + try { + return new OtelAsyncContext(super.startAsync()); + } finally { + helper().attachAsyncListener(this, Context.current()); + } + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) { + try { + return new OtelAsyncContext(super.startAsync(servletRequest, servletResponse)); + } finally { + helper().attachAsyncListener(this, Context.current()); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelHttpServletResponse.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelHttpServletResponse.java new file mode 100644 index 000000000000..068b2c1f754c --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/OtelHttpServletResponse.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import io.opentelemetry.instrumentation.servlet.v3_0.copied.CallDepth; +import io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3ResponseAdviceScope; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/// Wrapper around [HttpServletResponse]. +public class OtelHttpServletResponse extends HttpServletResponseWrapper { + + public OtelHttpServletResponse(HttpServletResponse response) { + super(response); + } + + @Override + public void sendError(int sc, String msg) throws IOException { + Servlet3ResponseAdviceScope scope = + new Servlet3ResponseAdviceScope( + CallDepth.forClass(HttpServletResponse.class), this.getClass(), "sendError"); + Throwable throwable = null; + try { + super.sendError(sc, msg); + } catch (Throwable ex) { + throwable = ex; + throw ex; + } finally { + scope.exit(throwable); + } + } + + @Override + public void sendError(int sc) throws IOException { + Servlet3ResponseAdviceScope scope = + new Servlet3ResponseAdviceScope( + CallDepth.forClass(HttpServletResponse.class), this.getClass(), "sendError"); + Throwable throwable = null; + try { + super.sendError(sc); + } catch (Throwable ex) { + throwable = ex; + throw ex; + } finally { + scope.exit(throwable); + } + } + + @Override + public void sendRedirect(String location) throws IOException { + Servlet3ResponseAdviceScope scope = + new Servlet3ResponseAdviceScope( + CallDepth.forClass(HttpServletResponse.class), this.getClass(), "sendRedirect"); + Throwable throwable = null; + try { + super.sendRedirect(location); + } catch (Throwable ex) { + throwable = ex; + throw ex; + } finally { + scope.exit(throwable); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AgentCommonConfig.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AgentCommonConfig.java new file mode 100644 index 000000000000..dfb5365a8700 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AgentCommonConfig.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class AgentCommonConfig { + private AgentCommonConfig() {} + + private static final CommonConfig instance = new CommonConfig(AgentInstrumentationConfig.get()); + + public static CommonConfig get() { + return instance; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AgentInstrumentationConfig.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AgentInstrumentationConfig.java new file mode 100644 index 000000000000..57f9cfedb4b5 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AgentInstrumentationConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import java.util.logging.Logger; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class AgentInstrumentationConfig { + private AgentInstrumentationConfig() {} + + private static final Logger logger = Logger.getLogger(AgentInstrumentationConfig.class.getName()); + + private static final InstrumentationConfig DEFAULT = new EmptyInstrumentationConfig(); + + // lazy initialized, so that javaagent can set it + private static volatile InstrumentationConfig instance = DEFAULT; + + /** + * Sets the instrumentation configuration singleton. This method is only supposed to be called + * once, during the agent initialization, just before {@link AgentInstrumentationConfig#get()} is + * used for the first time. + * + *

This method is internal and is hence not for public use. Its API is unstable and can change + * at any time. + */ + public static void internalInitializeConfig(InstrumentationConfig config) { + if (instance != DEFAULT) { + logger.warning("InstrumentationConfig#instance was already set earlier"); + return; + } + instance = requireNonNull(config); + } + + /** Returns the global instrumentation configuration. */ + public static InstrumentationConfig get() { + return instance; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AppServerBridge.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AppServerBridge.java new file mode 100644 index 000000000000..e27c33d55423 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AppServerBridge.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import javax.annotation.Nullable; + +/** + * Helper container for Context attributes for transferring certain information between servlet + * integration and app-server server handler integrations. + */ +public class AppServerBridge { + + private static final ContextKey CONTEXT_KEY = + ContextKey.named("opentelemetry-servlet-app-server-bridge"); + + private final boolean servletShouldRecordException; + private boolean captureServletAttributes; + private Throwable exception; + + private AppServerBridge(Builder builder) { + servletShouldRecordException = builder.recordException; + captureServletAttributes = builder.captureServletAttributes; + } + + /** + * Record exception that happened during servlet invocation so that app server instrumentation can + * add it to server span. + * + * @param context server context + * @param exception exception that happened during servlet invocation + */ + public static void recordException(Context context, Throwable exception) { + AppServerBridge appServerBridge = context.get(AppServerBridge.CONTEXT_KEY); + if (appServerBridge != null && appServerBridge.servletShouldRecordException) { + appServerBridge.exception = exception; + } + } + + /** + * Get exception that happened during servlet invocation. + * + * @param context server context + * @return exception that happened during servlet invocation + */ + @Nullable + public static Throwable getException(Context context) { + AppServerBridge appServerBridge = context.get(AppServerBridge.CONTEXT_KEY); + if (appServerBridge != null) { + return appServerBridge.exception; + } + return null; + } + + /** + * Test whether servlet attributes should be captured. This method will return true only on the + * first call with given context. + * + * @param context server context + * @return true when servlet attributes should be captured + */ + public static boolean captureServletAttributes(Context context) { + AppServerBridge appServerBridge = context.get(AppServerBridge.CONTEXT_KEY); + if (appServerBridge != null) { + boolean result = appServerBridge.captureServletAttributes; + appServerBridge.captureServletAttributes = false; + return result; + } + return false; + } + + /** + * Class used as key in CallDepthThreadLocalMap for counting servlet invocation depth in + * Servlet3Advice and Servlet2Advice. We can not use helper classes like Servlet3Advice and + * Servlet2Advice for determining call depth of server invocation because they can be injected + * into multiple class loaders. + * + * @return class used as a key in CallDepthThreadLocalMap for counting servlet invocation depth + */ + public static Class getCallDepthKey() { + class Key {} + + return Key.class; + } + + public static class Builder { + boolean recordException; + boolean captureServletAttributes; + + /** + * Use on servers where exceptions thrown during servlet invocation are not propagated to the + * method where server span is closed. Recorded exception can be retrieved by calling {@link + * #getException(Context)} + * + * @return this builder. + */ + @CanIgnoreReturnValue + public Builder recordException() { + recordException = true; + return this; + } + + /** + * Use on servers where server instrumentation is not based on servlet instrumentation. Setting + * this flag lets servlet instrumentation know that it should augment server span with servlet + * specific attributes. + * + * @return this builder. + */ + @CanIgnoreReturnValue + public Builder captureServletAttributes() { + captureServletAttributes = true; + return this; + } + + /** + * Attach AppServerBridge to context. + * + * @param context server context + * @return new context with AppServerBridge attached. + */ + public Context init(Context context) { + Context result = context.with(AppServerBridge.CONTEXT_KEY, new AppServerBridge(this)); + result = ServletAsyncContext.init(result); + return result; + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AsyncRequestCompletionListener.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AsyncRequestCompletionListener.java new file mode 100644 index 000000000000..eee17d055527 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AsyncRequestCompletionListener.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.concurrent.atomic.AtomicBoolean; + +public class AsyncRequestCompletionListener + implements ServletAsyncListener { + private final ServletHelper servletHelper; + private final Instrumenter, ServletResponseContext> + instrumenter; + private final ServletRequestContext requestContext; + private final Context context; + private final AtomicBoolean responseHandled = new AtomicBoolean(); + + public AsyncRequestCompletionListener( + ServletHelper servletHelper, + Instrumenter, ServletResponseContext> instrumenter, + ServletRequestContext requestContext, + Context context) { + // The context passed into this method may contain other spans besides the server span. To end + // the server span we get the context that set at the start of the request with + // ServletHelper#setAsyncListenerResponse that contains just the server span. + Context serverSpanContext = servletHelper.getAsyncListenerContext(context); + this.servletHelper = servletHelper; + this.instrumenter = instrumenter; + this.requestContext = requestContext; + this.context = serverSpanContext != null ? serverSpanContext : context; + } + + @Override + public void onComplete(RESPONSE response) { + if (responseHandled.compareAndSet(false, true)) { + ServletResponseContext responseContext = new ServletResponseContext<>(response); + Throwable throwable = servletHelper.getAsyncException(context); + instrumenter.end(context, requestContext, responseContext, throwable); + } + } + + @Override + public void onTimeout(long timeout) { + if (responseHandled.compareAndSet(false, true)) { + RESPONSE response = servletHelper.getAsyncListenerResponse(context); + ServletResponseContext responseContext = new ServletResponseContext<>(response); + responseContext.setTimeout(timeout); + Throwable throwable = servletHelper.getAsyncException(context); + instrumenter.end(context, requestContext, responseContext, throwable); + } + } + + @Override + public void onError(Throwable throwable, RESPONSE response) { + if (responseHandled.compareAndSet(false, true)) { + ServletResponseContext responseContext = new ServletResponseContext<>(response); + instrumenter.end(context, requestContext, responseContext, throwable); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AsyncRunnableWrapper.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AsyncRunnableWrapper.java new file mode 100644 index 000000000000..99ab7e354f16 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/AsyncRunnableWrapper.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class AsyncRunnableWrapper implements Runnable { + private final ServletHelper helper; + private final Runnable runnable; + private final Context context; + + private AsyncRunnableWrapper(ServletHelper helper, Runnable runnable) { + this.helper = helper; + this.runnable = runnable; + this.context = Context.current(); + } + + public static Runnable wrap(ServletHelper helper, Runnable runnable) { + if (runnable == null || runnable instanceof AsyncRunnableWrapper) { + return runnable; + } + return new AsyncRunnableWrapper<>(helper, runnable); + } + + @Override + public void run() { + try (Scope scope = context.makeCurrent()) { + runnable.run(); + } catch (Throwable throwable) { + helper.recordAsyncException(context, throwable); + throw throwable; + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/BaseServletHelper.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/BaseServletHelper.java new file mode 100644 index 000000000000..4aeadf98c297 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/BaseServletHelper.java @@ -0,0 +1,174 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.SERVER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.SERVER_FILTER; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import java.security.Principal; +import java.util.function.Function; + +@SuppressWarnings("deprecation") // using deprecated semconv +public abstract class BaseServletHelper { + protected final Instrumenter, ServletResponseContext> + instrumenter; + protected final ServletAccessor accessor; + private final ServletSpanNameProvider spanNameProvider; + private final Function contextPathExtractor; + private final ServletRequestParametersExtractor parameterExtractor; + + protected BaseServletHelper( + Instrumenter, ServletResponseContext> instrumenter, + ServletAccessor accessor) { + this.instrumenter = instrumenter; + this.accessor = accessor; + this.spanNameProvider = new ServletSpanNameProvider<>(accessor); + this.contextPathExtractor = accessor::getRequestContextPath; + this.parameterExtractor = + ServletRequestParametersExtractor.enabled() + ? new ServletRequestParametersExtractor<>(accessor) + : null; + } + + public boolean shouldStart(Context parentContext, ServletRequestContext requestContext) { + return instrumenter.shouldStart(parentContext, requestContext); + } + + public Context start(Context parentContext, ServletRequestContext requestContext) { + Context context = instrumenter.start(parentContext, requestContext); + + REQUEST request = requestContext.request(); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + // we do this e.g. so that servlet containers can use these values in their access logs + // TODO: These are only available when using servlet instrumentation or when server + // instrumentation extends servlet instrumentation e.g. jetty. Either remove or make sure they + // also work on tomcat and wildfly. + accessor.setRequestAttribute(request, "trace_id", spanContext.getTraceId()); + accessor.setRequestAttribute(request, "span_id", spanContext.getSpanId()); + + context = addServletContextPath(context, request); + context = addAsyncContext(context); + + attachServerContext(context, request); + + return context; + } + + protected Context addServletContextPath(Context context, REQUEST request) { + return ServletContextPath.init(context, contextPathExtractor, request); + } + + protected Context addAsyncContext(Context context) { + return ServletAsyncContext.init(context); + } + + public Context getServerContext(REQUEST request) { + Object context = accessor.getRequestAttribute(request, ServletHelper.CONTEXT_ATTRIBUTE); + return context instanceof Context ? (Context) context : null; + } + + private void attachServerContext(Context context, REQUEST request) { + accessor.setRequestAttribute(request, ServletHelper.CONTEXT_ATTRIBUTE, context); + } + + public void recordException(Context context, Throwable throwable) { + AppServerBridge.recordException(context, throwable); + } + + public Context updateContext( + Context context, REQUEST request, MappingResolver mappingResolver, boolean servlet) { + Context result = addServletContextPath(context, request); + result = addAsyncContext(result); + + if (mappingResolver != null) { + HttpServerRoute.update( + result, servlet ? SERVER : SERVER_FILTER, spanNameProvider, mappingResolver, request); + } + + return result; + } + + /** + * Capture servlet specific span attributes when SERVER span is not create by servlet + * instrumentation. + */ + public void captureServletAttributes(Context context, REQUEST request) { + if (!AppServerBridge.captureServletAttributes(context)) { + return; + } + Span serverSpan = LocalRootSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + + captureRequestParameters(serverSpan, request); + captureEnduserId(serverSpan, request); + } + + /** + * Capture servlet request parameters as span attributes when SERVER span is not create by servlet + * instrumentation. + * + *

When SERVER span is created by servlet instrumentation we register {@link + * ServletRequestParametersExtractor} as an attribute extractor. When SERVER span is not created + * by servlet instrumentation we call this method on exit from the last servlet or filter. + */ + private void captureRequestParameters(Span serverSpan, REQUEST request) { + if (parameterExtractor == null) { + return; + } + + parameterExtractor.setAttributes(request, serverSpan::setAttribute); + } + + /** + * Capture {@link ServletAdditionalAttributesExtractor#ENDUSER_ID} as span attributes when SERVER + * span is not create by servlet instrumentation. + * + *

When SERVER span is created by servlet instrumentation we register {@link + * ServletAdditionalAttributesExtractor} as an attribute extractor. When SERVER span is not + * created by servlet instrumentation we call this method on exit from the last servlet or filter. + */ + private void captureEnduserId(Span serverSpan, REQUEST request) { + if (!AgentCommonConfig.get().getEnduserConfig().isIdEnabled()) { + return; + } + + Principal principal = accessor.getRequestUserPrincipal(request); + if (principal != null) { + String name = principal.getName(); + if (name != null) { + serverSpan.setAttribute(ServletAdditionalAttributesExtractor.ENDUSER_ID, name); + } + } + } + + /* + Given request already has a context associated with it. + As there should not be nested spans of kind SERVER, we should NOT create a new span here. + + But it may happen that there is no span in current Context or it is from a different trace. + E.g. in case of async servlet request processing we create span for incoming request in one thread, + but actual request continues processing happens in another thread. + Depending on servlet container implementation, this processing may again arrive into this method. + E.g. Jetty handles async requests in a way that calls HttpServlet.service method twice. + + In this case we have to put the span from the request into current context before continuing. + */ + public boolean needsRescoping(Context currentContext, Context attachedContext) { + return !sameTrace(Span.fromContext(currentContext), Span.fromContext(attachedContext)); + } + + private static boolean sameTrace(Span oneSpan, Span otherSpan) { + return oneSpan.getSpanContext().getTraceId().equals(otherSpan.getSpanContext().getTraceId()); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/CallDepth.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/CallDepth.java new file mode 100644 index 000000000000..03565046cc67 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/CallDepth.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +/** + * A utility to track nested calls in an instrumentation. + * + *

For example, this can be used to track nested calls to {@code super()} in constructors by + * calling {@link #getAndIncrement()} at the beginning of each constructor. + * + *

This works the following way: when you enter some method that you want to track, you call + * {@link #getAndIncrement()} method. If returned number is larger than 0, then you have already + * been in this method and are in recursive call now. When you then leave the method, you call + * {@link #decrementAndGet()} method. If returned number is larger than 0, then you have already + * been in this method and are in recursive call now. + * + *

In short, the semantic of both methods is the same: they will return value 0 if and only if + * current method invocation is the first one for the current call stack. + */ +public final class CallDepth { + + private int depth; + + CallDepth() { + this.depth = 0; + } + + /** + * Return the current call depth for a given class (not method; we want to be able to track calls + * between different methods in a class). + * + *

The returned instance is unique per given class and per thread. + */ + public static CallDepth forClass(Class cls) { + return CallDepthThreadLocalMap.getCallDepth(cls); + } + + /** + * Increment the current call depth and return the previous value. This method will always return + * 0 if it's the first (outermost) call. + */ + public int getAndIncrement() { + return this.depth++; + } + + /** + * Decrement the current call depth and return the current value. This method will always return 0 + * if it's the last (outermost) call. + */ + public int decrementAndGet() { + return --this.depth; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/CallDepthThreadLocalMap.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/CallDepthThreadLocalMap.java new file mode 100644 index 000000000000..1b4bdb7a10f5 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/CallDepthThreadLocalMap.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +final class CallDepthThreadLocalMap { + + private static final ClassValue TLS = + new ClassValue() { + @Override + protected ThreadLocalDepth computeValue(Class type) { + return new ThreadLocalDepth(); + } + }; + + static CallDepth getCallDepth(Class k) { + return TLS.get(k).get(); + } + + private static final class ThreadLocalDepth extends ThreadLocal { + @Override + protected CallDepth initialValue() { + return new CallDepth(); + } + } + + private CallDepthThreadLocalMap() {} +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/EmptyInstrumentationConfig.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/EmptyInstrumentationConfig.java new file mode 100644 index 000000000000..6dd67a05120d --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/EmptyInstrumentationConfig.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.api.incubator.config.ConfigProvider; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +final class EmptyInstrumentationConfig implements InstrumentationConfig { + + @Nullable + @Override + public String getString(String name) { + return null; + } + + @Override + public String getString(String name, String defaultValue) { + return defaultValue; + } + + @Override + public boolean getBoolean(String name, boolean defaultValue) { + return defaultValue; + } + + @Override + public int getInt(String name, int defaultValue) { + return defaultValue; + } + + @Override + public long getLong(String name, long defaultValue) { + return defaultValue; + } + + @Override + public double getDouble(String name, double defaultValue) { + return defaultValue; + } + + @Override + public Duration getDuration(String name, Duration defaultValue) { + return defaultValue; + } + + @Override + public List getList(String name, List defaultValue) { + return defaultValue; + } + + @Override + public Map getMap(String name, Map defaultValue) { + return defaultValue; + } + + @Override + public boolean isDeclarative() { + return false; + } + + @Override + public DeclarativeConfigProperties getDeclarativeConfig(String node) { + throw new IllegalStateException( + "Declarative configuration is not supported in the empty instrumentation config"); + } + + @Nullable + @Override + public ConfigProvider getConfigProvider() { + return null; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseCustomizer.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseCustomizer.java new file mode 100644 index 000000000000..0dab5f2e99ba --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseCustomizer.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.context.Context; + +/** + * {@link HttpServerResponseCustomizer} can be used to execute code after an HTTP server response is + * created for the purpose of mutating the response in some way, such as appending headers, that may + * depend on the context of the SERVER span. + * + *

This is a service provider interface that requires implementations to be registered in a + * provider-configuration file stored in the {@code META-INF/services} resource directory. + */ +public interface HttpServerResponseCustomizer { + /** + * Called for each HTTP server response with its SERVER span context provided. This is called at a + * time when response headers can already be set, but the response is not yet committed, which is + * typically at the start of request handling. + * + * @param serverContext Context of a SERVER span {@link io.opentelemetry.api.trace.SpanContext} + * @param response Response object specific to the library being instrumented + * @param responseMutator Mutator through which the provided response object can be mutated + */ + void customize( + Context serverContext, + RESPONSE response, + HttpServerResponseMutator responseMutator); +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseCustomizerHolder.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseCustomizerHolder.java new file mode 100644 index 000000000000..a9490de44813 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseCustomizerHolder.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.context.Context; + +/** + * Holds the currently active response customizer. This is set during agent initialization to an + * instance that calls each {@link HttpServerResponseCustomizer} found in the agent classpath. It is + * intended to be used directly from HTTP server library instrumentations, which is why this package + * is inside the bootstrap package that gets loaded in the bootstrap classloader. + */ +public final class HttpServerResponseCustomizerHolder { + private static volatile HttpServerResponseCustomizer responseCustomizer = new NoOpCustomizer(); + + public static void setCustomizer(HttpServerResponseCustomizer customizer) { + HttpServerResponseCustomizerHolder.responseCustomizer = customizer; + } + + public static HttpServerResponseCustomizer getCustomizer() { + return responseCustomizer; + } + + private HttpServerResponseCustomizerHolder() {} + + private static class NoOpCustomizer implements HttpServerResponseCustomizer { + + @Override + public void customize( + Context serverContext, T response, HttpServerResponseMutator responseMutator) {} + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseMutator.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseMutator.java new file mode 100644 index 000000000000..488c57e76f92 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServerResponseMutator.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +/** Provides the ability to mutate an instrumentation library specific response. */ +public interface HttpServerResponseMutator { + void appendHeader(RESPONSE response, String name, String value); +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServletResponseAdviceHelper.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServletResponseAdviceHelper.java new file mode 100644 index 000000000000..f2b2a8140579 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/HttpServletResponseAdviceHelper.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +public class HttpServletResponseAdviceHelper { + + public static StartResult startSpan( + Instrumenter instrumenter, Class declaringClass, String methodName) { + Context parentContext = Context.current(); + // Don't want to generate a new top-level span + if (Span.fromContext(parentContext).getSpanContext().isValid()) { + ClassAndMethod classAndMethod = ClassAndMethod.create(declaringClass, methodName); + if (instrumenter.shouldStart(parentContext, classAndMethod)) { + Context context = instrumenter.start(parentContext, classAndMethod); + Scope scope = context.makeCurrent(); + return new StartResult(classAndMethod, context, scope); + } + } + + return null; + } + + public static final class StartResult { + private final ClassAndMethod classAndMethod; + private final Context context; + private final Scope scope; + + private StartResult(ClassAndMethod classAndMethod, Context context, Scope scope) { + this.classAndMethod = classAndMethod; + this.context = context; + this.scope = scope; + } + + public ClassAndMethod getClassAndMethod() { + return classAndMethod; + } + + public Context getContext() { + return context; + } + + public Scope getScope() { + return scope; + } + } + + public static void stopSpan( + Instrumenter instrumenter, + Throwable throwable, + Context context, + Scope scope, + ClassAndMethod request) { + if (scope != null) { + scope.close(); + + instrumenter.end(context, request, null, throwable); + } + } + + private HttpServletResponseAdviceHelper() {} +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/JavaagentHttpServerInstrumenters.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/JavaagentHttpServerInstrumenters.java new file mode 100644 index 000000000000..ef7a3aaf6079 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/JavaagentHttpServerInstrumenters.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.util.function.Consumer; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class JavaagentHttpServerInstrumenters { + + private JavaagentHttpServerInstrumenters() {} + + public static Instrumenter create( + String instrumentationName, + HttpServerAttributesGetter httpAttributesGetter, + TextMapGetter headerGetter) { + return create(instrumentationName, httpAttributesGetter, headerGetter, customizer -> {}); + } + + public static Instrumenter create( + DefaultHttpServerInstrumenterBuilder builder) { + return create(builder, customizer -> {}); + } + + public static Instrumenter create( + String instrumentationName, + HttpServerAttributesGetter httpAttributesGetter, + TextMapGetter headerGetter, + Consumer> instrumenterBuilderConsumer) { + return create( + DefaultHttpServerInstrumenterBuilder.create( + instrumentationName, GlobalOpenTelemetry.get(), httpAttributesGetter, headerGetter), + instrumenterBuilderConsumer); + } + + public static Instrumenter create( + DefaultHttpServerInstrumenterBuilder builder, + Consumer> builderCustomizer) { + return builder + .configure(AgentCommonConfig.get()) + .setBuilderCustomizer(builderCustomizer) + .build(); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/JavaxServletAccessor.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/JavaxServletAccessor.java new file mode 100644 index 000000000000..f42dc3833c89 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/JavaxServletAccessor.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +public abstract class JavaxServletAccessor implements ServletAccessor { + @Override + public String getRequestContextPath(HttpServletRequest request) { + return request.getContextPath(); + } + + @Override + public String getRequestScheme(HttpServletRequest request) { + return request.getScheme(); + } + + @Override + public String getRequestUri(HttpServletRequest request) { + return request.getRequestURI(); + } + + @Override + public String getRequestQueryString(HttpServletRequest request) { + return request.getQueryString(); + } + + @Override + public Object getRequestAttribute(HttpServletRequest request, String name) { + return request.getAttribute(name); + } + + @Override + public void setRequestAttribute(HttpServletRequest request, String name, Object value) { + request.setAttribute(name, value); + } + + @Override + public String getRequestProtocol(HttpServletRequest request) { + return request.getProtocol(); + } + + @Override + public String getRequestMethod(HttpServletRequest request) { + return request.getMethod(); + } + + @Override + public String getRequestRemoteAddr(HttpServletRequest request) { + return request.getRemoteAddr(); + } + + @Override + public String getRequestHeader(HttpServletRequest request, String name) { + return request.getHeader(name); + } + + @Override + public List getRequestHeaderValues(HttpServletRequest request, String name) { + @SuppressWarnings("unchecked") + Enumeration values = request.getHeaders(name); + return values == null ? Collections.emptyList() : Collections.list(values); + } + + @Override + public Iterable getRequestHeaderNames(HttpServletRequest httpServletRequest) { + @SuppressWarnings("unchecked") + Enumeration names = httpServletRequest.getHeaderNames(); + return Collections.list(names); + } + + @Override + public List getRequestParameterValues( + HttpServletRequest httpServletRequest, String name) { + String[] values = httpServletRequest.getParameterValues(name); + return values == null ? Collections.emptyList() : Arrays.asList(values); + } + + @Override + public String getRequestServletPath(HttpServletRequest request) { + return request.getServletPath(); + } + + @Override + public String getRequestPathInfo(HttpServletRequest request) { + return request.getPathInfo(); + } + + @Override + public Principal getRequestUserPrincipal(HttpServletRequest request) { + return request.getUserPrincipal(); + } + + @Override + public boolean isServletException(Throwable throwable) { + return throwable instanceof ServletException; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/MappingResolver.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/MappingResolver.java new file mode 100644 index 000000000000..8faf16c4e0af --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/MappingResolver.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Helper class for finding a mapping that matches current request from a collection of mappings. + */ +public final class MappingResolver { + private final Set exactMatches; + private final List wildcardMatchers; + private final boolean hasDefault; + + private MappingResolver( + Set exactMatches, List wildcardMatchers, boolean hasDefault) { + this.exactMatches = exactMatches.isEmpty() ? Collections.emptySet() : exactMatches; + this.wildcardMatchers = wildcardMatchers.isEmpty() ? Collections.emptyList() : wildcardMatchers; + this.hasDefault = hasDefault; + } + + public static MappingResolver build(Collection mappings) { + List wildcardMatchers = new ArrayList<>(); + Set exactMatches = new HashSet<>(); + boolean hasDefault = false; + for (String mapping : mappings) { + if (mapping.equals("")) { + exactMatches.add("/"); + } else if (mapping.equals("/") || mapping.equals("/*")) { + hasDefault = true; + } else if (mapping.startsWith("*.") && mapping.length() > 2) { + wildcardMatchers.add(new SuffixMatcher("/" + mapping, mapping.substring(1))); + } else if (mapping.endsWith("/*")) { + wildcardMatchers.add( + new PrefixMatcher(mapping, mapping.substring(0, mapping.length() - 2))); + } else { + exactMatches.add(mapping); + } + } + + // wildfly has empty mappings for default servlet + if (mappings.isEmpty()) { + hasDefault = true; + } + + return new MappingResolver(exactMatches, wildcardMatchers, hasDefault); + } + + /** Find mapping for requested path. */ + @Nullable + public String resolve(@Nullable String servletPath, @Nullable String pathInfo) { + if (servletPath == null) { + return null; + } + + // get full path inside context + String path = servletPath; + if (pathInfo != null) { + path += pathInfo; + } + // trim trailing / + if (path.endsWith("/") && !path.equals("/")) { + path = path.substring(0, path.length() - 1); + } + + if (exactMatches.contains(path)) { + return path; + } + + for (WildcardMatcher matcher : wildcardMatchers) { + if (matcher.match(path)) { + String mapping = matcher.getMapping(); + // for jsp return servlet path + if ("/*.jsp".equals(mapping) || "/*.jspx".equals(mapping)) { + return servletPath; + } + return mapping; + } + } + + if (hasDefault) { + return path.equals("/") ? "/" : "/*"; + } + + return null; + } + + private interface WildcardMatcher { + boolean match(String path); + + String getMapping(); + } + + private static class PrefixMatcher implements WildcardMatcher { + private final String mapping; + private final String prefix; + + private PrefixMatcher(String mapping, String prefix) { + this.mapping = mapping; + this.prefix = prefix; + } + + @Override + public boolean match(String path) { + return path.equals(prefix) || path.startsWith(prefix + "/"); + } + + @Override + public String getMapping() { + return mapping; + } + } + + private static class SuffixMatcher implements WildcardMatcher { + private final String mapping; + private final String suffix; + + private SuffixMatcher(String mapping, String suffix) { + this.mapping = mapping; + this.suffix = suffix; + } + + @Override + public boolean match(String path) { + return path.endsWith(suffix); + } + + @Override + public String getMapping() { + return mapping; + } + } + + /** + * Factory interface for creating {@link MappingResolver} instances. The main reason this class is + * here is that we need to ensure that the class used for {@code VirtualField} lookup is always + * the same. If we would use an injected class it could be different in different class loaders. + */ + public interface Factory { + + @Nullable + MappingResolver get(); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ResponseInstrumenterFactory.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ResponseInstrumenterFactory.java new file mode 100644 index 000000000000..f1827c14b1de --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ResponseInstrumenterFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +public final class ResponseInstrumenterFactory { + + public static Instrumenter createInstrumenter(String instrumentationName) { + CodeAttributesGetter codeAttributesGetter = + ClassAndMethod.codeAttributesGetter(); + return Instrumenter.builder( + GlobalOpenTelemetry.get(), + instrumentationName, + CodeSpanNameExtractor.create(codeAttributesGetter)) + .addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter)) + .buildInstrumenter(); + } + + private ResponseInstrumenterFactory() {} +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3Accessor.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3Accessor.java new file mode 100644 index 000000000000..3e8d75a576c4 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3Accessor.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class Servlet3Accessor extends JavaxServletAccessor + implements HttpServerResponseMutator { + public static final Servlet3Accessor INSTANCE = new Servlet3Accessor(); + + private Servlet3Accessor() {} + + @Override + public Integer getRequestRemotePort(HttpServletRequest request) { + return request.getRemotePort(); + } + + @Override + public String getRequestLocalAddr(HttpServletRequest request) { + return request.getLocalAddr(); + } + + @Override + public Integer getRequestLocalPort(HttpServletRequest request) { + return request.getLocalPort(); + } + + @Override + public void addRequestAsyncListener( + HttpServletRequest request, + ServletAsyncListener listener, + Object response) { + if (response instanceof HttpServletResponse) { + request + .getAsyncContext() + .addListener(new Listener(listener), request, (HttpServletResponse) response); + } + } + + @Override + public int getResponseStatus(HttpServletResponse response) { + return response.getStatus(); + } + + @Override + public List getResponseHeaderValues(HttpServletResponse response, String name) { + Collection values = response.getHeaders(name); + if (values == null) { + return Collections.emptyList(); + } + if (values instanceof List) { + return (List) values; + } + return new ArrayList<>(values); + } + + @Override + public boolean isResponseCommitted(HttpServletResponse response) { + return response.isCommitted(); + } + + @Override + public void appendHeader(HttpServletResponse response, String name, String value) { + response.addHeader(name, value); + } + + private static class Listener implements AsyncListener { + private final ServletAsyncListener listener; + + private Listener(ServletAsyncListener listener) { + this.listener = listener; + } + + @Override + public void onComplete(AsyncEvent event) { + listener.onComplete((HttpServletResponse) event.getSuppliedResponse()); + } + + @Override + public void onTimeout(AsyncEvent event) { + listener.onTimeout(event.getAsyncContext().getTimeout()); + } + + @Override + public void onError(AsyncEvent event) { + listener.onError(event.getThrowable(), (HttpServletResponse) event.getSuppliedResponse()); + } + + @Override + public void onStartAsync(AsyncEvent event) { + event + .getAsyncContext() + .addListener(this, event.getSuppliedRequest(), event.getSuppliedResponse()); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3FilterMappingResolverFactory.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3FilterMappingResolverFactory.java new file mode 100644 index 000000000000..f1326e1c61dc --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3FilterMappingResolverFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import java.util.Collection; +import javax.servlet.FilterConfig; +import javax.servlet.FilterRegistration; +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; + +public class Servlet3FilterMappingResolverFactory + extends ServletFilterMappingResolverFactory { + private final FilterConfig filterConfig; + + public Servlet3FilterMappingResolverFactory(FilterConfig filterConfig) { + this.filterConfig = filterConfig; + } + + @Override + protected FilterRegistration getFilterRegistration() { + String filterName = filterConfig.getFilterName(); + ServletContext servletContext = filterConfig.getServletContext(); + if (filterName == null || servletContext == null) { + return null; + } + return servletContext.getFilterRegistration(filterName); + } + + @Override + protected Collection getUrlPatternMappings(FilterRegistration filterRegistration) { + return filterRegistration.getUrlPatternMappings(); + } + + @Override + protected Collection getServletNameMappings(FilterRegistration filterRegistration) { + return filterRegistration.getServletNameMappings(); + } + + @Override + @SuppressWarnings("ReturnsNullCollection") + protected Collection getServletMappings(String servletName) { + ServletRegistration servletRegistration = + filterConfig.getServletContext().getServletRegistration(servletName); + if (servletRegistration == null) { + return null; + } + return servletRegistration.getMappings(); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3RequestAdviceScope.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3RequestAdviceScope.java new file mode 100644 index 000000000000..5fcdf4c35754 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3RequestAdviceScope.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import static io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3Singletons.helper; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import javax.annotation.Nullable; +import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class Servlet3RequestAdviceScope { + private final CallDepth callDepth; + private final ServletRequestContext requestContext; + private final Context context; + private final Scope scope; + + public Servlet3RequestAdviceScope( + CallDepth callDepth, + HttpServletRequest request, + HttpServletResponse response, + Object servletOrFilter) { + this.callDepth = callDepth; + this.callDepth.getAndIncrement(); + + Context currentContext = Context.current(); + Context attachedContext = helper().getServerContext(request); + Context contextToUpdate; + + requestContext = new ServletRequestContext<>(request, servletOrFilter); + if (attachedContext == null && helper().shouldStart(currentContext, requestContext)) { + context = helper().start(currentContext, requestContext); + helper().setAsyncListenerResponse(context, response); + + contextToUpdate = context; + } else if (attachedContext != null + && helper().needsRescoping(currentContext, attachedContext)) { + // Given request already has a context associated with it. + // see the needsRescoping() javadoc for more explanation + contextToUpdate = attachedContext; + context = null; + } else { + // We are inside nested servlet/filter/app-server span, don't create new span + contextToUpdate = currentContext; + context = null; + } + + // Update context with info from current request to ensure that server span gets the best + // possible name. + // In case server span was created by app server instrumentations calling updateContext + // returns a new context that contains servlet context path that is used in other + // instrumentations for naming server span. + MappingResolver mappingResolver = Servlet3Singletons.getMappingResolver(servletOrFilter); + boolean servlet = servletOrFilter instanceof Servlet; + contextToUpdate = helper().updateContext(contextToUpdate, request, mappingResolver, servlet); + scope = contextToUpdate.makeCurrent(); + + if (context != null) { + // Only trigger response customizer once, so only if server span was created here + HttpServerResponseCustomizerHolder.getCustomizer() + .customize(contextToUpdate, response, Servlet3Accessor.INSTANCE); + } + } + + public void exit( + @Nullable Throwable throwable, HttpServletRequest request, HttpServletResponse response) { + + boolean topLevel = callDepth.decrementAndGet() == 0; + helper().end(requestContext, request, response, throwable, topLevel, context, scope); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3ResponseAdviceScope.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3ResponseAdviceScope.java new file mode 100644 index 000000000000..9c4568952f97 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3ResponseAdviceScope.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import static io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3Singletons.responseInstrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import javax.annotation.Nullable; + +public class Servlet3ResponseAdviceScope { + private final CallDepth callDepth; + private final ClassAndMethod classAndMethod; + private final Context context; + private final Scope scope; + + public Servlet3ResponseAdviceScope( + CallDepth callDepth, Class declaringClass, String methodName) { + this.callDepth = callDepth; + if (callDepth.getAndIncrement() > 0) { + this.classAndMethod = null; + this.context = null; + this.scope = null; + return; + } + HttpServletResponseAdviceHelper.StartResult result = + HttpServletResponseAdviceHelper.startSpan( + responseInstrumenter(), declaringClass, methodName); + if (result != null) { + classAndMethod = result.getClassAndMethod(); + context = result.getContext(); + scope = result.getScope(); + } else { + classAndMethod = null; + context = null; + scope = null; + } + } + + public void exit(@Nullable Throwable throwable) { + if (callDepth.decrementAndGet() > 0) { + return; + } + HttpServletResponseAdviceHelper.stopSpan( + responseInstrumenter(), throwable, context, scope, classAndMethod); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3Singletons.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3Singletons.java new file mode 100644 index 000000000000..3450fc72e3bc --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/Servlet3Singletons.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import javax.servlet.Filter; +import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public final class Servlet3Singletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.servlet-3.0"; + + private static final Instrumenter< + ServletRequestContext, ServletResponseContext> + INSTRUMENTER = + ServletInstrumenterBuilder.create() + .build(INSTRUMENTATION_NAME, Servlet3Accessor.INSTANCE); + + private static final ServletHelper HELPER = + new ServletHelper<>(INSTRUMENTER, Servlet3Accessor.INSTANCE); + + public static final VirtualField SERVLET_MAPPING_RESOLVER = + VirtualField.find(Servlet.class, MappingResolver.Factory.class); + public static final VirtualField FILTER_MAPPING_RESOLVER = + VirtualField.find(Filter.class, MappingResolver.Factory.class); + + private static final Instrumenter RESPONSE_INSTRUMENTER = + ResponseInstrumenterFactory.createInstrumenter(INSTRUMENTATION_NAME); + + public static ServletHelper helper() { + return HELPER; + } + + public static Instrumenter responseInstrumenter() { + return RESPONSE_INSTRUMENTER; + } + + public static MappingResolver getMappingResolver(Object servletOrFilter) { + MappingResolver.Factory factory = getMappingResolverFactory(servletOrFilter); + if (factory != null) { + return factory.get(); + } + return null; + } + + private static MappingResolver.Factory getMappingResolverFactory(Object servletOrFilter) { + boolean servlet = servletOrFilter instanceof Servlet; + if (servlet) { + return SERVLET_MAPPING_RESOLVER.get((Servlet) servletOrFilter); + } else { + return FILTER_MAPPING_RESOLVER.get((Filter) servletOrFilter); + } + } + + private Servlet3Singletons() {} +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAccessor.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAccessor.java new file mode 100644 index 000000000000..2efca2850808 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAccessor.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import java.security.Principal; +import java.util.List; + +/** + * This interface is used to access methods of ServletContext, HttpServletRequest and + * HttpServletResponse classes in shared code that is used for both jakarta.servlet and + * javax.servlet versions of those classes. A wrapper class with extra information attached may be + * used as well in cases where the class itself does not provide some field (such as response status + * for Servlet API 2.2). + * + * @param HttpServletRequest class (or a wrapper) + * @param HttpServletResponse class (or a wrapper) + */ +public interface ServletAccessor { + String getRequestContextPath(REQUEST request); + + String getRequestScheme(REQUEST request); + + String getRequestUri(REQUEST request); + + String getRequestQueryString(REQUEST request); + + Object getRequestAttribute(REQUEST request, String name); + + void setRequestAttribute(REQUEST request, String name, Object value); + + String getRequestProtocol(REQUEST request); + + String getRequestMethod(REQUEST request); + + String getRequestRemoteAddr(REQUEST request); + + Integer getRequestRemotePort(REQUEST request); + + String getRequestLocalAddr(REQUEST request); + + Integer getRequestLocalPort(REQUEST request); + + String getRequestHeader(REQUEST request, String name); + + List getRequestHeaderValues(REQUEST request, String name); + + Iterable getRequestHeaderNames(REQUEST request); + + List getRequestParameterValues(REQUEST request, String name); + + String getRequestServletPath(REQUEST request); + + String getRequestPathInfo(REQUEST request); + + Principal getRequestUserPrincipal(REQUEST request); + + void addRequestAsyncListener( + REQUEST request, ServletAsyncListener listener, Object response); + + int getResponseStatus(RESPONSE response); + + List getResponseHeaderValues(RESPONSE response, String name); + + boolean isResponseCommitted(RESPONSE response); + + boolean isServletException(Throwable throwable); +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAdditionalAttributesExtractor.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAdditionalAttributesExtractor.java new file mode 100644 index 000000000000..d595648db552 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAdditionalAttributesExtractor.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import java.security.Principal; +import javax.annotation.Nullable; + +public class ServletAdditionalAttributesExtractor + implements AttributesExtractor< + ServletRequestContext, ServletResponseContext> { + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.servlet.experimental-span-attributes", false); + private static final AttributeKey SERVLET_TIMEOUT = longKey("servlet.timeout"); + + // copied from EnduserIncubatingAttributes + static final AttributeKey ENDUSER_ID = stringKey("enduser.id"); + + private final ServletAccessor accessor; + + public ServletAdditionalAttributesExtractor(ServletAccessor accessor) { + this.accessor = accessor; + } + + @Override + public void onStart( + AttributesBuilder attributes, + Context parentContext, + ServletRequestContext requestContext) {} + + @SuppressWarnings("deprecation") // using deprecated semconv + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + ServletRequestContext requestContext, + @Nullable ServletResponseContext responseContext, + @Nullable Throwable error) { + if (AgentCommonConfig.get().getEnduserConfig().isIdEnabled()) { + Principal principal = accessor.getRequestUserPrincipal(requestContext.request()); + if (principal != null) { + String name = principal.getName(); + if (name != null) { + attributes.put(ENDUSER_ID, name); + } + } + } + if (!CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + return; + } + if (responseContext != null && responseContext.hasTimeout()) { + attributes.put(SERVLET_TIMEOUT, responseContext.getTimeout()); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAsyncContext.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAsyncContext.java new file mode 100644 index 000000000000..6534528b0d7a --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAsyncContext.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import static io.opentelemetry.context.ContextKey.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.ImplicitContextKeyed; +import javax.annotation.Nullable; + +public class ServletAsyncContext implements ImplicitContextKeyed { + private static final ContextKey CONTEXT_KEY = + named("opentelemetry-servlet-async-context"); + + private boolean isAsyncListenerAttached; + private Throwable throwable; + private Object response; + private Context context; + + public static Context init(Context context) { + if (context.get(CONTEXT_KEY) != null) { + return context; + } + return context.with(new ServletAsyncContext()); + } + + @Nullable + public static ServletAsyncContext get(@Nullable Context context) { + return context != null ? context.get(CONTEXT_KEY) : null; + } + + public static boolean isAsyncListenerAttached(@Nullable Context context) { + ServletAsyncContext servletAsyncContext = get(context); + return servletAsyncContext != null && servletAsyncContext.isAsyncListenerAttached; + } + + public static void setAsyncListenerAttached(@Nullable Context context, boolean value) { + ServletAsyncContext servletAsyncContext = get(context); + if (servletAsyncContext != null) { + servletAsyncContext.isAsyncListenerAttached = value; + } + } + + public static Throwable getAsyncException(@Nullable Context context) { + ServletAsyncContext servletAsyncContext = get(context); + return servletAsyncContext != null ? servletAsyncContext.throwable : null; + } + + public static void recordAsyncException(@Nullable Context context, Throwable throwable) { + ServletAsyncContext servletAsyncContext = get(context); + if (servletAsyncContext != null) { + servletAsyncContext.throwable = throwable; + } + } + + public static Object getAsyncListenerResponse(@Nullable Context context) { + ServletAsyncContext servletAsyncContext = get(context); + return servletAsyncContext != null ? servletAsyncContext.response : null; + } + + public static void setAsyncListenerResponse(Context context, Object response) { + ServletAsyncContext servletAsyncContext = get(context); + if (servletAsyncContext != null) { + servletAsyncContext.response = response; + servletAsyncContext.context = context; + } + } + + public static Context getAsyncListenerContext(Context context) { + ServletAsyncContext servletAsyncContext = get(context); + if (servletAsyncContext != null) { + return servletAsyncContext.context; + } + return null; + } + + @Override + public Context storeInContext(Context context) { + return context.with(CONTEXT_KEY, this); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAsyncListener.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAsyncListener.java new file mode 100644 index 000000000000..120c387a080d --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletAsyncListener.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +public interface ServletAsyncListener { + void onComplete(RESPONSE response); + + void onTimeout(long timeout); + + void onError(Throwable throwable, RESPONSE response); +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletContextPath.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletContextPath.java new file mode 100644 index 000000000000..d127824eaa38 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletContextPath.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import java.util.function.Function; + +/** + * The context key here is used to propagate the servlet context path throughout the request, so + * that routing framework instrumentation that updates the span name with a more specific route can + * prepend the servlet context path in front of that route. + * + *

This needs to be in the instrumentation-api module, instead of injected as a helper class into + * the different modules that need it, in order to make sure that there is only a single instance of + * the context key, since otherwise instrumentation across different class loaders would use + * different context keys and not be able to share the servlet context path. + */ +public final class ServletContextPath { + + // Keeps track of the servlet context path that needs to be prepended to the route when updating + // the span name + private static final ContextKey CONTEXT_KEY = + ContextKey.named("opentelemetry-servlet-context-path-key"); + + public static Context init( + Context context, Function contextPathExtractor, REQUEST request) { + ServletContextPath servletContextPath = context.get(CONTEXT_KEY); + if (servletContextPath != null) { + return context; + } + String contextPath = contextPathExtractor.apply(request); + if (contextPath == null) { + // context path isn't know yet + return context; + } + if (contextPath.isEmpty() || contextPath.equals("/")) { + // normalize empty context path to null + contextPath = null; + } + return context.with(CONTEXT_KEY, new ServletContextPath(contextPath)); + } + + private final String contextPath; + + private ServletContextPath(String contextPath) { + this.contextPath = contextPath; + } + + /** + * Returns a concatenation of a servlet context path stored in the given {@code context} and a + * given {@code spanName}. If there is no servlet path stored in the context, returns {@code + * spanName}. + */ + public static String prepend(Context context, String spanName) { + ServletContextPath servletContextPath = context.get(CONTEXT_KEY); + if (servletContextPath != null) { + String value = servletContextPath.contextPath; + if (value != null) { + if (spanName == null || spanName.isEmpty()) { + return value; + } else { + return value + (spanName.startsWith("/") ? spanName : ("/" + spanName)); + } + } + } + + return spanName; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletErrorCauseExtractor.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletErrorCauseExtractor.java new file mode 100644 index 000000000000..9e223d95454b --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletErrorCauseExtractor.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor; + +public class ServletErrorCauseExtractor implements ErrorCauseExtractor { + private final ServletAccessor accessor; + + public ServletErrorCauseExtractor(ServletAccessor accessor) { + this.accessor = accessor; + } + + @Override + public Throwable extract(Throwable error) { + if (accessor.isServletException(error) && error.getCause() != null) { + error = error.getCause(); + } + return ErrorCauseExtractor.getDefault().extract(error); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletFilterMappingResolverFactory.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletFilterMappingResolverFactory.java new file mode 100644 index 000000000000..2c05bfc95db9 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletFilterMappingResolverFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +public abstract class ServletFilterMappingResolverFactory + extends ServletMappingResolverFactory { + + protected abstract FILTERREGISTRATION getFilterRegistration(); + + protected abstract Collection getUrlPatternMappings( + FILTERREGISTRATION filterRegistration); + + protected abstract Collection getServletNameMappings( + FILTERREGISTRATION filterRegistration); + + protected abstract Collection getServletMappings(String servletName); + + @Override + @Nullable + protected Mappings getMappings() { + FILTERREGISTRATION filterRegistration = getFilterRegistration(); + if (filterRegistration == null) { + return null; + } + Set mappings = new HashSet<>(); + Collection urlPatternMappings = getUrlPatternMappings(filterRegistration); + if (urlPatternMappings != null) { + mappings.addAll(urlPatternMappings); + } + Collection servletNameMappings = getServletNameMappings(filterRegistration); + if (servletNameMappings != null) { + for (String servletName : servletNameMappings) { + Collection servletMappings = getServletMappings(servletName); + if (servletMappings != null) { + mappings.addAll(servletMappings); + } + } + } + + if (mappings.isEmpty()) { + return null; + } + + List mappingsList = new ArrayList<>(mappings); + // sort the longest mapping first + mappingsList.sort((s1, s2) -> s2.length() - s1.length()); + + return new Mappings(mappingsList); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletHelper.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletHelper.java new file mode 100644 index 000000000000..9458e8dbc8de --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletHelper.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +public class ServletHelper extends BaseServletHelper { + public static final String CONTEXT_ATTRIBUTE = ServletHelper.class.getName() + ".Context"; + + public ServletHelper( + Instrumenter, ServletResponseContext> instrumenter, + ServletAccessor accessor) { + super(instrumenter, accessor); + } + + public void end( + ServletRequestContext requestContext, + REQUEST request, + RESPONSE response, + Throwable throwable, + boolean topLevel, + Context context, + Scope scope) { + + if (scope != null) { + scope.close(); + } + + if (context == null && topLevel) { + Context currentContext = Context.current(); + // Something else is managing the context, we're in the outermost level of Servlet + // instrumentation and we have an uncaught throwable. Let's add it to the current span. + if (throwable != null) { + recordException(currentContext, throwable); + if (!mustEndOnHandlerMethodExit(currentContext)) { + // We could be inside async dispatch. Unlike tomcat jetty does not call + // ServletAsyncListener.onError when exception is thrown inside async dispatch. + recordAsyncException(currentContext, throwable); + } + } + // also capture request parameters as servlet attributes + captureServletAttributes(currentContext, request); + } + + if (scope == null || context == null) { + return; + } + + ServletResponseContext responseContext = new ServletResponseContext<>(response); + if (throwable != null || mustEndOnHandlerMethodExit(context)) { + instrumenter.end(context, requestContext, responseContext, throwable); + } + } + + /** + * Helper method to determine whether the appserver handler/servlet service/servlet filter method + * that started a span must also end it, even if no error was detected. Extracted as a separate + * method to avoid duplicating the comments on the logic behind this choice. + */ + public boolean mustEndOnHandlerMethodExit(Context context) { + // This request is handled asynchronously and startAsync instrumentation has already attached + // the listener. + return !isAsyncListenerAttached(context); + + // This means that startAsync was not called (assuming startAsync instrumentation works + // correctly on this servlet engine), therefore the request was handled synchronously, and + // handler method end must also end the span. + } + + /** + * Response object must be attached to a request prior to {@link #attachAsyncListener(REQUEST, + * Context)} being called, as otherwise in some environments it is not possible to access response + * from async event in listeners. + */ + public void setAsyncListenerResponse(Context context, RESPONSE response) { + ServletAsyncContext.setAsyncListenerResponse(context, response); + } + + @SuppressWarnings("unchecked") + public RESPONSE getAsyncListenerResponse(Context context) { + return (RESPONSE) ServletAsyncContext.getAsyncListenerResponse(context); + } + + public void attachAsyncListener(REQUEST request, Context context) { + if (isAsyncListenerAttached(context)) { + return; + } + + Object response = getAsyncListenerResponse(context); + + ServletRequestContext requestContext = new ServletRequestContext<>(request, null); + accessor.addRequestAsyncListener( + request, + new AsyncRequestCompletionListener<>(this, instrumenter, requestContext, context), + response); + ServletAsyncContext.setAsyncListenerAttached(context, true); + } + + private static boolean isAsyncListenerAttached(Context context) { + return ServletAsyncContext.isAsyncListenerAttached(context); + } + + public Runnable wrapAsyncRunnable(Runnable runnable) { + return AsyncRunnableWrapper.wrap(this, runnable); + } + + public void recordAsyncException(Context context, Throwable throwable) { + ServletAsyncContext.recordAsyncException(context, throwable); + } + + public Throwable getAsyncException(Context context) { + return ServletAsyncContext.getAsyncException(context); + } + + public Context getAsyncListenerContext(Context context) { + return ServletAsyncContext.getAsyncListenerContext(context); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletHttpAttributesGetter.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletHttpAttributesGetter.java new file mode 100644 index 000000000000..f4b9578b0f00 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletHttpAttributesGetter.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.util.List; +import javax.annotation.Nullable; + +public class ServletHttpAttributesGetter + implements HttpServerAttributesGetter< + ServletRequestContext, ServletResponseContext> { + + protected final ServletAccessor accessor; + + public ServletHttpAttributesGetter(ServletAccessor accessor) { + this.accessor = accessor; + } + + @Override + @Nullable + public String getHttpRequestMethod(ServletRequestContext requestContext) { + return accessor.getRequestMethod(requestContext.request()); + } + + @Override + @Nullable + public String getUrlScheme(ServletRequestContext requestContext) { + return accessor.getRequestScheme(requestContext.request()); + } + + @Nullable + @Override + public String getUrlPath(ServletRequestContext requestContext) { + return accessor.getRequestUri(requestContext.request()); + } + + @Nullable + @Override + public String getUrlQuery(ServletRequestContext requestContext) { + return accessor.getRequestQueryString(requestContext.request()); + } + + @Override + public List getHttpRequestHeader( + ServletRequestContext requestContext, String name) { + return accessor.getRequestHeaderValues(requestContext.request(), name); + } + + @Override + @Nullable + public Integer getHttpResponseStatusCode( + ServletRequestContext requestContext, + ServletResponseContext responseContext, + @Nullable Throwable error) { + RESPONSE response = responseContext.response(); + + // OpenLiberty might call the AsyncListener with an AsyncEvent that does not contain a response + // in some cases where the connection is dropped + if (response == null) { + return null; + } + + if (!accessor.isResponseCommitted(response) && error != null) { + // if response is not committed and there is a throwable set status to 500 / + // INTERNAL_SERVER_ERROR, due to servlet spec + // https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf: + // "If a servlet generates an error that is not handled by the error page mechanism as + // described above, the container must ensure to send a response with status 500." + return 500; + } + return accessor.getResponseStatus(response); + } + + @Override + public List getHttpResponseHeader( + ServletRequestContext requestContext, + ServletResponseContext responseContext, + String name) { + return accessor.getResponseHeaderValues(responseContext.response(), name); + } + + @Nullable + @Override + public String getNetworkProtocolName( + ServletRequestContext requestContext, + @Nullable ServletResponseContext responseContext) { + String protocol = accessor.getRequestProtocol(requestContext.request()); + if (protocol != null && protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + ServletRequestContext requestContext, + @Nullable ServletResponseContext responseContext) { + String protocol = accessor.getRequestProtocol(requestContext.request()); + if (protocol != null && protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Override + @Nullable + public String getNetworkPeerAddress( + ServletRequestContext requestContext, + @Nullable ServletResponseContext response) { + return accessor.getRequestRemoteAddr(requestContext.request()); + } + + @Override + @Nullable + public Integer getNetworkPeerPort( + ServletRequestContext requestContext, + @Nullable ServletResponseContext response) { + return accessor.getRequestRemotePort(requestContext.request()); + } + + @Nullable + @Override + public String getNetworkLocalAddress( + ServletRequestContext requestContext, + @Nullable ServletResponseContext response) { + return accessor.getRequestLocalAddr(requestContext.request()); + } + + @Nullable + @Override + public Integer getNetworkLocalPort( + ServletRequestContext requestContext, + @Nullable ServletResponseContext response) { + return accessor.getRequestLocalPort(requestContext.request()); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletInstrumenterBuilder.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletInstrumenterBuilder.java new file mode 100644 index 000000000000..4f4ed0324f35 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletInstrumenterBuilder.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import java.util.ArrayList; +import java.util.List; + +public final class ServletInstrumenterBuilder { + + private ServletInstrumenterBuilder() {} + + private final List>> contextCustomizers = + new ArrayList<>(); + + private boolean propagateOperationListenersToOnEnd; + + public static ServletInstrumenterBuilder create() { + return new ServletInstrumenterBuilder<>(); + } + + @CanIgnoreReturnValue + public ServletInstrumenterBuilder addContextCustomizer( + ContextCustomizer> contextCustomizer) { + contextCustomizers.add(contextCustomizer); + return this; + } + + @CanIgnoreReturnValue + public ServletInstrumenterBuilder propagateOperationListenersToOnEnd() { + propagateOperationListenersToOnEnd = true; + return this; + } + + public Instrumenter, ServletResponseContext> build( + String instrumentationName, + ServletAccessor accessor, + SpanNameExtractor> spanNameExtractor, + HttpServerAttributesGetter, ServletResponseContext> + httpAttributesGetter) { + + DefaultHttpServerInstrumenterBuilder< + ServletRequestContext, ServletResponseContext> + serverBuilder = + DefaultHttpServerInstrumenterBuilder.create( + instrumentationName, + GlobalOpenTelemetry.get(), + httpAttributesGetter, + new ServletRequestGetter<>(accessor)); + serverBuilder.setSpanNameExtractor(e -> spanNameExtractor); + + return JavaagentHttpServerInstrumenters.create( + serverBuilder, + builder -> { + if (ServletRequestParametersExtractor.enabled()) { + AttributesExtractor, ServletResponseContext> + requestParametersExtractor = new ServletRequestParametersExtractor<>(accessor); + builder.addAttributesExtractor(requestParametersExtractor); + } + for (ContextCustomizer> contextCustomizer : + contextCustomizers) { + builder.addContextCustomizer(contextCustomizer); + } + + if (propagateOperationListenersToOnEnd) { + InstrumenterUtil.propagateOperationListenersToOnEnd(builder); + } + + builder + .addAttributesExtractor(new ServletAdditionalAttributesExtractor<>(accessor)) + .setErrorCauseExtractor(new ServletErrorCauseExtractor<>(accessor)); + }); + } + + public Instrumenter, ServletResponseContext> build( + String instrumentationName, ServletAccessor accessor) { + HttpServerAttributesGetter, ServletResponseContext> + httpAttributesGetter = new ServletHttpAttributesGetter<>(accessor); + SpanNameExtractor> spanNameExtractor = + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build(); + + return build(instrumentationName, accessor, spanNameExtractor, httpAttributesGetter); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletMappingResolverFactory.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletMappingResolverFactory.java new file mode 100644 index 000000000000..70062307bea9 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletMappingResolverFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import java.util.Collection; +import javax.annotation.Nullable; + +public abstract class ServletMappingResolverFactory implements MappingResolver.Factory { + + private volatile MappingResolverHolder holder; + + @Nullable + protected abstract Mappings getMappings(); + + private MappingResolver build() { + Mappings mappings = getMappings(); + if (mappings == null) { + return null; + } + + return MappingResolver.build(mappings.getMappings()); + } + + @Override + @Nullable + public final MappingResolver get() { + // build MappingResolver if it is not already built, no need to synchronize as it can safely be + // built more than once + if (holder == null) { + holder = new MappingResolverHolder(build()); + } + + return holder.mappingResolver; + } + + // using a holder class to distinguish build() returning null from build() not called + private static class MappingResolverHolder { + final MappingResolver mappingResolver; + + MappingResolverHolder(MappingResolver mappingResolver) { + this.mappingResolver = mappingResolver; + } + } + + public static class Mappings { + private final Collection mappings; + + public Mappings(Collection mappings) { + this.mappings = mappings; + } + + public Collection getMappings() { + return mappings; + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestContext.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestContext.java new file mode 100644 index 000000000000..59f5141f83fd --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestContext.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import javax.annotation.Nullable; + +public class ServletRequestContext { + private final T request; + private final Object servletOrFilter; + + public ServletRequestContext(T request) { + this(request, null); + } + + public ServletRequestContext(T request, Object servletOrFilter) { + this.request = request; + this.servletOrFilter = servletOrFilter; + } + + public T request() { + return request; + } + + @Nullable + public Object servletOrFilter() { + return servletOrFilter; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestGetter.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestGetter.java new file mode 100644 index 000000000000..039f9599f0bf --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestGetter.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Iterator; + +public class ServletRequestGetter + implements TextMapGetter> { + protected final ServletAccessor accessor; + + public ServletRequestGetter(ServletAccessor accessor) { + this.accessor = accessor; + } + + @Override + public Iterable keys(ServletRequestContext carrier) { + return accessor.getRequestHeaderNames(carrier.request()); + } + + @Override + public String get(ServletRequestContext carrier, String key) { + return accessor.getRequestHeader(carrier.request(), key); + } + + @Override + public Iterator getAll(ServletRequestContext carrier, String key) { + return accessor.getRequestHeaderValues(carrier.request(), key).iterator(); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestParametersExtractor.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestParametersExtractor.java new file mode 100644 index 000000000000..6ea2935a7d56 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletRequestParametersExtractor.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import static java.util.Collections.emptyList; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; + +public class ServletRequestParametersExtractor + implements AttributesExtractor< + ServletRequestContext, ServletResponseContext> { + private static final List CAPTURE_REQUEST_PARAMETERS = + AgentInstrumentationConfig.get() + .getList( + "otel.instrumentation.servlet.experimental.capture-request-parameters", emptyList()); + + private static final ConcurrentMap>> parameterKeysCache = + new ConcurrentHashMap<>(); + + private final ServletAccessor accessor; + + public ServletRequestParametersExtractor(ServletAccessor accessor) { + this.accessor = accessor; + } + + public static boolean enabled() { + return !CAPTURE_REQUEST_PARAMETERS.isEmpty(); + } + + public void setAttributes( + REQUEST request, BiConsumer>, List> consumer) { + for (String name : CAPTURE_REQUEST_PARAMETERS) { + List values = accessor.getRequestParameterValues(request, name); + if (!values.isEmpty()) { + consumer.accept(parameterAttributeKey(name), values); + } + } + } + + @Override + public void onStart( + AttributesBuilder attributes, + Context parentContext, + ServletRequestContext requestContext) {} + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + ServletRequestContext requestContext, + @Nullable ServletResponseContext responseContext, + @Nullable Throwable error) { + // request parameters are extracted at the end of the request to make sure that we don't access + // them before request encoding has been set + REQUEST request = requestContext.request(); + setAttributes(request, attributes::put); + } + + private static AttributeKey> parameterAttributeKey(String headerName) { + return parameterKeysCache.computeIfAbsent( + headerName, ServletRequestParametersExtractor::createKey); + } + + private static AttributeKey> createKey(String parameterName) { + // normalize parameter name similarly as is done with header names when header values are + // captured as span attributes + parameterName = parameterName.toLowerCase(Locale.ROOT); + String key = "servlet.request.parameter." + parameterName; + return AttributeKey.stringArrayKey(key); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletResponseContext.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletResponseContext.java new file mode 100644 index 000000000000..92c49182d881 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletResponseContext.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +public class ServletResponseContext { + private final T response; + // used for servlet 2.2 where request status can't be extracted from HttpServletResponse + private Integer status; + private Long timeout; + + public ServletResponseContext(T response) { + this.response = response; + } + + public T response() { + return response; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getStatus() { + return status; + } + + public boolean hasStatus() { + return status != null; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public long getTimeout() { + return timeout; + } + + public boolean hasTimeout() { + return timeout != null; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletSpanNameProvider.java b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletSpanNameProvider.java new file mode 100644 index 000000000000..eee8ec2c667a --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/copied/ServletSpanNameProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.copied; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBiGetter; +import javax.annotation.Nullable; + +/** Helper class for constructing span name for given servlet/filter mapping and request. */ +public class ServletSpanNameProvider + implements HttpServerRouteBiGetter { + private final ServletAccessor servletAccessor; + + public ServletSpanNameProvider(ServletAccessor servletAccessor) { + this.servletAccessor = servletAccessor; + } + + @Override + @Nullable + public String get(Context context, MappingResolver mappingResolver, REQUEST request) { + String servletPath = servletAccessor.getRequestServletPath(request); + String pathInfo = servletAccessor.getRequestPathInfo(request); + String mapping = mappingResolver.resolve(servletPath, pathInfo); + // mapping was not found + if (mapping == null) { + return null; + } + + return ServletContextPath.prepend(context, mapping); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/AbstractServlet3Test.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/AbstractServlet3Test.java new file mode 100644 index 000000000000..390ec467953e --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/AbstractServlet3Test.java @@ -0,0 +1,144 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.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.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.servlet.v3_0.copied.HttpServerResponseCustomizerHolder; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import javax.servlet.Servlet; + +public abstract class AbstractServlet3Test extends AbstractHttpServerTest { + + public static final ServerEndpoint HTML_PRINT_WRITER = + new ServerEndpoint( + "HTML_PRINT_WRITER", + "htmlPrintWriter", + 200, + "\n" + + "\n" + + "\n" + + " \n" + + " Title\n" + + "\n" + + "\n" + + "

test works

\n" + + "\n" + + ""); + public static final ServerEndpoint HTML_SERVLET_OUTPUT_STREAM = + new ServerEndpoint( + "HTML_SERVLET_OUTPUT_STREAM", + "htmlServletOutputStream", + 200, + "\n" + + "\n" + + "\n" + + " \n" + + " Title\n" + + "\n" + + "\n" + + "

test works

\n" + + "\n" + + ""); + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setTestCaptureRequestParameters(false); // Requires AgentConfig. + options.setTestCaptureHttpHeaders(false); // Requires AgentConfig. + options.disableTestNonStandardHttpMethod(); // test doesn't use route mapping correctly. + options.setTestException(false); // filters don't have visibility into exception handling above. + HttpServerResponseCustomizerHolder.setCustomizer(new TestAgentHttpResponseCustomizer()); + options.setHasResponseCustomizer(e -> true); + options.setHasResponseSpan(this::hasResponseSpan); + } + + protected boolean hasResponseSpan(ServerEndpoint endpoint) { + return endpoint.equals(REDIRECT) || (endpoint.equals(ERROR) && errorEndpointUsesSendError()); + } + + public abstract Class servlet(); + + public abstract void addServlet(CONTEXT context, String path, Class servlet) + throws Exception; + + protected void setupServlets(CONTEXT context) throws Exception { + Class servlet = servlet(); + + addServlet(context, SUCCESS.getPath(), servlet); + addServlet(context, QUERY_PARAM.getPath(), servlet); + addServlet(context, ERROR.getPath(), servlet); + addServlet(context, EXCEPTION.getPath(), servlet); + addServlet(context, REDIRECT.getPath(), servlet); + addServlet(context, AUTH_REQUIRED.getPath(), servlet); + addServlet(context, INDEXED_CHILD.getPath(), servlet); + addServlet(context, CAPTURE_HEADERS.getPath(), servlet); + addServlet(context, CAPTURE_PARAMETERS.getPath(), servlet); + addServlet(context, HTML_PRINT_WRITER.getPath(), servlet); + addServlet(context, HTML_SERVLET_OUTPUT_STREAM.getPath(), servlet); + } + + @Override + public String expectedHttpRoute(ServerEndpoint endpoint, String method) { + // no need to compute route if we're not expecting it + if (!hasHttpRouteAttribute(endpoint)) { + return null; + } + + if (method.equals(HttpConstants._OTHER)) { + return getContextPath() + endpoint.getPath(); + } + + // NOTE: Primary difference from javaagent servlet instrumentation! + // Since just we're working with a filter, we can't actually get the proper servlet path. + return getContextPath() + "/*"; + } + + public boolean errorEndpointUsesSendError() { + return true; + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, SpanData parentSpan, String method, ServerEndpoint endpoint) { + switch (endpoint.name()) { + case "REDIRECT": + SpanDataAssert spanDataAssert = + span.satisfies(s -> assertThat(s.getName()).matches(".*\\.sendRedirect")) + .hasKind(SpanKind.INTERNAL); + if (assertParentOnRedirect()) { + return spanDataAssert.hasParent(parentSpan); + } + return spanDataAssert; + case "ERROR": + return span.satisfies(s -> assertThat(s.getName()).matches(".*\\.sendError")) + .hasKind(SpanKind.INTERNAL) + .hasParent(parentSpan); + default: + break; + } + return span; + } + + protected boolean assertParentOnRedirect() { + return true; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/RequestDispatcherServlet.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/RequestDispatcherServlet.java new file mode 100644 index 000000000000..6d93f53d0f8e --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/RequestDispatcherServlet.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import java.io.IOException; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class RequestDispatcherServlet { + + @WebServlet(asyncSupported = true) + public static class Forward extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String target = req.getServletPath().replace("/dispatch", ""); + ServletContext context = getServletContext(); + RequestDispatcher dispatcher = context.getRequestDispatcher(target); + dispatcher.forward(req, resp); + } + } + + @WebServlet(asyncSupported = true) + public static class Include extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String target = req.getServletPath().replace("/dispatch", ""); + ServletContext context = getServletContext(); + RequestDispatcher dispatcher = context.getRequestDispatcher(target); + // for HTML test case, set the content type before calling include because + // setContentType will be rejected if called inside include + // check + // https://docs.oracle.com/javaee/7/api/javax/servlet/RequestDispatcher.html#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse- + if ("/htmlPrintWriter".equals(target) || "/htmlServletOutputStream".equals(target)) { + resp.setContentType("text/html"); + } + dispatcher.include(req, resp); + } + } + + private RequestDispatcherServlet() {} +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3MappingResolverFactory.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3MappingResolverFactory.java new file mode 100644 index 000000000000..573670876207 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3MappingResolverFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import io.opentelemetry.instrumentation.servlet.v3_0.copied.ServletMappingResolverFactory; +import javax.annotation.Nullable; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; + +public class Servlet3MappingResolverFactory extends ServletMappingResolverFactory { + private final ServletConfig servletConfig; + + public Servlet3MappingResolverFactory(ServletConfig servletConfig) { + this.servletConfig = servletConfig; + } + + @Override + @Nullable + public Mappings getMappings() { + String servletName = servletConfig.getServletName(); + ServletContext servletContext = servletConfig.getServletContext(); + if (servletName == null || servletContext == null) { + return null; + } + + ServletRegistration servletRegistration = servletContext.getServletRegistration(servletName); + if (servletRegistration == null) { + return null; + } + return new Mappings(servletRegistration.getMappings()); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/TestAgentHttpResponseCustomizer.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/TestAgentHttpResponseCustomizer.java new file mode 100644 index 000000000000..8d7240e06237 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/TestAgentHttpResponseCustomizer.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.servlet.v3_0.copied.HttpServerResponseCustomizer; +import io.opentelemetry.instrumentation.servlet.v3_0.copied.HttpServerResponseMutator; + +public class TestAgentHttpResponseCustomizer implements HttpServerResponseCustomizer { + + @Override + public void customize( + Context serverContext, T response, HttpServerResponseMutator responseMutator) { + + SpanContext spanContext = Span.fromContext(serverContext).getSpanContext(); + String traceId = spanContext.getTraceId(); + String spanId = spanContext.getSpanId(); + + responseMutator.appendHeader(response, "X-Test-TraceId", traceId); + responseMutator.appendHeader(response, "X-Test-SpanId", spanId); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/TestServlet3.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/TestServlet3.java new file mode 100644 index 000000000000..eac7d502ed5a --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/TestServlet3.java @@ -0,0 +1,352 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.servlet.v3_0.AbstractServlet3Test.HTML_PRINT_WRITER; +import static io.opentelemetry.instrumentation.servlet.v3_0.AbstractServlet3Test.HTML_SERVLET_OUTPUT_STREAM; +import static io.opentelemetry.instrumentation.servlet.v3_0.copied.Servlet3Singletons.SERVLET_MAPPING_RESOLVER; +import static io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest.controller; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.testing.GlobalTraceUtil; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import javax.servlet.AsyncContext; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class TestServlet3 { + + private TestServlet3() {} + + @WebServlet + public static class Sync extends HttpServlet { + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + SERVLET_MAPPING_RESOLVER.set(this, new Servlet3MappingResolverFactory(config)); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String servletPath = (String) req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + if (servletPath == null) { + servletPath = req.getServletPath(); + } + + ServerEndpoint endpoint = ServerEndpoint.forPath(servletPath); + controller( + endpoint, + () -> { + resp.setContentType("text/plain"); + if (SUCCESS.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (INDEXED_CHILD.equals(endpoint)) { + endpoint.collectSpanAttributes(req::getParameter); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (QUERY_PARAM.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(req.getQueryString()); + } else if (REDIRECT.equals(endpoint)) { + resp.sendRedirect(endpoint.getBody()); + } else if (CAPTURE_HEADERS.equals(endpoint)) { + resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (CAPTURE_PARAMETERS.equals(endpoint)) { + req.setCharacterEncoding("UTF8"); + String value = req.getParameter("test-parameter"); + if (!value.equals("test value õäöü")) { + throw new IllegalStateException( + "request parameter does not have expected value " + value); + } + + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (ERROR.equals(endpoint)) { + resp.sendError(endpoint.getStatus(), endpoint.getBody()); + } else if (EXCEPTION.equals(endpoint)) { + throw new IllegalStateException(endpoint.getBody()); + } else if (HTML_PRINT_WRITER.equals(endpoint)) { + resp.setContentType("text/html"); + resp.setStatus(endpoint.getStatus()); + resp.setContentLength(endpoint.getBody().length()); + resp.getWriter().print(endpoint.getBody()); + } else if (HTML_SERVLET_OUTPUT_STREAM.equals(endpoint)) { + resp.setContentType("text/html"); + resp.setStatus(endpoint.getStatus()); + resp.setContentLength(endpoint.getBody().length()); + byte[] body = endpoint.getBody().getBytes(StandardCharsets.UTF_8); + resp.getOutputStream().write(body, 0, body.length); + } + return null; + }); + } + } + + @WebServlet(asyncSupported = true) + public static class Async extends HttpServlet { + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + SERVLET_MAPPING_RESOLVER.set(this, new Servlet3MappingResolverFactory(config)); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + ServerEndpoint endpoint = ServerEndpoint.forPath(req.getServletPath()); + CountDownLatch latch = new CountDownLatch(1); + boolean startAsyncInSpan = + SUCCESS.equals(endpoint) && "true".equals(req.getParameter("startAsyncInSpan")); + AsyncContext context = + startAsyncInSpan + ? GlobalTraceUtil.runWithSpan("startAsync", () -> req.startAsync()) + : req.startAsync(); + if (endpoint.equals(EXCEPTION)) { + context.setTimeout(5000); + } + + context.start( + () -> { + try { + controller( + endpoint, + () -> { + resp.setContentType("text/plain"); + if (SUCCESS.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + context.complete(); + } else if (INDEXED_CHILD.equals(endpoint)) { + endpoint.collectSpanAttributes(req::getParameter); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + context.complete(); + } else if (QUERY_PARAM.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(req.getQueryString()); + context.complete(); + } else if (REDIRECT.equals(endpoint)) { + resp.sendRedirect(endpoint.getBody()); + context.complete(); + } else if (CAPTURE_HEADERS.equals(endpoint)) { + resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + context.complete(); + } else if (CAPTURE_PARAMETERS.equals(endpoint)) { + req.setCharacterEncoding("UTF8"); + String value = req.getParameter("test-parameter"); + if (!value.equals("test value õäöü")) { + throw new IllegalStateException( + "request parameter does not have expected value " + value); + } + + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + context.complete(); + } else if (ERROR.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + context.complete(); + } else if (EXCEPTION.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + PrintWriter writer = resp.getWriter(); + writer.print(endpoint.getBody()); + if (req.getClass().getName().contains("catalina")) { + // on tomcat close the writer to ensure response is sent immediately, + // otherwise there is a chance that tomcat resets the connection before the + // response is sent + writer.close(); + } + throw new IllegalStateException(endpoint.getBody()); + } else if (HTML_PRINT_WRITER.equals(endpoint)) { + resp.setContentType("text/html"); + resp.setStatus(endpoint.getStatus()); + resp.setContentLength(endpoint.getBody().length()); + resp.getWriter().print(endpoint.getBody()); + context.complete(); + } else if (HTML_SERVLET_OUTPUT_STREAM.equals(endpoint)) { + resp.setContentType("text/html"); + resp.setStatus(endpoint.getStatus()); + resp.getOutputStream().print(endpoint.getBody()); + context.complete(); + } + return null; + }); + } catch (Exception exception) { + if (exception instanceof RuntimeException) { + throw (RuntimeException) exception; + } + throw new IllegalStateException(exception); + } finally { + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + } + } + } + + @WebServlet(asyncSupported = true) + public static class FakeAsync extends HttpServlet { + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + SERVLET_MAPPING_RESOLVER.set(this, new Servlet3MappingResolverFactory(config)); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + AsyncContext context = req.startAsync(); + try { + ServerEndpoint endpoint = ServerEndpoint.forPath(req.getServletPath()); + + controller( + endpoint, + () -> { + resp.setContentType("text/plain"); + if (SUCCESS.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (INDEXED_CHILD.equals(endpoint)) { + endpoint.collectSpanAttributes(req::getParameter); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (QUERY_PARAM.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(req.getQueryString()); + } else if (REDIRECT.equals(endpoint)) { + resp.sendRedirect(endpoint.getBody()); + } else if (CAPTURE_HEADERS.equals(endpoint)) { + resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (CAPTURE_PARAMETERS.equals(endpoint)) { + req.setCharacterEncoding("UTF8"); + String value = req.getParameter("test-parameter"); + if (!value.equals("test value õäöü")) { + throw new IllegalStateException( + "request parameter does not have expected value " + value); + } + + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (ERROR.equals(endpoint)) { + resp.sendError(endpoint.getStatus(), endpoint.getBody()); + } else if (EXCEPTION.equals(endpoint)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + throw new IllegalStateException(endpoint.getBody()); + } else if (HTML_PRINT_WRITER.equals(endpoint)) { + // intentionally testing setting status before contentType here to cover that case + // somewhere + resp.setStatus(endpoint.getStatus()); + resp.setContentType("text/html"); + resp.getWriter().print(endpoint.getBody()); + } else if (HTML_SERVLET_OUTPUT_STREAM.equals(endpoint)) { + resp.setContentType("text/html"); + resp.setStatus(endpoint.getStatus()); + resp.getOutputStream().print(endpoint.getBody()); + } + return null; + }); + } finally { + context.complete(); + } + } + } + + @WebServlet(asyncSupported = true) + public static class DispatchImmediate extends HttpServlet { + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + SERVLET_MAPPING_RESOLVER.set(this, new Servlet3MappingResolverFactory(config)); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + String target = req.getServletPath().replace("/dispatch", ""); + if (req.getQueryString() != null) { + target += "?" + req.getQueryString(); + } + + req.startAsync().dispatch(target); + } + } + + @WebServlet(asyncSupported = true) + public static class DispatchAsync extends HttpServlet { + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + SERVLET_MAPPING_RESOLVER.set(this, new Servlet3MappingResolverFactory(config)); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + AsyncContext context = req.startAsync(); + context.start( + () -> { + String target = req.getServletPath().replace("/dispatch", ""); + if (req.getQueryString() != null) { + target += "?" + req.getQueryString(); + } + context.dispatch(target); + }); + } + } + + @WebServlet(asyncSupported = true) + public static class DispatchRecursive extends HttpServlet { + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + SERVLET_MAPPING_RESOLVER.set(this, new Servlet3MappingResolverFactory(config)); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + if (req.getServletPath().equals("/recursive")) { + resp.getWriter().print("Hello Recursive"); + } + + int depth = Integer.parseInt(req.getParameter("depth")); + if (depth > 0) { + req.startAsync().dispatch("/dispatch/recursive?depth=" + (depth - 1)); + } else { + req.startAsync().dispatch("/recursive"); + } + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3AsyncTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3AsyncTest.java new file mode 100644 index 000000000000..ebcc14d6b137 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3AsyncTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpMethod; +import javax.servlet.Servlet; +import org.junit.jupiter.api.Test; + +class JettyServlet3AsyncTest extends JettyServlet3Test { + + @Override + public Class servlet() { + return TestServlet3.Async.class; + } + + @Override + public boolean errorEndpointUsesSendError() { + return false; + } + + @Override + public boolean isAsyncTest() { + return true; + } + + @Test + void startAsyncInSpan() { + AggregatedHttpRequest request = + AggregatedHttpRequest.of( + HttpMethod.GET, resolveAddress(SUCCESS, "h1c://") + "?startAsyncInSpan=true"); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody()); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/*") + .hasKind(SpanKind.SERVER) + .hasNoParent(), + span -> + span.hasName("startAsync") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("controller") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3FakeAsyncTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3FakeAsyncTest.java new file mode 100644 index 000000000000..e448c644d738 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3FakeAsyncTest.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty; + +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import javax.servlet.Servlet; + +class JettyServlet3FakeAsyncTest extends JettyServlet3Test { + @Override + public Class servlet() { + return TestServlet3.FakeAsync.class; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3SyncTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3SyncTest.java new file mode 100644 index 000000000000..ab4fc7e85f88 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3SyncTest.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty; + +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import javax.servlet.Servlet; + +class JettyServlet3SyncTest extends JettyServlet3Test { + @Override + public Class servlet() { + return TestServlet3.Sync.class; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3Test.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3Test.java new file mode 100644 index 000000000000..bd0f1c70e87c --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/JettyServlet3Test.java @@ -0,0 +1,119 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.servlet.v3_0.AbstractServlet3Test; +import io.opentelemetry.instrumentation.servlet.v3_0.OpenTelemetryServletFilter; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +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.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.io.IOException; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.util.EnumSet; +import javax.servlet.DispatcherType; +import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class JettyServlet3Test + extends AbstractServlet3Test { + + @RegisterExtension + protected static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forLibrary(); + + static final boolean IS_BEFORE_94 = isBefore94(); + + public static boolean isBefore94() { + String[] version = Server.getVersion().split("\\."); + int major = Integer.parseInt(version[0]); + int minor = Integer.parseInt(version[1]); + return major < 9 || (major == 9 && minor < 4); + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setTestNotFound(false); + options.setContextPath("/jetty-context"); + options.setVerifyServerSpanEndTime(!isAsyncTest()); + } + + @Override + protected boolean hasResponseSpan(ServerEndpoint endpoint) { + return (IS_BEFORE_94 && endpoint == EXCEPTION && !isAsyncTest()) + || super.hasResponseSpan(endpoint); + } + + public boolean isAsyncTest() { + return false; + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, + SpanData controllerSpan, + SpanData handlerSpan, + String method, + ServerEndpoint endpoint) { + if (IS_BEFORE_94 && endpoint.equals(EXCEPTION)) { + span.satisfies(it -> assertThat(it.getName()).matches(".*\\.sendError")) + .hasKind(SpanKind.INTERNAL) + .hasParent(handlerSpan); + } + + return super.assertResponseSpan(span, controllerSpan, handlerSpan, method, endpoint); + } + + @Override + protected Server setupServer() throws Exception { + Server jettyServer = new Server(new InetSocketAddress("localhost", port)); + + ServletContextHandler servletContext = new ServletContextHandler(null, getContextPath()); + servletContext.setErrorHandler( + new ErrorHandler() { + @Override + protected void handleErrorPage( + HttpServletRequest request, Writer writer, int code, String message) + throws IOException { + Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception"); + writer.write(th != null ? th.getMessage() : message); + } + }); + servletContext.addFilter( + OpenTelemetryServletFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); + setupServlets(servletContext); + jettyServer.setHandler(servletContext); + + jettyServer.start(); + + return jettyServer; + } + + @Override + public void stopServer(Server server) throws Exception { + server.stop(); + server.destroy(); + } + + @Override + public void addServlet( + ServletContextHandler servletContext, String path, Class servlet) + throws Exception { + servletContext.addServlet(servlet, path); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyDispatchTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyDispatchTest.java new file mode 100644 index 000000000000..97aa7260e907 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyDispatchTest.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty.dispatch; + +import io.opentelemetry.instrumentation.servlet.v3_0.jetty.JettyServlet3Test; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; + +abstract class JettyDispatchTest extends JettyServlet3Test { + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setContextPath(getContextPath() + "/dispatch"); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3DispatchAsyncTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3DispatchAsyncTest.java new file mode 100644 index 000000000000..c6e0d86b9e11 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3DispatchAsyncTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty.dispatch; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import javax.servlet.Servlet; +import org.eclipse.jetty.servlet.ServletContextHandler; + +class JettyServlet3DispatchAsyncTest extends JettyDispatchTest { + @Override + public Class servlet() { + return TestServlet3.Async.class; + } + + @Override + public boolean isAsyncTest() { + return true; + } + + @Override + protected void setupServlets(ServletContextHandler context) throws Exception { + super.setupServlets(context); + addServlet( + context, "/dispatch" + HTML_PRINT_WRITER.getPath(), TestServlet3.DispatchAsync.class); + addServlet( + context, + "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.getPath(), + TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + SUCCESS.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + QUERY_PARAM.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + ERROR.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + EXCEPTION.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + REDIRECT.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + AUTH_REQUIRED.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + CAPTURE_HEADERS.getPath(), TestServlet3.DispatchAsync.class); + addServlet( + context, "/dispatch" + CAPTURE_PARAMETERS.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + INDEXED_CHILD.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive.class); + } + + @Override + public boolean errorEndpointUsesSendError() { + return false; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3DispatchImmediateTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3DispatchImmediateTest.java new file mode 100644 index 000000000000..b547f1dc82f2 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3DispatchImmediateTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty.dispatch; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import javax.servlet.Servlet; +import org.eclipse.jetty.servlet.ServletContextHandler; + +class JettyServlet3DispatchImmediateTest extends JettyDispatchTest { + @Override + public Class servlet() { + return TestServlet3.Async.class; + } + + @Override + public boolean isAsyncTest() { + return true; + } + + @Override + public boolean errorEndpointUsesSendError() { + return false; + } + + @Override + protected void setupServlets(ServletContextHandler context) throws Exception { + super.setupServlets(context); + addServlet( + context, "/dispatch" + HTML_PRINT_WRITER.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, + "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.getPath(), + TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + SUCCESS.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + QUERY_PARAM.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + ERROR.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + EXCEPTION.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + REDIRECT.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + AUTH_REQUIRED.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + CAPTURE_HEADERS.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + CAPTURE_PARAMETERS.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + INDEXED_CHILD.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive.class); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3ForwardTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3ForwardTest.java new file mode 100644 index 000000000000..901deb0b4a8e --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3ForwardTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty.dispatch; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.servlet.v3_0.RequestDispatcherServlet; +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import javax.servlet.Servlet; +import org.eclipse.jetty.servlet.ServletContextHandler; + +class JettyServlet3ForwardTest extends JettyDispatchTest { + @Override + public Class servlet() { + return TestServlet3.Sync.class; // dispatch to sync servlet + } + + @Override + protected void setupServlets(ServletContextHandler context) throws Exception { + super.setupServlets(context); + + addServlet(context, "/dispatch" + SUCCESS.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + HTML_PRINT_WRITER.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, + "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.getPath(), + RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + QUERY_PARAM.getPath(), RequestDispatcherServlet.Forward.class); + addServlet(context, "/dispatch" + REDIRECT.getPath(), RequestDispatcherServlet.Forward.class); + addServlet(context, "/dispatch" + ERROR.getPath(), RequestDispatcherServlet.Forward.class); + addServlet(context, "/dispatch" + EXCEPTION.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + AUTH_REQUIRED.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + CAPTURE_HEADERS.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, + "/dispatch" + CAPTURE_PARAMETERS.getPath(), + RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + INDEXED_CHILD.getPath(), RequestDispatcherServlet.Forward.class); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3IncludeTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3IncludeTest.java new file mode 100644 index 000000000000..365563c0c8e8 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/jetty/dispatch/JettyServlet3IncludeTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.jetty.dispatch; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.servlet.v3_0.RequestDispatcherServlet; +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import javax.servlet.Servlet; +import org.eclipse.jetty.servlet.ServletContextHandler; + +class JettyServlet3IncludeTest extends JettyDispatchTest { + @Override + public Class servlet() { + return TestServlet3.Sync.class; // dispatch to sync servlet + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setTestRedirect(false); + options.setTestCaptureHttpHeaders(false); + options.setTestError(false); + } + + @Override + protected void setupServlets(ServletContextHandler context) throws Exception { + super.setupServlets(context); + + addServlet(context, "/dispatch" + SUCCESS.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, "/dispatch" + HTML_PRINT_WRITER.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, + "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.getPath(), + RequestDispatcherServlet.Include.class); + addServlet( + context, "/dispatch" + QUERY_PARAM.getPath(), RequestDispatcherServlet.Include.class); + addServlet(context, "/dispatch" + REDIRECT.getPath(), RequestDispatcherServlet.Include.class); + addServlet(context, "/dispatch" + ERROR.getPath(), RequestDispatcherServlet.Include.class); + addServlet(context, "/dispatch" + EXCEPTION.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, "/dispatch" + AUTH_REQUIRED.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, + "/dispatch" + CAPTURE_PARAMETERS.getPath(), + RequestDispatcherServlet.Include.class); + addServlet( + context, "/dispatch" + INDEXED_CHILD.getPath(), RequestDispatcherServlet.Include.class); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/ErrorHandlerValve.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/ErrorHandlerValve.java new file mode 100644 index 000000000000..d26efccaf8aa --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/ErrorHandlerValve.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat; + +import java.io.IOException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ErrorReportValve; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// public, because it's loaded by reflection +public class ErrorHandlerValve extends ErrorReportValve { + + private static final Logger logger = LoggerFactory.getLogger(ErrorHandlerValve.class); + + @Override + protected void report(Request request, Response response, Throwable t) { + if (response.getStatus() < 400 || response.getContentWritten() > 0 || !response.isError()) { + return; + } + + try { + response.getWriter().print(t != null ? t.getCause().getMessage() : response.getMessage()); + } catch (IOException e) { + logger.error("Failed to write error response", e); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TestAccessLogValve.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TestAccessLogValve.java new file mode 100644 index 000000000000..d6292eb6a846 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TestAccessLogValve.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat; + +import java.io.IOException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import org.apache.catalina.AccessLog; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; + +// public, because it's loaded by reflection +public class TestAccessLogValve extends ValveBase implements AccessLog { + + public final List> getLoggedIds() { + return loggedIds; + } + + private final List> loggedIds = new ArrayList<>(); + + public TestAccessLogValve() { + super(true); + } + + @Override + public void log(Request request, Response response, long time) { + if (request.getParameter("access-log") == null) { + return; + } + + synchronized (loggedIds) { + loggedIds.add( + new AbstractMap.SimpleEntry<>( + request.getAttribute("trace_id").toString(), + request.getAttribute("span_id").toString())); + loggedIds.notifyAll(); + } + } + + public void waitForLoggedIds(int expected) { + long timeout = TimeUnit.SECONDS.toMillis(20); + long startTime = System.currentTimeMillis(); + long endTime = startTime + timeout; + long toWait = timeout; + synchronized (loggedIds) { + while (loggedIds.size() < expected && toWait > 0) { + try { + loggedIds.wait(toWait); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + toWait = endTime - System.currentTimeMillis(); + } + + if (toWait <= 0) { + throw new RuntimeException( + "Timeout waiting for " + expected + " access log ids, got " + loggedIds.size()); + } + } + } + + @Override + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) {} + + @Override + public boolean getRequestAttributesEnabled() { + return false; + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + getNext().invoke(request, response); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3AsyncTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3AsyncTest.java new file mode 100644 index 000000000000..b5ab735e8715 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3AsyncTest.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpMethod; +import javax.servlet.Servlet; +import org.junit.jupiter.api.Test; + +class TomcatServlet3AsyncTest extends TomcatServlet3Test { + + @Override + public Class servlet() { + return TestServlet3.Async.class; + } + + @Override + public boolean errorEndpointUsesSendError() { + return false; + } + + @Test + void startAsyncInSpan() { + AggregatedHttpRequest request = + AggregatedHttpRequest.of( + HttpMethod.GET, resolveAddress(SUCCESS, "h1c://") + "?startAsyncInSpan=true"); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody()); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/*") + .hasKind(SpanKind.SERVER) + .hasNoParent(), + span -> + span.hasName("startAsync") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("controller") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3FakeAsyncTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3FakeAsyncTest.java new file mode 100644 index 000000000000..7b14184cce75 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3FakeAsyncTest.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat; + +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import javax.servlet.Servlet; + +class TomcatServlet3FakeAsyncTest extends TomcatServlet3Test { + + @Override + public Class servlet() { + return TestServlet3.FakeAsync.class; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3SyncTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3SyncTest.java new file mode 100644 index 000000000000..2c16d5205a87 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3SyncTest.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat; + +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import javax.servlet.Servlet; + +class TomcatServlet3SyncTest extends TomcatServlet3Test { + + @Override + public Class servlet() { + return TestServlet3.Sync.class; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3Test.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3Test.java new file mode 100644 index 000000000000..f63e54b9920e --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/TomcatServlet3Test.java @@ -0,0 +1,246 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.servlet.v3_0.AbstractServlet3Test; +import io.opentelemetry.instrumentation.servlet.v3_0.OpenTelemetryServletFilter; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +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.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.servlet.Servlet; +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public abstract class TomcatServlet3Test extends AbstractServlet3Test { + + @RegisterExtension + protected static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forLibrary(); + + private static final ServerEndpoint ACCESS_LOG_SUCCESS = + new ServerEndpoint( + "ACCESS_LOG_SUCCESS", + "success?access-log=true", + SUCCESS.getStatus(), + SUCCESS.getBody(), + false); + private static final ServerEndpoint ACCESS_LOG_ERROR = + new ServerEndpoint( + "ACCESS_LOG_ERROR", + "error-status?access-log=true", + ERROR.getStatus(), + ERROR.getBody(), + false); + private final TestAccessLogValve accessLogValue = new TestAccessLogValve(); + + @TempDir private static File tempDir; + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setContextPath("/tomcat-context"); + options.setTestError(testError()); + } + + public boolean testError() { + return false; + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, SpanData parentSpan, String method, ServerEndpoint endpoint) { + if (NOT_FOUND.equals(endpoint)) { + span.satisfies(s -> assertThat(s.getName()).matches(".*\\.sendError")) + .hasKind(SpanKind.INTERNAL) + .hasParent(parentSpan); + } + return super.assertResponseSpan(span, parentSpan, method, endpoint); + } + + @Override + protected boolean hasResponseSpan(ServerEndpoint endpoint) { + return endpoint == NOT_FOUND || super.hasResponseSpan(endpoint); + } + + @SuppressWarnings("deprecation") // needed API also on Engine. + @Override + protected Tomcat setupServer() throws Exception { + Tomcat tomcatServer = new Tomcat(); + + File baseDir = tempDir; + tomcatServer.setBaseDir(baseDir.getAbsolutePath()); + + tomcatServer.setPort(port); + tomcatServer.getConnector().setEnableLookups(true); // get localhost instead of 127.0.0.1 + + File applicationDir = new File(baseDir, "/webapps/ROOT"); + applicationDir.mkdirs(); + + Context servletContext = + tomcatServer.addWebapp(getContextPath(), applicationDir.getAbsolutePath()); + // Speed up startup by disabling jar scanning: + servletContext.getJarScanner().setJarScanFilter((jarScanType, jarName) -> false); + + setupServlets(servletContext); + + ((StandardHost) tomcatServer.getHost()) + .setErrorReportValveClass(ErrorHandlerValve.class.getName()); + tomcatServer.getHost().getPipeline().addValve(accessLogValue); + + StandardEngine engine = + (StandardEngine) tomcatServer.getServer().findService("Tomcat").getContainer(); + Container container = engine.findChild(engine.getDefaultHost()); + StandardContext context = (StandardContext) container.findChild(getContextPath()); + + FilterDef filter1definition = new FilterDef(); + filter1definition.setFilterName(OpenTelemetryServletFilter.class.getSimpleName()); + filter1definition.setFilterClass(OpenTelemetryServletFilter.class.getName()); + context.addFilterDef(filter1definition); + + FilterMap filter1mapping = new FilterMap(); + filter1mapping.setFilterName(OpenTelemetryServletFilter.class.getSimpleName()); + filter1mapping.addURLPattern("/*"); + context.addFilterMap(filter1mapping); + + tomcatServer.start(); + + return tomcatServer; + } + + @BeforeEach + void setUp() { + accessLogValue.getLoggedIds().clear(); + testing().clearAllExportedData(); + } + + @Override + public void stopServer(Tomcat server) throws LifecycleException { + // requires --add-opens=java.base/java.util=ALL-UNNAMED on newer JVMs. + server.stop(); + server.destroy(); + } + + @Override + public void addServlet(Context servletContext, String path, Class servlet) + throws Exception { + String name = UUID.randomUUID().toString(); + Tomcat.addServlet(servletContext, name, servlet.getConstructor().newInstance()); + servletContext.addServletMappingDecoded(path, name); + } + + @ParameterizedTest + @CsvSource({"1", "4"}) + void accessLogHasIdsForCountRequests(int count) { + AggregatedHttpRequest request = request(ACCESS_LOG_SUCCESS, "GET"); + + IntStream.range(0, count) + .mapToObj(i -> client.execute(request).aggregate().join()) + .forEach( + response -> { + assertThat(response.status().code()).isEqualTo(ACCESS_LOG_SUCCESS.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(ACCESS_LOG_SUCCESS.getBody()); + }); + + accessLogValue.waitForLoggedIds(count); + assertThat(accessLogValue.getLoggedIds().size()).isEqualTo(count); + List loggedTraces = + accessLogValue.getLoggedIds().stream().map(Map.Entry::getKey).collect(Collectors.toList()); + List loggedSpans = + accessLogValue.getLoggedIds().stream() + .map(Map.Entry::getValue) + .collect(Collectors.toList()); + + testing() + .waitAndAssertTraces( + IntStream.range(0, count) + .mapToObj( + i -> + (Consumer) + trace -> { + trace.hasSpansSatisfyingExactly( + span -> + assertServerSpan( + span, "GET", ACCESS_LOG_SUCCESS, SUCCESS.getStatus()), + span -> assertControllerSpan(span, null)); + SpanData span = trace.getSpan(0); + assertThat(loggedTraces).contains(span.getTraceId()); + assertThat(loggedSpans).contains(span.getSpanId()); + }) + .collect(Collectors.toList())); + } + + @Test + void accessLogHasIdsForErrorRequest() { + Assumptions.assumeTrue(testError()); + + AggregatedHttpRequest request = request(ACCESS_LOG_ERROR, "GET"); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(ACCESS_LOG_ERROR.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(ACCESS_LOG_ERROR.getBody()); + + List> spanDataAsserts = new ArrayList<>(); + spanDataAsserts.add( + (span, trace) -> assertServerSpan(span, "GET", ACCESS_LOG_ERROR, ERROR.getStatus())); + spanDataAsserts.add((span, trace) -> assertControllerSpan(span, null)); + if (errorEndpointUsesSendError()) { + spanDataAsserts.add( + (span, trace) -> + span.satisfies(s -> assertThat(s.getName()).matches(".*\\.sendError")) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1))); + } + + accessLogValue.waitForLoggedIds(1); + testing() + .waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + spanDataAsserts.stream() + .map(e -> (Consumer) span -> e.accept(span, trace)) + .collect(Collectors.toList())); + SpanData span = trace.getSpan(0); + Map.Entry entry = accessLogValue.getLoggedIds().get(0); + assertThat(entry.getKey()).isEqualTo(span.getTraceId()); + assertThat(entry.getValue()).isEqualTo(span.getSpanId()); + }); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatDispatchTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatDispatchTest.java new file mode 100644 index 000000000000..adf599f19a29 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatDispatchTest.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat.dispatch; + +import io.opentelemetry.instrumentation.servlet.v3_0.tomcat.TomcatServlet3Test; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; + +abstract class TomcatDispatchTest extends TomcatServlet3Test { + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setContextPath(getContextPath() + "/dispatch"); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3DispatchAsyncTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3DispatchAsyncTest.java new file mode 100644 index 000000000000..ca9a737a9f05 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3DispatchAsyncTest.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat.dispatch; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import javax.servlet.Servlet; +import org.apache.catalina.Context; + +class TomcatServlet3DispatchAsyncTest extends TomcatDispatchTest { + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setVerifyServerSpanEndTime(false); + } + + @Override + public Class servlet() { + return TestServlet3.Async.class; + } + + @Override + protected void setupServlets(Context context) throws Exception { + super.setupServlets(context); + + addServlet(context, "/dispatch" + SUCCESS.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + QUERY_PARAM.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + ERROR.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + EXCEPTION.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + REDIRECT.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + AUTH_REQUIRED.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + CAPTURE_HEADERS.getPath(), TestServlet3.DispatchAsync.class); + addServlet( + context, "/dispatch" + CAPTURE_PARAMETERS.getPath(), TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch" + INDEXED_CHILD.getPath(), TestServlet3.DispatchAsync.class); + addServlet( + context, "/dispatch" + HTML_PRINT_WRITER.getPath(), TestServlet3.DispatchAsync.class); + addServlet( + context, + "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.getPath(), + TestServlet3.DispatchAsync.class); + addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive.class); + } + + @Override + public boolean errorEndpointUsesSendError() { + return false; + } + + @Override + protected boolean assertParentOnRedirect() { + return false; + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3DispatchImmediateTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3DispatchImmediateTest.java new file mode 100644 index 000000000000..7136887ec70f --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3DispatchImmediateTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat.dispatch; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import javax.servlet.Servlet; +import org.apache.catalina.Context; + +class TomcatServlet3DispatchImmediateTest extends TomcatDispatchTest { + + @Override + public Class servlet() { + return TestServlet3.Sync.class; + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setTestNotFound(false); + } + + @Override + protected void setupServlets(Context context) throws Exception { + super.setupServlets(context); + + addServlet(context, "/dispatch" + SUCCESS.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + QUERY_PARAM.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + ERROR.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + EXCEPTION.getPath(), TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch" + REDIRECT.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + AUTH_REQUIRED.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + CAPTURE_HEADERS.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + CAPTURE_PARAMETERS.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + INDEXED_CHILD.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, "/dispatch" + HTML_PRINT_WRITER.getPath(), TestServlet3.DispatchImmediate.class); + addServlet( + context, + "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.getPath(), + TestServlet3.DispatchImmediate.class); + addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive.class); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3ForwardTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3ForwardTest.java new file mode 100644 index 000000000000..8e76458d959b --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3ForwardTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat.dispatch; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.servlet.v3_0.RequestDispatcherServlet; +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import javax.servlet.Servlet; +import org.apache.catalina.Context; + +class TomcatServlet3ForwardTest extends TomcatDispatchTest { + + @Override + public Class servlet() { + return TestServlet3.Sync.class; // dispatch to sync servlet + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setTestNotFound(false); + } + + @Override + protected void setupServlets(Context context) throws Exception { + super.setupServlets(context); + + addServlet(context, "/dispatch" + SUCCESS.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + QUERY_PARAM.getPath(), RequestDispatcherServlet.Forward.class); + addServlet(context, "/dispatch" + REDIRECT.getPath(), RequestDispatcherServlet.Forward.class); + addServlet(context, "/dispatch" + ERROR.getPath(), RequestDispatcherServlet.Forward.class); + addServlet(context, "/dispatch" + EXCEPTION.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + AUTH_REQUIRED.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + CAPTURE_HEADERS.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, + "/dispatch" + CAPTURE_PARAMETERS.getPath(), + RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + INDEXED_CHILD.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, "/dispatch" + HTML_PRINT_WRITER.getPath(), RequestDispatcherServlet.Forward.class); + addServlet( + context, + "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.getPath(), + RequestDispatcherServlet.Forward.class); + } +} diff --git a/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3IncludeTest.java b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3IncludeTest.java new file mode 100644 index 000000000000..acf4e0569b5d --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/library/src/test/java/io/opentelemetry/instrumentation/servlet/v3_0/tomcat/dispatch/TomcatServlet3IncludeTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0.tomcat.dispatch; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +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.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.servlet.v3_0.RequestDispatcherServlet; +import io.opentelemetry.instrumentation.servlet.v3_0.TestServlet3; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import javax.servlet.Servlet; +import org.apache.catalina.Context; + +class TomcatServlet3IncludeTest extends TomcatDispatchTest { + + @Override + public Class servlet() { + return TestServlet3.Sync.class; // dispatch to sync servlet + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setTestNotFound(false); + options.setTestRedirect(false); + options.setTestCaptureHttpHeaders(false); + options.setTestError(false); + } + + @Override + protected void setupServlets(Context context) throws Exception { + super.setupServlets(context); + + addServlet(context, "/dispatch" + SUCCESS.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, "/dispatch" + QUERY_PARAM.getPath(), RequestDispatcherServlet.Include.class); + addServlet(context, "/dispatch" + REDIRECT.getPath(), RequestDispatcherServlet.Include.class); + addServlet(context, "/dispatch" + ERROR.getPath(), RequestDispatcherServlet.Include.class); + addServlet(context, "/dispatch" + EXCEPTION.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, "/dispatch" + AUTH_REQUIRED.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, + "/dispatch" + CAPTURE_PARAMETERS.getPath(), + RequestDispatcherServlet.Include.class); + addServlet( + context, "/dispatch" + INDEXED_CHILD.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, "/dispatch" + HTML_PRINT_WRITER.getPath(), RequestDispatcherServlet.Include.class); + addServlet( + context, + "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.getPath(), + RequestDispatcherServlet.Include.class); + } +} diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/AsyncRunnableWrapper.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/AsyncRunnableWrapper.java index 31aafe5a6214..f101a8112c53 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/AsyncRunnableWrapper.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/AsyncRunnableWrapper.java @@ -6,6 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.servlet; import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; public class AsyncRunnableWrapper implements Runnable { private final ServletHelper helper; @@ -27,7 +28,7 @@ public static Runnable wrap(ServletHelper helper, Runnable @Override public void run() { - try { + try (Scope scope = context.makeCurrent()) { runnable.run(); } catch (Throwable throwable) { helper.recordAsyncException(context, throwable); diff --git a/settings.gradle.kts b/settings.gradle.kts index f1757a2155a3..d53bad53dd45 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -590,6 +590,7 @@ include(":instrumentation:scala-fork-join-2.8:javaagent") include(":instrumentation:servlet:servlet-2.2:javaagent") include(":instrumentation:servlet:servlet-3.0:javaagent") include(":instrumentation:servlet:servlet-3.0:javaagent-unit-tests") +include(":instrumentation:servlet:servlet-3.0:library") include(":instrumentation:servlet:servlet-3.0:testing") include(":instrumentation:servlet:servlet-5.0:javaagent") include(":instrumentation:servlet:servlet-5.0:javaagent-unit-tests")