From a577ad2d1c17d21b5e60de8bfae97c7b022329bb Mon Sep 17 00:00:00 2001 From: Shashank Patidar Date: Mon, 13 May 2024 19:42:34 +0530 Subject: [PATCH] add instrumentation for servlet-5.0 --- instrumentation/build.gradle.kts | 1 + .../servlet/v3_0/nowrapping/Utils.java | 6 +- .../servlet/servlet-5.0/build.gradle.kts | 38 ++ .../Servlet50AndFilterInstrumentation.java | 239 +++++++++++ .../v5_0/Servlet50InstrumentationModule.java | 52 +++ .../v5_0/Servlet50InstrumentationName.java | 22 + .../hypertrace/servlet/v5_0/Utils.java | 128 ++++++ .../v5_0/async/BodyCaptureAsyncListener.java | 130 ++++++ .../async/Servlet50AsyncInstrumentation.java | 130 ++++++ .../ServletInputStreamInstrumentation.java | 325 ++++++++++++++ .../ServletRequestInstrumentation.java | 261 +++++++++++ .../servlet/v5_0/request/Utils.java | 78 ++++ .../ServletOutputStreamInstrumentation.java | 202 +++++++++ .../ServletResponseInstrumentation.java | 187 ++++++++ .../v5_0/Servlet50InstrumentationTest.java | 405 ++++++++++++++++++ .../hypertrace/servlet/v5_0/TestServlets.java | 294 +++++++++++++ .../java/org/DelegatingBufferedReader.java | 36 ++ .../test/java/org/DelegatingPrintWriter.java | 40 ++ .../org/DelegatingServletInputStream.java | 63 +++ .../org/DelegatingServletOutputStream.java | 63 +++ .../java/org/ServletStreamContextAccess.java | 31 ++ .../test/java/org/TestServletInputStream.java | 49 +++ .../java/org/TestServletOutputStream.java | 37 ++ .../src/test/java/org/WrappingFilter.java | 106 +++++ settings.gradle.kts | 2 + testing-common/build.gradle.kts | 19 + 26 files changed, 2943 insertions(+), 1 deletion(-) create mode 100644 instrumentation/servlet/servlet-5.0/build.gradle.kts create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50AndFilterInstrumentation.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationModule.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationName.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Utils.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/async/BodyCaptureAsyncListener.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/async/Servlet50AsyncInstrumentation.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/ServletInputStreamInstrumentation.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/ServletRequestInstrumentation.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/Utils.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/response/ServletOutputStreamInstrumentation.java create mode 100644 instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/response/ServletResponseInstrumentation.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationTest.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/TestServlets.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingBufferedReader.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingPrintWriter.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingServletInputStream.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingServletOutputStream.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/org/ServletStreamContextAccess.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/org/TestServletInputStream.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/org/TestServletOutputStream.java create mode 100644 instrumentation/servlet/servlet-5.0/src/test/java/org/WrappingFilter.java diff --git a/instrumentation/build.gradle.kts b/instrumentation/build.gradle.kts index 014016548..6a2d1b42e 100644 --- a/instrumentation/build.gradle.kts +++ b/instrumentation/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies{ implementation(project(":instrumentation:servlet:servlet-rw")) implementation(project(":instrumentation:servlet:servlet-3.0")) + implementation(project(":instrumentation:servlet:servlet-5.0")) implementation(project(":instrumentation:spark-2.3")) implementation(project(":instrumentation:grpc-1.6")) implementation(project(":instrumentation:grpc-shaded-netty-1.9")) diff --git a/instrumentation/servlet/servlet-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java b/instrumentation/servlet/servlet-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java index 7176a416d..ccfc9ee2e 100644 --- a/instrumentation/servlet/servlet-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java +++ b/instrumentation/servlet/servlet-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java @@ -29,7 +29,11 @@ import javax.servlet.http.HttpSession; import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair; -import org.hypertrace.agent.core.instrumentation.buffer.*; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair; public class Utils { diff --git a/instrumentation/servlet/servlet-5.0/build.gradle.kts b/instrumentation/servlet/servlet-5.0/build.gradle.kts new file mode 100644 index 000000000..ffffd88d9 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + `java-library` + id("net.bytebuddy.byte-buddy") + id("io.opentelemetry.instrumentation.auto-instrumentation") + muzzle +} +evaluationDependsOn(":javaagent-tooling") + +muzzle { + pass { + group = "jakarta.servlet" + module = "jakarta.servlet-api" + versions = "[5.0.0,)" + } +} + +afterEvaluate{ + io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project, + sourceSets.main.get(), + io.opentelemetry.javaagent.tooling.muzzle.generation.MuzzleCodeGenerationPlugin::class.java.name, + files(project(":javaagent-tooling").configurations["instrumentationMuzzle"], configurations.runtimeClasspath) + ).configure() +} + +val versions: Map by extra + +dependencies { + implementation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-common:${versions["opentelemetry_java_agent"]}") + implementation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-5.0:${versions["opentelemetry_java_agent"]}") // Servlet5Accessor + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-bootstrap:${versions["opentelemetry_java_agent"]}") + compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") + muzzleBootstrap("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-common-bootstrap:${versions["opentelemetry_java_agent"]}") + + testImplementation(project(":testing-common", "shadow")) + testCompileOnly("com.squareup.okhttp3:okhttp:4.9.0") + testImplementation("org.eclipse.jetty:jetty-server:11.0.0") + testImplementation("org.eclipse.jetty:jetty-servlet:11.0.0") +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50AndFilterInstrumentation.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50AndFilterInstrumentation.java new file mode 100644 index 000000000..6200c2867 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50AndFilterInstrumentation.java @@ -0,0 +1,239 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hypertrace.agent.core.config.InstrumentationConfig; +import org.hypertrace.agent.core.filter.FilterResult; +import org.hypertrace.agent.core.instrumentation.HypertraceCallDepthThreadLocalMap; +import org.hypertrace.agent.core.instrumentation.HypertraceEvaluationException; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; +import org.hypertrace.agent.filter.FilterRegistry; + +public class Servlet50AndFilterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("jakarta.servlet.Filter"); + } + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(namedOneOf("jakarta.servlet.Filter", "jakarta.servlet.Servlet")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf("doFilter", "service") + .and(takesArgument(0, named("jakarta.servlet.ServletRequest"))) + .and(takesArgument(1, named("jakarta.servlet.ServletResponse"))) + .and(isPublic()), + Servlet50AndFilterInstrumentation.class.getName() + "$ServletAdvice"); + } + + public static class ServletAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class, skipOn = Advice.OnNonDefaultValue.class) + public static boolean start( + @Advice.Argument(value = 0) ServletRequest request, + @Advice.Argument(value = 1) ServletResponse response, + @Advice.Local("currentSpan") Span currentSpan) { + + int callDepth = + HypertraceCallDepthThreadLocalMap.incrementCallDepth(Servlet50InstrumentationName.class); + if (callDepth > 0) { + return false; + } + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + return false; + } + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + currentSpan = Java8BytecodeBridge.currentSpan(); + + InstrumentationConfig instrumentationConfig = InstrumentationConfig.ConfigProvider.get(); + + Utils.addSessionId(currentSpan, httpRequest); + + // set request headers + Enumeration headerNames = httpRequest.getHeaderNames(); + Map headers = new HashMap<>(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + String headerValue = httpRequest.getHeader(headerName); + AttributeKey attributeKey = + HypertraceSemanticAttributes.httpRequestHeader(headerName); + + if (instrumentationConfig.httpHeaders().request()) { + currentSpan.setAttribute(attributeKey, headerValue); + } + headers.put(attributeKey.getKey(), headerValue); + } + + FilterResult filterResult = + FilterRegistry.getFilter().evaluateRequestHeaders(currentSpan, headers); + if (filterResult.shouldBlock()) { + try { + httpResponse.getWriter().write(filterResult.getBlockingMsg()); + } catch (IOException ignored) { + } + httpResponse.setStatus(filterResult.getBlockingStatusCode()); + // skip execution of the user code + return true; + } + + if (instrumentationConfig.httpBody().request() + && ContentTypeUtils.shouldCapture(httpRequest.getContentType())) { + // The HttpServletRequest instrumentation uses this to + // enable the instrumentation + VirtualField.find(HttpServletRequest.class, SpanAndObjectPair.class) + .set( + httpRequest, + new SpanAndObjectPair(currentSpan, Collections.unmodifiableMap(headers))); + } + return false; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Argument(0) ServletRequest request, + @Advice.Argument(1) ServletResponse response, + @Advice.Thrown(readOnly = false) Throwable throwable, + @Advice.Local("currentSpan") Span currentSpan) { + int callDepth = + HypertraceCallDepthThreadLocalMap.decrementCallDepth(Servlet50InstrumentationName.class); + if (callDepth > 0) { + return; + } + // we are in the most outermost level of Servlet instrumentation + + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + return; + } + + HttpServletResponse httpResponse = (HttpServletResponse) response; + HttpServletRequest httpRequest = (HttpServletRequest) request; + InstrumentationConfig instrumentationConfig = InstrumentationConfig.ConfigProvider.get(); + + try { + // response context to capture body and clear the context + VirtualField responseContextStore = + VirtualField.find(HttpServletResponse.class, SpanAndObjectPair.class); + VirtualField outputStreamContextStore = + VirtualField.find(ServletOutputStream.class, BoundedByteArrayOutputStream.class); + VirtualField writerContextStore = + VirtualField.find(PrintWriter.class, BoundedCharArrayWriter.class); + + // request context to clear body buffer + VirtualField requestContextStore = + VirtualField.find(HttpServletRequest.class, SpanAndObjectPair.class); + VirtualField inputStreamContextStore = + VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class); + VirtualField readerContextStore = + VirtualField.find(BufferedReader.class, CharBufferSpanPair.class); + VirtualField urlEncodedMapContextStore = + VirtualField.find(HttpServletRequest.class, StringMapSpanPair.class); + + if (!request.isAsyncStarted()) { + if (instrumentationConfig.httpHeaders().response()) { + for (String headerName : httpResponse.getHeaderNames()) { + String headerValue = httpResponse.getHeader(headerName); + currentSpan.setAttribute( + HypertraceSemanticAttributes.httpResponseHeader(headerName), headerValue); + } + } + + // capture response body + if (instrumentationConfig.httpBody().response() + && ContentTypeUtils.shouldCapture(httpResponse.getContentType())) { + Utils.captureResponseBody( + currentSpan, + httpResponse, + responseContextStore, + outputStreamContextStore, + writerContextStore); + } + + // remove request body buffers from context stores, otherwise they might get reused + if (instrumentationConfig.httpBody().request() + && ContentTypeUtils.shouldCapture(httpRequest.getContentType())) { + Utils.resetRequestBodyBuffers( + httpRequest, + requestContextStore, + inputStreamContextStore, + readerContextStore, + urlEncodedMapContextStore); + } + } + } finally { + Throwable tmp = throwable; + while (tmp != null) { // loop in case our exception is nested (eg. springframework) + if (tmp instanceof HypertraceEvaluationException) { + FilterResult filterResult = ((HypertraceEvaluationException) tmp).getFilterResult(); + try { + httpResponse.getWriter().write(filterResult.getBlockingMsg()); + } catch (IOException ignored) { + } + httpResponse.setStatus(filterResult.getBlockingStatusCode()); + // bytebuddy treats the reassignment of this variable to null as an instruction to + // suppress this exception, which is what we want + throwable = null; + break; + } + tmp = tmp.getCause(); + } + } + } + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationModule.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationModule.java new file mode 100644 index 000000000..e52023677 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationModule.java @@ -0,0 +1,52 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.async.Servlet50AsyncInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.request.ServletInputStreamInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.request.ServletRequestInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.response.ServletOutputStreamInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.response.ServletResponseInstrumentation; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Servlet50InstrumentationModule extends InstrumentationModule { + + public Servlet50InstrumentationModule() { + super(Servlet50InstrumentationName.PRIMARY, Servlet50InstrumentationName.OTHER); + } + + @Override + public int order() { + return 1; + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new Servlet50AndFilterInstrumentation(), + new ServletRequestInstrumentation(), + new ServletInputStreamInstrumentation(), + new ServletResponseInstrumentation(), + new ServletOutputStreamInstrumentation(), + new Servlet50AsyncInstrumentation()); + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationName.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationName.java new file mode 100644 index 000000000..202567aaf --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationName.java @@ -0,0 +1,22 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0; + +public class Servlet50InstrumentationName { + public static final String PRIMARY = "servlet"; + public static final String[] OTHER = {"servlet-5", "ht", "servlet-ht", "servlet-5-ht"}; +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Utils.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Utils.java new file mode 100644 index 000000000..4bfce8b6b --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Utils.java @@ -0,0 +1,128 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.util.Map; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair; + +public class Utils { + + private Utils() {} + + public static void addSessionId(Span span, HttpServletRequest httpRequest) { + if (httpRequest.isRequestedSessionIdValid()) { + HttpSession session = httpRequest.getSession(); + if (session != null && session.getId() != "") { + span.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_SESSION_ID, session.getId()); + } + } + } + + public static void captureResponseBody( + Span span, + HttpServletResponse httpServletResponse, + VirtualField responseContextStore, + VirtualField streamContextStore, + VirtualField writerContextStore) { + + SpanAndObjectPair responseStreamWriterHolder = responseContextStore.get(httpServletResponse); + if (responseStreamWriterHolder == null) { + return; + } + responseContextStore.set(httpServletResponse, null); + + if (responseStreamWriterHolder.getAssociatedObject() instanceof ServletOutputStream) { + ServletOutputStream servletOutputStream = + (ServletOutputStream) responseStreamWriterHolder.getAssociatedObject(); + BoundedByteArrayOutputStream buffer = streamContextStore.get(servletOutputStream); + if (buffer != null) { + try { + span.setAttribute( + HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, + buffer.toStringWithSuppliedCharset()); + } catch (UnsupportedEncodingException e) { + // should not happen + } + streamContextStore.set(servletOutputStream, null); + } + } else if (responseStreamWriterHolder.getAssociatedObject() instanceof PrintWriter) { + PrintWriter printWriter = (PrintWriter) responseStreamWriterHolder.getAssociatedObject(); + BoundedCharArrayWriter buffer = writerContextStore.get(printWriter); + if (buffer != null) { + span.setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, buffer.toString()); + writerContextStore.set(printWriter, null); + } + } + } + + public static void resetRequestBodyBuffers( + HttpServletRequest httpServletRequest, + VirtualField requestContextStore, + VirtualField streamContextStore, + VirtualField bufferedReaderContextStore, + VirtualField urlEncodedMapContextStore) { + + SpanAndObjectPair requestStreamReaderHolder = requestContextStore.get(httpServletRequest); + if (requestStreamReaderHolder == null) { + return; + } + requestContextStore.set(httpServletRequest, null); + + if (requestStreamReaderHolder.getAssociatedObject() instanceof ServletInputStream) { + ServletInputStream servletInputStream = + (ServletInputStream) requestStreamReaderHolder.getAssociatedObject(); + ByteBufferSpanPair byteBufferSpanPair = streamContextStore.get(servletInputStream); + if (byteBufferSpanPair != null) { + // capture body explicitly e.g. Jackson does not call ServletInputStream$read() until -1 is + // returned + // it does not even call ServletInputStream#available() + byteBufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + streamContextStore.set(servletInputStream, null); + } + } else if (requestStreamReaderHolder.getAssociatedObject() instanceof BufferedReader) { + BufferedReader bufferedReader = + (BufferedReader) requestStreamReaderHolder.getAssociatedObject(); + CharBufferSpanPair charBufferSpanPair = bufferedReaderContextStore.get(bufferedReader); + if (charBufferSpanPair != null) { + charBufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + bufferedReaderContextStore.set(bufferedReader, null); + } + } else if (requestStreamReaderHolder.getAssociatedObject() instanceof Map) { + StringMapSpanPair stringMapSpanPair = urlEncodedMapContextStore.get(httpServletRequest); + if (stringMapSpanPair != null) { + stringMapSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + urlEncodedMapContextStore.set(httpServletRequest, null); + } + } + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/async/BodyCaptureAsyncListener.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/async/BodyCaptureAsyncListener.java new file mode 100644 index 000000000..5e8b54727 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/async/BodyCaptureAsyncListener.java @@ -0,0 +1,130 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.async; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.Utils; +import io.opentelemetry.javaagent.instrumentation.servlet.ServletAsyncListener; +import io.opentelemetry.javaagent.instrumentation.servlet.v5_0.Servlet5Singletons; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.util.concurrent.atomic.AtomicBoolean; +import org.hypertrace.agent.core.config.InstrumentationConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public final class BodyCaptureAsyncListener implements ServletAsyncListener { + + private static final InstrumentationConfig instrumentationConfig = + InstrumentationConfig.ConfigProvider.get(); + + private final AtomicBoolean responseHandled; + private final Span span; + + private final VirtualField responseContextStore; + private final VirtualField streamContextStore; + private final VirtualField writerContextStore; + + private final VirtualField requestContextStore; + private final VirtualField inputStreamContextStore; + private final VirtualField readerContextStore; + private final VirtualField urlEncodedMapContextStore; + private final HttpServletRequest request; + + public BodyCaptureAsyncListener( + AtomicBoolean responseHandled, + VirtualField responseContextStore, + VirtualField streamContextStore, + VirtualField writerContextStore, + VirtualField requestContextStore, + VirtualField inputStreamContextStore, + VirtualField readerContextStore, + VirtualField urlEncodedMapContextStore, + HttpServletRequest request) { + this.responseHandled = responseHandled; + this.span = Span.fromContext(Servlet5Singletons.helper().getServerContext(request)); + this.responseContextStore = responseContextStore; + this.streamContextStore = streamContextStore; + this.writerContextStore = writerContextStore; + this.requestContextStore = requestContextStore; + this.inputStreamContextStore = inputStreamContextStore; + this.readerContextStore = readerContextStore; + this.urlEncodedMapContextStore = urlEncodedMapContextStore; + this.request = request; + } + + @Override + public void onComplete(HttpServletResponse response) { + if (responseHandled.compareAndSet(false, true)) { + captureResponseDataAndClearRequestBuffer(response, request); + } + } + + @Override + public void onError(Throwable throwable, HttpServletResponse response) { + if (responseHandled.compareAndSet(false, true)) { + captureResponseDataAndClearRequestBuffer(response, request); + } + } + + @Override + public void onTimeout(long timeout) { + // noop + } + + private void captureResponseDataAndClearRequestBuffer( + HttpServletResponse servletResponse, HttpServletRequest servletRequest) { + if (servletResponse != null) { + if (instrumentationConfig.httpBody().response() + && ContentTypeUtils.shouldCapture(servletResponse.getContentType())) { + Utils.captureResponseBody( + span, servletResponse, responseContextStore, streamContextStore, writerContextStore); + } + + if (instrumentationConfig.httpHeaders().response()) { + for (String headerName : servletResponse.getHeaderNames()) { + String headerValue = servletResponse.getHeader(headerName); + span.setAttribute( + HypertraceSemanticAttributes.httpResponseHeader(headerName), headerValue); + } + } + } + if (servletRequest != null) { + // remove request body buffers from context stores, otherwise they might get reused + if (instrumentationConfig.httpBody().request() + && ContentTypeUtils.shouldCapture(servletRequest.getContentType())) { + Utils.resetRequestBodyBuffers( + servletRequest, + requestContextStore, + inputStreamContextStore, + readerContextStore, + urlEncodedMapContextStore); + } + } + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/async/Servlet50AsyncInstrumentation.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/async/Servlet50AsyncInstrumentation.java new file mode 100644 index 000000000..59b5cf220 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/async/Servlet50AsyncInstrumentation.java @@ -0,0 +1,130 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.async; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.servlet.ServletHelper; +import io.opentelemetry.javaagent.instrumentation.servlet.v5_0.Servlet5Accessor; +import io.opentelemetry.javaagent.instrumentation.servlet.v5_0.Servlet5Singletons; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.util.concurrent.atomic.AtomicBoolean; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hypertrace.agent.core.instrumentation.HypertraceCallDepthThreadLocalMap; +import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair; + +public final class Servlet50AsyncInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("jakarta.servlet.Servlet"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("jakarta.servlet.ServletRequest")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("startAsync").and(returns(named("jakarta.servlet.AsyncContext"))).and(isPublic()), + Servlet50AsyncInstrumentation.class.getName() + "$StartAsyncAdvice"); + } + + static final class StartAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startAsyncEnter() { + // This allows to detect the outermost invocation of startAsync in method exit + HypertraceCallDepthThreadLocalMap.incrementCallDepth(AsyncContext.class); + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void startAsyncExit(@Advice.This ServletRequest servletRequest) { + int callDepth = HypertraceCallDepthThreadLocalMap.decrementCallDepth(AsyncContext.class); + if (callDepth != 0) { + // This is not the outermost invocation, ignore. + return; + } + + // response context to capture body and clear the context + VirtualField responseContextStore = + VirtualField.find(HttpServletResponse.class, SpanAndObjectPair.class); + VirtualField outputStreamContextStore = + VirtualField.find(ServletOutputStream.class, BoundedByteArrayOutputStream.class); + VirtualField writerContextStore = + VirtualField.find(PrintWriter.class, BoundedCharArrayWriter.class); + + // request context to clear body buffer + VirtualField requestContextStore = + VirtualField.find(HttpServletRequest.class, SpanAndObjectPair.class); + VirtualField inputStreamContextStore = + VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class); + VirtualField readerContextStore = + VirtualField.find(BufferedReader.class, CharBufferSpanPair.class); + VirtualField urlEncodedMapContextStore = + VirtualField.find(HttpServletRequest.class, StringMapSpanPair.class); + + if (servletRequest instanceof HttpServletRequest) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + Servlet5Accessor accessor = Servlet5Accessor.INSTANCE; + if (accessor.getRequestAttribute(request, HYPERTRACE_ASYNC_LISTENER_ATTRIBUTE) == null) { + ServletHelper helper = + Servlet5Singletons.helper(); + accessor.addRequestAsyncListener( + request, + new BodyCaptureAsyncListener( + new AtomicBoolean(), + responseContextStore, + outputStreamContextStore, + writerContextStore, + requestContextStore, + inputStreamContextStore, + readerContextStore, + urlEncodedMapContextStore, + request), + helper.getAsyncListenerResponse(request)); + accessor.setRequestAttribute(request, HYPERTRACE_ASYNC_LISTENER_ATTRIBUTE, true); + } + } + } + + public static final String HYPERTRACE_ASYNC_LISTENER_ATTRIBUTE = "org.hypertrace.AsyncListener"; + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/ServletInputStreamInstrumentation.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/ServletInputStreamInstrumentation.java new file mode 100644 index 000000000..12664471d --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/ServletInputStreamInstrumentation.java @@ -0,0 +1,325 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.request; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import jakarta.servlet.ServletInputStream; +import java.io.IOException; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hypertrace.agent.core.instrumentation.HypertraceCallDepthThreadLocalMap; +import org.hypertrace.agent.core.instrumentation.HypertraceEvaluationException; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; + +public class ServletInputStreamInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("jakarta.servlet.ServletInputStream")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("read").and(takesArguments(0)).and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadNoArgs"); + transformer.applyAdviceToMethod( + named("read") + .and(takesArguments(1)) + .and(takesArgument(0, is(byte[].class))) + .and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadByteArray"); + transformer.applyAdviceToMethod( + named("read") + .and(takesArguments(3)) + .and(takesArgument(0, is(byte[].class))) + .and(takesArgument(1, is(int.class))) + .and(takesArgument(2, is(int.class))) + .and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadByteArrayOffset"); + transformer.applyAdviceToMethod( + named("readAllBytes").and(takesArguments(0)).and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadAllBytes"); + transformer.applyAdviceToMethod( + named("readNBytes") + .and(takesArguments(0)) + .and(takesArgument(0, is(byte[].class))) + .and(takesArgument(1, is(int.class))) + .and(takesArgument(2, is(int.class))) + .and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadNBytes"); + + // ServletInputStream methods + transformer.applyAdviceToMethod( + named("readLine") + .and(takesArguments(3)) + .and(takesArgument(0, is(byte[].class))) + .and(takesArgument(1, is(int.class))) + .and(takesArgument(2, is(int.class))) + .and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadByteArrayOffset"); + } + + static class InputStream_ReadNoArgs { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void exit( + @Advice.Return int read, @Advice.Enter ByteBufferSpanPair bufferSpanPair) { + try { + if (bufferSpanPair == null) { + return; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.writeToBuffer((byte) read); + } + } catch (Throwable t) { + if (t instanceof HypertraceEvaluationException) { + throw t; + } else { + // ignore + } + } + } + } + + public static class InputStream_ReadByteArray { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void exit( + @Advice.This ServletInputStream thizz, + @Advice.Return int read, + @Advice.Argument(0) byte b[], + @Advice.Enter ByteBufferSpanPair bufferSpanPair) + throws Throwable { + try { + if (bufferSpanPair == null) { + return; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.writeToBuffer(b, 0, read); + if (thizz.available() == 0) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } + } + } catch (Throwable t) { + if (t instanceof HypertraceEvaluationException) { + throw t; + } else { + // ignore + } + } + } + } + + public static class InputStream_ReadByteArrayOffset { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void exit( + @Advice.This ServletInputStream thizz, + @Advice.Return int read, + @Advice.Argument(0) byte b[], + @Advice.Argument(1) int off, + @Advice.Argument(2) int len, + @Advice.Enter ByteBufferSpanPair bufferSpanPair) + throws Throwable { + try { + + if (bufferSpanPair == null) { + return; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.writeToBuffer(b, off, read); + if (thizz.available() == 0) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } + } + } catch (Throwable t) { + if (t instanceof HypertraceEvaluationException) { + throw t; + } else { + // ignore + } + } + } + } + + public static class InputStream_ReadAllBytes { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void exit( + @Advice.Return byte[] b, @Advice.Enter ByteBufferSpanPair bufferSpanPair) + throws IOException { + try { + if (bufferSpanPair == null) { + return; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + bufferSpanPair.writeToBuffer(b); + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } catch (Throwable t) { + if (t instanceof HypertraceEvaluationException) { + throw t; + } else { + // ignore + } + } + } + } + + public static class InputStream_ReadNBytes { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void exit( + @Advice.This ServletInputStream thizz, + @Advice.Return int read, + @Advice.Argument(0) byte[] b, + @Advice.Argument(1) int off, + @Advice.Argument(2) int len, + @Advice.Enter ByteBufferSpanPair bufferSpanPair) + throws Throwable { + try { + if (bufferSpanPair == null) { + return; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.writeToBuffer(b, off, read); + if (thizz.available() == 0) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } + } + } catch (Throwable t) { + if (t instanceof HypertraceEvaluationException) { + throw t; + } else { + // ignore + } + } + } + } + + private static final String HANDLER_NAME = + "io.opentelemetry.javaagent.bootstrap.ExceptionLogger".replace('.', '/'); + private static final String LOGGER_NAME = "org.slf4j.Logger".replace('.', '/'); + private static final String LOG_FACTORY_NAME = "org.slf4j.LoggerFactory".replace('.', '/'); +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/ServletRequestInstrumentation.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/ServletRequestInstrumentation.java new file mode 100644 index 000000000..b74ddc3e8 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/ServletRequestInstrumentation.java @@ -0,0 +1,261 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.request; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hypertrace.agent.core.instrumentation.HypertraceCallDepthThreadLocalMap; +import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair; + +public class ServletRequestInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("jakarta.servlet.ServletRequest")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("getInputStream") + .and(takesArguments(0)) + .and(returns(named("jakarta.servlet.ServletInputStream"))) + .and(isPublic()), + ServletRequestInstrumentation.class.getName() + "$ServletRequest_getInputStream_advice"); + transformer.applyAdviceToMethod( + named("getReader") + .and(takesArguments(0)) + // .and(returns(BufferedReader.class)) + .and(isPublic()), + ServletRequestInstrumentation.class.getName() + "$ServletRequest_getReader_advice"); + transformer.applyAdviceToMethod( + named("getParameter") + .and(takesArguments(1)) + .and(takesArgument(0, is(String.class))) + .and(returns(String.class)) + .and(isPublic()), + ServletRequestInstrumentation.class.getName() + "$ServletRequest_getParameter_advice"); + } + + static class ServletRequest_getInputStream_advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static SpanAndObjectPair enter(@Advice.This ServletRequest servletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + // span is added in servlet/filter instrumentation if data capture is enabled + SpanAndObjectPair requestBufferWrapper = + VirtualField.find(HttpServletRequest.class, SpanAndObjectPair.class) + .get(httpServletRequest); + if (requestBufferWrapper == null) { + return null; + } + + // the getReader method might call getInputStream + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletRequest.class); + return requestBufferWrapper; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.This ServletRequest servletRequest, + @Advice.Return ServletInputStream servletInputStream, + @Advice.Thrown Throwable throwable, + @Advice.Enter SpanAndObjectPair spanAndObjectPair) { + + if (spanAndObjectPair == null) { + return; + } + + int callDepth = HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletRequest.class); + if (callDepth > 0) { + return; + } + + if (!(servletRequest instanceof HttpServletRequest) || throwable != null) { + return; + } + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + VirtualField contextStore = + VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class); + if (contextStore.get(servletInputStream) != null) { + // getInputStream() can be called multiple times + return; + } + + ByteBufferSpanPair bufferSpanPair = + Utils.createRequestByteBufferSpanPair( + httpServletRequest, spanAndObjectPair.getSpan(), spanAndObjectPair.getHeaders()); + contextStore.set(servletInputStream, bufferSpanPair); + spanAndObjectPair.setAssociatedObject(servletInputStream); + } + } + + static class ServletRequest_getReader_advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static SpanAndObjectPair enter(@Advice.This ServletRequest servletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + SpanAndObjectPair spanAndObjectPair = + VirtualField.find(HttpServletRequest.class, SpanAndObjectPair.class) + .get(httpServletRequest); + if (spanAndObjectPair == null) { + return null; + } + + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletRequest.class); + return spanAndObjectPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.This ServletRequest servletRequest, + @Advice.Return BufferedReader reader, + @Advice.Thrown Throwable throwable, + @Advice.Enter SpanAndObjectPair spanAndObjectPair) { + + if (spanAndObjectPair == null) { + return; + } + + int callDepth = HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletRequest.class); + if (callDepth > 0) { + return; + } + + if (!(servletRequest instanceof HttpServletRequest) || throwable != null) { + return; + } + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + VirtualField contextStore = + VirtualField.find(BufferedReader.class, CharBufferSpanPair.class); + if (contextStore.get(reader) != null) { + // getReader() can be called multiple times + return; + } + + CharBufferSpanPair bufferSpanPair = + Utils.createRequestCharBufferSpanPair( + httpServletRequest, spanAndObjectPair.getSpan(), spanAndObjectPair.getHeaders()); + contextStore.set(reader, bufferSpanPair); + spanAndObjectPair.setAssociatedObject(reader); + } + } + + /** Provides instrumentation template for ServletRequest.getParameter() method. */ + static class ServletRequest_getParameter_advice { + + /** + * Instrumentation template for ServletRequest.getParameter() entry point. + * + * @param servletRequest + * @return a (possibly null) SpanAndObjectPair, which will be passed to the method exit + * instrumentation + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static SpanAndObjectPair enter(@Advice.This ServletRequest servletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + SpanAndObjectPair spanAndObjectPair = + VirtualField.find(HttpServletRequest.class, SpanAndObjectPair.class) + .get(httpServletRequest); + if (spanAndObjectPair == null) { + return null; + } + + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletRequest.class); + return spanAndObjectPair; + } + + /** + * Instrumentation template for ServletRequest.getParameter() exit point(s). + * + * @param servletRequest the ServletRequest instance + * @param returnValue the value that is being returned by getParameter() + * @param parmName the argument that was passed to getParameter() + * @param throwable the Throwable object, if exiting method because of a 'throw' + * @param spanAndObjectPair the value returned by the getParameter() method entry + * instrumentation + */ + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.This ServletRequest servletRequest, + @Advice.Return String returnValue, + @Advice.Argument(0) String parmName, + @Advice.Thrown Throwable throwable, + @Advice.Enter SpanAndObjectPair spanAndObjectPair) { + if (spanAndObjectPair == null) { + return; + } + + int callDepth = HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletRequest.class); + if (callDepth > 0) { + return; + } + + if (returnValue == null) { + return; + } + + if (!(servletRequest instanceof HttpServletRequest) || throwable != null) { + return; + } + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + Map stringMap; + + VirtualField contextStore = + VirtualField.find(HttpServletRequest.class, StringMapSpanPair.class); + + StringMapSpanPair stringMapSpanPair = contextStore.get(httpServletRequest); + + if (stringMapSpanPair != null) { + stringMap = stringMapSpanPair.stringMap; + } else { + stringMap = new HashMap<>(); + stringMapSpanPair = + Utils.createStringMapSpanPair( + stringMap, spanAndObjectPair.getSpan(), spanAndObjectPair.getHeaders()); + contextStore.set(httpServletRequest, stringMapSpanPair); + } + + stringMap.put(parmName, returnValue); + spanAndObjectPair.setAssociatedObject(stringMap); + } + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/Utils.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/Utils.java new file mode 100644 index 000000000..35355db86 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/request/Utils.java @@ -0,0 +1,78 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.request; + +import io.opentelemetry.api.trace.Span; +import jakarta.servlet.http.HttpServletRequest; +import java.nio.charset.Charset; +import java.util.Map; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair; +import org.hypertrace.agent.core.instrumentation.utils.ContentLengthUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeCharsetUtils; +import org.hypertrace.agent.filter.FilterRegistry; +import org.hypertrace.agent.filter.api.Filter; + +public class Utils { + + private static final Filter filter = FilterRegistry.getFilter(); + + private Utils() {} + + public static ByteBufferSpanPair createRequestByteBufferSpanPair( + HttpServletRequest httpServletRequest, Span span, Map headers) { + String charsetStr = httpServletRequest.getCharacterEncoding(); + Charset charset = ContentTypeCharsetUtils.toCharset(charsetStr); + int contentLength = httpServletRequest.getContentLength(); + if (contentLength < 0) { + contentLength = ContentLengthUtils.DEFAULT; + } + return new ByteBufferSpanPair( + span, + BoundedBuffersFactory.createStream(contentLength, charset), + filter::evaluateRequestBody, + headers); + } + + public static CharBufferSpanPair createRequestCharBufferSpanPair( + HttpServletRequest httpServletRequest, Span span, Map headers) { + int contentLength = httpServletRequest.getContentLength(); + if (contentLength < 0) { + contentLength = ContentLengthUtils.DEFAULT; + } + return new CharBufferSpanPair( + span, + BoundedBuffersFactory.createWriter(contentLength), + filter::evaluateRequestBody, + headers); + } + + /** + * Create a StringMapSpanPair. + * + * @param stringMap + * @param span + * @param headers + * @return + */ + public static StringMapSpanPair createStringMapSpanPair( + Map stringMap, Span span, Map headers) { + return new StringMapSpanPair(span, stringMap, headers); + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/response/ServletOutputStreamInstrumentation.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/response/ServletOutputStreamInstrumentation.java new file mode 100644 index 000000000..5d5f14b8d --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/response/ServletOutputStreamInstrumentation.java @@ -0,0 +1,202 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.response; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import jakarta.servlet.ServletOutputStream; +import java.io.IOException; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hypertrace.agent.core.instrumentation.HypertraceCallDepthThreadLocalMap; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; + +public class ServletOutputStreamInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("jakarta.servlet.ServletOutputStream")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("print") + .and(takesArguments(1)) + .and(takesArgument(0, is(String.class))) + .and(isPublic()), + ServletOutputStreamInstrumentation.class.getName() + "$ServletOutputStream_print"); + // other print methods call print or write on the OutputStream + + // OutputStream methods + transformer.applyAdviceToMethod( + named("write").and(takesArguments(1)).and(takesArgument(0, is(int.class))).and(isPublic()), + ServletOutputStreamInstrumentation.class.getName() + "$OutputStream_write"); + transformer.applyAdviceToMethod( + named("write") + .and(takesArguments(1)) + .and(takesArgument(0, is(byte[].class))) + .and(isPublic()), + ServletOutputStreamInstrumentation.class.getName() + "$OutputStream_writeByteArr"); + transformer.applyAdviceToMethod( + named("write") + .and(takesArguments(3)) + .and(takesArgument(0, is(byte[].class))) + .and(takesArgument(1, is(int.class))) + .and(takesArgument(2, is(int.class))) + .and(isPublic()), + ServletOutputStreamInstrumentation.class.getName() + "$OutputStream_writeByteArrOffset"); + + // close is not called on Tomcat (tested with Spring Boot) + // transformer.applyAdviceToMethod( + // named("close").and(takesArguments(0)) + // .and(isPublic()), + // ServletOutputStreamInstrumentation.class.getName() + "$OutputStream_close"); + } + + static class OutputStream_write { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedByteArrayOutputStream enter( + @Advice.This ServletOutputStream thizz, @Advice.Argument(0) int b) { + BoundedByteArrayOutputStream buffer = + VirtualField.find(ServletOutputStream.class, BoundedByteArrayOutputStream.class) + .get(thizz); + if (buffer == null) { + return null; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletOutputStream.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(b); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedByteArrayOutputStream buffer) { + if (buffer != null) { + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletOutputStream.class); + } + } + } + + static class OutputStream_writeByteArr { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedByteArrayOutputStream enter( + @Advice.This ServletOutputStream thizz, @Advice.Argument(0) byte[] b) throws IOException { + + BoundedByteArrayOutputStream buffer = + VirtualField.find(ServletOutputStream.class, BoundedByteArrayOutputStream.class) + .get(thizz); + if (buffer == null) { + return null; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletOutputStream.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(b); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedByteArrayOutputStream buffer) { + if (buffer != null) { + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletOutputStream.class); + } + } + } + + static class OutputStream_writeByteArrOffset { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedByteArrayOutputStream enter( + @Advice.This ServletOutputStream thizz, + @Advice.Argument(0) byte b[], + @Advice.Argument(1) int off, + @Advice.Argument(2) int len) { + + BoundedByteArrayOutputStream buffer = + VirtualField.find(ServletOutputStream.class, BoundedByteArrayOutputStream.class) + .get(thizz); + if (buffer == null) { + return null; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletOutputStream.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(b, off, len); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedByteArrayOutputStream buffer) { + if (buffer != null) { + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletOutputStream.class); + } + } + } + + static class ServletOutputStream_print { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedByteArrayOutputStream enter( + @Advice.This ServletOutputStream thizz, @Advice.Argument(0) String s) throws IOException { + + BoundedByteArrayOutputStream buffer = + VirtualField.find(ServletOutputStream.class, BoundedByteArrayOutputStream.class) + .get(thizz); + if (buffer == null) { + return null; + } + int callDepth = + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletOutputStream.class); + if (callDepth > 0) { + return buffer; + } + + String bodyPart = s == null ? "null" : s; + buffer.write(bodyPart.getBytes()); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedByteArrayOutputStream buffer) { + if (buffer != null) { + HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletOutputStream.class); + } + } + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/response/ServletResponseInstrumentation.java b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/response/ServletResponseInstrumentation.java new file mode 100644 index 000000000..76dccc798 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/response/ServletResponseInstrumentation.java @@ -0,0 +1,187 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.response; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hypertrace.agent.core.config.InstrumentationConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceCallDepthThreadLocalMap; +import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeCharsetUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public class ServletResponseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("jakarta.servlet.ServletResponse")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("getOutputStream") + .and(takesArguments(0)) + .and(returns(named("jakarta.servlet.ServletOutputStream"))) + .and(isPublic()), + ServletResponseInstrumentation.class.getName() + "$ServletResponse_getOutputStream"); + transformer.applyAdviceToMethod( + named("getWriter").and(takesArguments(0)).and(returns(PrintWriter.class)).and(isPublic()), + ServletResponseInstrumentation.class.getName() + "$ServletResponse_getWriter_advice"); + } + + static class ServletResponse_getOutputStream { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static HttpServletResponse enter(@Advice.This ServletResponse servletResponse) { + if (!(servletResponse instanceof HttpServletResponse)) { + return null; + } + // ignore wrappers, the filter/servlet instrumentation gets the captured body from context + // store + // by using response as a key and the filter/servlet instrumentation runs early when wrappers + // are not used. + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + if (httpServletResponse instanceof HttpServletResponseWrapper) { + return null; + } + + // the getReader method might call getInputStream + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletResponse.class); + return httpServletResponse; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Enter HttpServletResponse httpServletResponse, + @Advice.Thrown Throwable throwable, + @Advice.Return ServletOutputStream servletOutputStream) { + + if (httpServletResponse == null) { + return; + } + + int callDepth = HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletResponse.class); + if (callDepth > 0) { + return; + } + if (throwable != null) { + return; + } + + VirtualField contextStore = + VirtualField.find(ServletOutputStream.class, BoundedByteArrayOutputStream.class); + if (contextStore.get(servletOutputStream) != null) { + // getOutputStream() can be called multiple times + return; + } + + // do not capture if data capture is disabled or not supported content type + InstrumentationConfig instrumentationConfig = InstrumentationConfig.ConfigProvider.get(); + String contentType = httpServletResponse.getContentType(); + if (instrumentationConfig.httpBody().response() + && ContentTypeUtils.shouldCapture(contentType)) { + + String charsetStr = httpServletResponse.getCharacterEncoding(); + Charset charset = ContentTypeCharsetUtils.toCharset(charsetStr); + BoundedByteArrayOutputStream buffer = BoundedBuffersFactory.createStream(charset); + contextStore.set(servletOutputStream, buffer); + SpanAndObjectPair spanAndObjectPair = new SpanAndObjectPair(null, null); + spanAndObjectPair.setAssociatedObject(servletOutputStream); + VirtualField.find(HttpServletResponse.class, SpanAndObjectPair.class) + .set(httpServletResponse, spanAndObjectPair); + } + } + } + + static class ServletResponse_getWriter_advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static HttpServletResponse enter(@Advice.This ServletResponse servletResponse) { + if (!(servletResponse instanceof HttpServletResponse)) { + return null; + } + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + if (httpServletResponse instanceof HttpServletResponseWrapper) { + return null; + } + + // the getWriter method might call getInputStream + HypertraceCallDepthThreadLocalMap.incrementCallDepth(ServletResponse.class); + return httpServletResponse; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Enter HttpServletResponse httpServletResponse, + @Advice.Thrown Throwable throwable, + @Advice.Return PrintWriter printWriter) { + + if (httpServletResponse == null) { + return; + } + + int callDepth = HypertraceCallDepthThreadLocalMap.decrementCallDepth(ServletResponse.class); + if (callDepth > 0) { + return; + } + if (throwable != null) { + return; + } + + VirtualField contextStore = + VirtualField.find(PrintWriter.class, BoundedCharArrayWriter.class); + if (contextStore.get(printWriter) != null) { + // getWriter() can be called multiple times + return; + } + + // do not capture if data capture is disabled or not supported content type + InstrumentationConfig instrumentationConfig = InstrumentationConfig.ConfigProvider.get(); + String contentType = httpServletResponse.getContentType(); + if (instrumentationConfig.httpBody().response() + && ContentTypeUtils.shouldCapture(contentType)) { + + BoundedCharArrayWriter writer = BoundedBuffersFactory.createWriter(); + contextStore.set(printWriter, writer); + SpanAndObjectPair spanAndObjectPair = new SpanAndObjectPair(null, null); + spanAndObjectPair.setAssociatedObject(printWriter); + VirtualField.find(HttpServletResponse.class, SpanAndObjectPair.class) + .set(httpServletResponse, spanAndObjectPair); + } + } + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationTest.java b/instrumentation/servlet/servlet-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationTest.java new file mode 100644 index 000000000..c80cf1003 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/Servlet50InstrumentationTest.java @@ -0,0 +1,405 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0; + +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoAsyncResponse_stream; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoAsyncResponse_writer; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoReader_read_large_array; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoStream_arr; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoStream_arr_offset; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoStream_readLine_print; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoStream_read_large_array; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoStream_single_byte; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.EchoWriter_single_char; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.TestServlets.GetHello; +import io.opentelemetry.proto.trace.v1.Span; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.io.IOException; +import java.util.EnumSet; +import java.util.List; +import okhttp3.FormBody; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.WrappingFilter; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.hypertrace.agent.testing.AbstractInstrumenterTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class Servlet50InstrumentationTest extends AbstractInstrumenterTest { + private static final String REQUEST_BODY = "hello"; + private static final String REQUEST_HEADER = "requestheader"; + private static final String REQUEST_HEADER_VALUE = "requestvalue"; + + private static Server server = new Server(0); + private static int serverPort; + + /* + * Filter that mimics the spring framework. It will catch and wrap our blocking exception + */ + public static class WrapExceptionFilter implements Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + System.out.print("hello from filter"); + try { + chain.doFilter(request, response); + } catch (Throwable t) { + if (t.getClass().getName().contains("HypertraceEvaluationException")) { + throw new RuntimeException("wrapped exception", t); + } + throw t; + } + } + + @Override + public void init(FilterConfig arg0) throws ServletException {} + + @Override + public void destroy() {} + } + + @BeforeAll + public static void startServer() throws Exception { + ServletContextHandler handler = new ServletContextHandler(); + + handler.addFilter(WrappingFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); + + handler.addServlet(GetHello.class, "/hello"); + handler.addServlet(EchoStream_single_byte.class, "/echo_stream_single_byte"); + handler.addServlet(EchoStream_arr.class, "/echo_stream_arr"); + handler.addFilter( + WrapExceptionFilter.class, "/echo_stream_arr", EnumSet.of(DispatcherType.REQUEST)); + handler.addServlet(EchoStream_arr_offset.class, "/echo_stream_arr_offset"); + handler.addServlet(EchoStream_readLine_print.class, "/echo_stream_readLine_print"); + handler.addServlet(EchoWriter_single_char.class, "/echo_writer_single_char"); + handler.addServlet(TestServlets.EchoWriter_arr.class, "/echo_writer_arr"); + handler.addServlet(TestServlets.EchoWriter_arr_offset.class, "/echo_writer_arr_offset"); + handler.addServlet(TestServlets.EchoWriter_readLine_write.class, "/echo_writer_readLine_write"); + handler.addServlet(TestServlets.EchoWriter_readLines.class, "/echo_writer_readLines"); + handler.addServlet( + TestServlets.EchoWriter_readLine_print_str.class, "/echo_writer_readLine_print_str"); + handler.addServlet( + TestServlets.EchoWriter_readLine_print_arr.class, "/echo_writer_readLine_print_arr"); + handler.addServlet(TestServlets.Forward_to_post.class, "/forward_to_echo"); + handler.addServlet(EchoAsyncResponse_stream.class, "/echo_async_response_stream"); + handler.addServlet(EchoAsyncResponse_writer.class, "/echo_async_response_writer"); + handler.addServlet(EchoStream_read_large_array.class, "/echo_stream_read_large_array"); + handler.addServlet(EchoReader_read_large_array.class, "/echo_reader_read_large_array"); + server.setHandler(handler); + server.start(); + serverPort = ((ServerConnector) server.getConnectors()[0]).getLocalPort(); + } + + @AfterAll + public static void stopServer() throws Exception { + server.stop(); + } + + @Test + public void forward_to_post() throws Exception { + postJson(String.format("http://localhost:%d/forward_to_echo", serverPort)); + } + + @Test + public void echo_async_response_stream() throws Exception { + postJson(String.format("http://localhost:%d/echo_async_response_stream", serverPort)); + } + + @Test + public void echo_async_response_writer() throws Exception { + postJson(String.format("http://localhost:%d/echo_async_response_writer", serverPort)); + } + + @Test + public void postJson_stream_single_byte() throws Exception { + postJson(String.format("http://localhost:%d/echo_stream_single_byte", serverPort)); + } + + @Test + public void postJson_stream_read_large_array() throws Exception { + postJson(String.format("http://localhost:%d/echo_stream_read_large_array", serverPort)); + } + + @Test + public void postJson_reader_read_large_array() throws Exception { + postJson(String.format("http://localhost:%d/echo_reader_read_large_array", serverPort)); + } + + @Test + public void postJson_stream_arr() throws Exception { + postJson(String.format("http://localhost:%d/echo_stream_arr", serverPort)); + } + + @Test + public void postJson_stream_arr_offset() throws Exception { + postJson(String.format("http://localhost:%d/echo_stream_arr_offset", serverPort)); + } + + @Test + public void postJson_stream_readLine_print() throws Exception { + postJson(String.format("http://localhost:%d/echo_stream_readLine_print", serverPort)); + } + + @Test + public void postJson_writer_single_char() throws Exception { + postJson(String.format("http://localhost:%d/echo_writer_single_char", serverPort)); + } + + @Test + public void postJson_writer_arr() throws Exception { + postJson(String.format("http://localhost:%d/echo_writer_arr", serverPort)); + } + + @Test + public void postJson_writer_arr_offset() throws Exception { + postJson(String.format("http://localhost:%d/echo_writer_arr_offset", serverPort)); + } + + @Test + public void postJson_writer_readLine_write() throws Exception { + postJson(String.format("http://localhost:%d/echo_writer_readLine_write", serverPort)); + } + + @Test + public void postJson_writer_readLine_print_str() throws Exception { + postJson(String.format("http://localhost:%d/echo_writer_readLine_print_str", serverPort)); + } + + @Test + public void postJson_writer_readLines() throws Exception { + postJson(String.format("http://localhost:%d/echo_writer_readLines", serverPort)); + } + + @Test + public void postJson_writer_readLine_print_arr() throws Exception { + postJson(String.format("http://localhost:%d/echo_writer_readLine_print_arr", serverPort)); + } + + @Test + public void portUrlEncoded() throws Exception { + FormBody formBody = new FormBody.Builder().add("key1", "value1").add("key2", "value2").build(); + Request request = + new Request.Builder() + .url(String.format("http://localhost:%d/echo_stream_single_byte", serverPort)) + .post(formBody) + .header(REQUEST_HEADER, REQUEST_HEADER_VALUE) + .build(); + try (Response response = httpClient.newCall(request).execute()) { + Assertions.assertEquals(200, response.code()); + } + + TEST_WRITER.waitForTraces(1); + List> traces = + TEST_WRITER.waitForSpans(1, span -> span.getKind().equals(Span.SpanKind.SPAN_KIND_CLIENT)); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + Span span = spans.get(0); + Assertions.assertEquals( + REQUEST_HEADER_VALUE, + TEST_WRITER + .getAttributesMap(span) + .get("http.request.header." + REQUEST_HEADER) + .getStringValue()); + Assertions.assertEquals( + TestServlets.RESPONSE_HEADER_VALUE, + TEST_WRITER + .getAttributesMap(span) + .get("http.response.header." + TestServlets.RESPONSE_HEADER) + .getStringValue()); + Assertions.assertEquals( + "key1=value1&key2=value2", + TEST_WRITER.getAttributesMap(span).get("http.request.body").getStringValue()); + Assertions.assertEquals( + TestServlets.RESPONSE_BODY, + TEST_WRITER.getAttributesMap(span).get("http.response.body").getStringValue()); + } + + @Test + public void getHello() throws Exception { + Request request = + new Request.Builder() + .url(String.format("http://localhost:%d/hello", serverPort)) + .get() + .header(REQUEST_HEADER, REQUEST_HEADER_VALUE) + .build(); + try (Response response = httpClient.newCall(request).execute()) { + Assertions.assertEquals(204, response.code()); + } + + TEST_WRITER.waitForTraces(1); + List> traces = + TEST_WRITER.waitForSpans(1, span -> span.getKind().equals(Span.SpanKind.SPAN_KIND_CLIENT)); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + Span span = spans.get(0); + Assertions.assertEquals( + REQUEST_HEADER_VALUE, + TEST_WRITER + .getAttributesMap(span) + .get("http.request.header." + REQUEST_HEADER) + .getStringValue()); + Assertions.assertEquals( + TestServlets.RESPONSE_HEADER_VALUE, + TEST_WRITER + .getAttributesMap(span) + .get("http.response.header." + TestServlets.RESPONSE_HEADER) + .getStringValue()); + Assertions.assertNull(TEST_WRITER.getAttributesMap(span).get("http.request.body")); + Assertions.assertNull(TEST_WRITER.getAttributesMap(span).get("http.response.body")); + } + + @Test + public void block() throws Exception { + Request request = + new Request.Builder() + .url(String.format("http://localhost:%d/hello", serverPort)) + .get() + .header("mockblock", "true") + .build(); + try (Response response = httpClient.newCall(request).execute()) { + Assertions.assertEquals(403, response.code()); + Assertions.assertEquals("Hypertrace Blocked Request", response.body().string()); + } + + TEST_WRITER.waitForTraces(1); + List> traces = + TEST_WRITER.waitForSpans(1, span -> span.getKind().equals(Span.SpanKind.SPAN_KIND_CLIENT)); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + Span span = spans.get(0); + Assertions.assertNull( + TEST_WRITER + .getAttributesMap(span) + .get("http.response.header." + TestServlets.RESPONSE_HEADER)); + Assertions.assertNull(TEST_WRITER.getAttributesMap(span).get("http.request.body")); + Assertions.assertNull(TEST_WRITER.getAttributesMap(span).get("http.response.body")); + } + + @Test + public void blockBody() throws Exception { + FormBody formBody = new FormBody.Builder().add("block", "true").build(); + Request request = + new Request.Builder() + .url(String.format("http://localhost:%d/echo_stream_single_byte", serverPort)) + .post(formBody) + .header(REQUEST_HEADER, REQUEST_HEADER_VALUE) + .build(); + try (Response response = httpClient.newCall(request).execute()) { + Assertions.assertEquals(403, response.code()); + Assertions.assertEquals("Hypertrace Blocked Request", response.body().string()); + } + + TEST_WRITER.waitForTraces(1); + List> traces = + TEST_WRITER.waitForSpans(1, span -> span.getKind().equals(Span.SpanKind.SPAN_KIND_CLIENT)); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + Span span = spans.get(0); + Assertions.assertNull( + TEST_WRITER + .getAttributesMap(span) + .get("http.response.header." + TestServlets.RESPONSE_HEADER)); + Assertions.assertEquals( + "block=true", TEST_WRITER.getAttributesMap(span).get("http.request.body").getStringValue()); + Assertions.assertNull(TEST_WRITER.getAttributesMap(span).get("http.response.body")); + } + + @Test + public void blockBodyWrappedException() throws Exception { + FormBody formBody = new FormBody.Builder().add("block", "true").build(); + Request request = + new Request.Builder() + .url(String.format("http://localhost:%d/echo_stream_arr", serverPort)) + .post(formBody) + .header(REQUEST_HEADER, REQUEST_HEADER_VALUE) + .build(); + try (Response response = httpClient.newCall(request).execute()) { + Assertions.assertEquals(403, response.code()); + Assertions.assertEquals("Hypertrace Blocked Request", response.body().string()); + } + + TEST_WRITER.waitForTraces(1); + List> traces = + TEST_WRITER.waitForSpans(1, span -> span.getKind().equals(Span.SpanKind.SPAN_KIND_CLIENT)); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + Span span = spans.get(0); + Assertions.assertNull( + TEST_WRITER + .getAttributesMap(span) + .get("http.response.header." + TestServlets.RESPONSE_HEADER)); + Assertions.assertEquals( + "block=true", TEST_WRITER.getAttributesMap(span).get("http.request.body").getStringValue()); + Assertions.assertNull(TEST_WRITER.getAttributesMap(span).get("http.response.body")); + } + + public void postJson(String url) throws Exception { + Request request = + new Request.Builder() + .url(url) + .post(RequestBody.create(REQUEST_BODY, MediaType.get("application/json"))) + .header(REQUEST_HEADER, REQUEST_HEADER_VALUE) + .build(); + try (Response response = httpClient.newCall(request).execute()) { + Assertions.assertEquals(200, response.code()); + Assertions.assertEquals(TestServlets.RESPONSE_BODY, response.body().string()); + } + + TEST_WRITER.waitForTraces(1); + List> traces = + TEST_WRITER.waitForSpans(1, span -> span.getKind().equals(Span.SpanKind.SPAN_KIND_CLIENT)); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + Span span = spans.get(0); + Assertions.assertEquals( + REQUEST_HEADER_VALUE, + TEST_WRITER + .getAttributesMap(span) + .get("http.request.header." + REQUEST_HEADER) + .getStringValue()); + Assertions.assertEquals( + TestServlets.RESPONSE_HEADER_VALUE, + TEST_WRITER + .getAttributesMap(span) + .get("http.response.header." + TestServlets.RESPONSE_HEADER) + .getStringValue()); + Assertions.assertEquals( + REQUEST_BODY, TEST_WRITER.getAttributesMap(span).get("http.request.body").getStringValue()); + Assertions.assertEquals( + TestServlets.RESPONSE_BODY, + TEST_WRITER.getAttributesMap(span).get("http.response.body").getStringValue()); + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/TestServlets.java b/instrumentation/servlet/servlet-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/TestServlets.java new file mode 100644 index 000000000..e383284e7 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v5_0/TestServlets.java @@ -0,0 +1,294 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.stream.Stream; + +public class TestServlets { + + public static final String RESPONSE_BODY = "{\"key\": \"val\"}"; + + public static final String RESPONSE_HEADER = "responseheader"; + public static final String RESPONSE_HEADER_VALUE = "responsevalue"; + + public static class GetHello extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getInputStream().read() != -1) {} + resp.setStatus(204); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + resp.getWriter().write("hello"); + } + } + + public static class EchoStream_single_byte extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getInputStream().read() != -1) {} + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + byte[] response_bodyBytes = RESPONSE_BODY.getBytes(); + for (int i = 0; i < RESPONSE_BODY.length(); i++) { + resp.getOutputStream().write(response_bodyBytes[i]); + } + } + } + + public static class EchoStream_arr extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getInputStream().read(new byte[2]) != -1) {} + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + resp.getOutputStream().write(RESPONSE_BODY.getBytes()); + } + } + + public static class EchoStream_arr_offset extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getInputStream().read(new byte[12], 3, 2) != -1) {} + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + byte[] responseBytes = RESPONSE_BODY.getBytes(); + resp.getOutputStream().write(responseBytes, 0, 2); + resp.getOutputStream().write(responseBytes, 2, 1); + resp.getOutputStream().write(responseBytes, 3, responseBytes.length - 3); + } + } + + public static class EchoStream_readLine_print extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getInputStream().readLine(new byte[14], 3, 3) != -1) {} + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + resp.getOutputStream().print(RESPONSE_BODY); + } + } + + public static class EchoWriter_single_char extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getReader().read() != -1) {} + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + for (int i = 0; i < RESPONSE_BODY.length(); i++) + resp.getWriter().write(RESPONSE_BODY.charAt(i)); + } + } + + public static class EchoWriter_arr extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getReader().read(new char[2]) != -1) {} + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + resp.getWriter().write(RESPONSE_BODY.toCharArray()); + } + } + + public static class EchoWriter_arr_offset extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getReader().read(new char[12], 3, 2) != -1) {} + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + char[] chars = RESPONSE_BODY.toCharArray(); + resp.getWriter().write(chars, 0, 2); + resp.getWriter().write(chars, 2, 2); + resp.getWriter().write(chars, 4, chars.length - 4); + } + } + + public static class EchoWriter_readLine_write extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getReader().readLine() != null) {} + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + resp.getWriter().write(RESPONSE_BODY); + } + } + + public static class EchoWriter_readLines extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + Stream lines = req.getReader().lines(); + lines.forEach(s -> {}); + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + resp.getWriter().write(RESPONSE_BODY); + } + } + + public static class EchoWriter_readLine_print_str extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getReader().readLine() != null) {} + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + resp.getWriter().print(RESPONSE_BODY); + } + } + + public static class EchoWriter_readLine_print_arr extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getReader().readLine() != null) {} + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + resp.getWriter().print(RESPONSE_BODY.toCharArray()); + } + } + + public static class EchoAsyncResponse_stream extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + + AsyncContext asyncContext = req.startAsync(); + asyncContext.start( + () -> { + while (true) { + try { + if (!(req.getInputStream().read() != -1)) break; + } catch (IOException e) { + e.printStackTrace(); + } + } + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + HttpServletResponse httpServletResponse = + (HttpServletResponse) asyncContext.getResponse(); + httpServletResponse.setStatus(200); + httpServletResponse.setContentType("application/json"); + httpServletResponse.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + try { + httpServletResponse.getOutputStream().print(RESPONSE_BODY); + } catch (IOException e) { + e.printStackTrace(); + } + asyncContext.complete(); + }); + } + } + + public static class EchoAsyncResponse_writer extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + + AsyncContext asyncContext = req.startAsync(); + asyncContext.start( + () -> { + while (true) { + try { + if (!(req.getReader().read() != -1)) break; + } catch (IOException e) { + e.printStackTrace(); + } + } + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + HttpServletResponse httpServletResponse = + (HttpServletResponse) asyncContext.getResponse(); + httpServletResponse.setStatus(200); + httpServletResponse.setContentType("application/json"); + httpServletResponse.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + try { + httpServletResponse.getWriter().print(RESPONSE_BODY); + } catch (IOException e) { + e.printStackTrace(); + } + asyncContext.complete(); + }); + } + } + + public static class Forward_to_post extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + req.getRequestDispatcher("/echo_stream_single_byte").forward(req, resp); + } + } + + public static class EchoStream_read_large_array extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + req.getInputStream().read(new byte[1000], 0, 1000); + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + resp.getWriter().print(RESPONSE_BODY.toCharArray()); + } + } + + public static class EchoReader_read_large_array extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + req.getReader().read(new char[1000], 0, 1000); + + resp.setStatus(200); + resp.setContentType("application/json"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + + resp.getWriter().print(RESPONSE_BODY.toCharArray()); + } + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingBufferedReader.java b/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingBufferedReader.java new file mode 100644 index 000000000..ebc67937f --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingBufferedReader.java @@ -0,0 +1,36 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; + +public class DelegatingBufferedReader extends BufferedReader { + + private final Reader delegate; + + public DelegatingBufferedReader(Reader delegate) { + super(delegate); + this.delegate = delegate; + } + + @Override + public int read(char[] cbuf) throws IOException { + return delegate.read(cbuf); + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingPrintWriter.java b/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingPrintWriter.java new file mode 100644 index 000000000..6522c50b6 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingPrintWriter.java @@ -0,0 +1,40 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; + +public class DelegatingPrintWriter extends PrintWriter { + + private final Writer delegate; + + public DelegatingPrintWriter(Writer delegate) { + super(delegate); + this.delegate = delegate; + } + + @Override + public void write(char[] buf) { + try { + this.delegate.write(buf); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingServletInputStream.java b/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingServletInputStream.java new file mode 100644 index 000000000..6f734e6b0 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingServletInputStream.java @@ -0,0 +1,63 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import java.io.IOException; + +public class DelegatingServletInputStream extends ServletInputStream { + + private final ServletInputStream wrapped; + + public DelegatingServletInputStream(ServletInputStream wrapped) { + this.wrapped = wrapped; + } + + @Override + public int read() throws IOException { + return wrapped.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return wrapped.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return wrapped.read(b, off, len); + } + + @Override + public int readLine(byte[] b, int off, int len) throws IOException { + return wrapped.readLine(b, off, len); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) {} +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingServletOutputStream.java b/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingServletOutputStream.java new file mode 100644 index 000000000..9ece67452 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/org/DelegatingServletOutputStream.java @@ -0,0 +1,63 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import java.io.IOException; + +public class DelegatingServletOutputStream extends ServletOutputStream { + + private final ServletOutputStream delegate; + + public DelegatingServletOutputStream(ServletOutputStream delegate) { + this.delegate = delegate; + } + + @Override + public void write(int b) throws IOException { + this.delegate.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + this.delegate.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.delegate.write(b, off, len); + } + + @Override + public void flush() throws IOException { + this.delegate.flush(); + } + + @Override + public void close() throws IOException { + this.delegate.close(); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) {} +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/org/ServletStreamContextAccess.java b/instrumentation/servlet/servlet-5.0/src/test/java/org/ServletStreamContextAccess.java new file mode 100644 index 000000000..449ca8355 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/org/ServletStreamContextAccess.java @@ -0,0 +1,31 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; + +public class ServletStreamContextAccess { + + public static void addToInputStreamContext( + ServletInputStream servletInputStream, ByteBufferSpanPair buffer) {} + + public static void addToOutputStreamContext( + ServletOutputStream servletOutputStream, BoundedByteArrayOutputStream buffer) {} +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/org/TestServletInputStream.java b/instrumentation/servlet/servlet-5.0/src/test/java/org/TestServletInputStream.java new file mode 100644 index 000000000..986ab7edd --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/org/TestServletInputStream.java @@ -0,0 +1,49 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class TestServletInputStream extends ServletInputStream { + + private final InputStream wrapped; + + public TestServletInputStream(InputStream wrapped) { + this.wrapped = wrapped; + } + + @Override + public int read() throws IOException { + return wrapped.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) {} +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/org/TestServletOutputStream.java b/instrumentation/servlet/servlet-5.0/src/test/java/org/TestServletOutputStream.java new file mode 100644 index 000000000..39a38b446 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/org/TestServletOutputStream.java @@ -0,0 +1,37 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import java.io.IOException; + +public class TestServletOutputStream extends ServletOutputStream { + + @Override + public void write(int b) throws IOException { + // noop + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) {} +} diff --git a/instrumentation/servlet/servlet-5.0/src/test/java/org/WrappingFilter.java b/instrumentation/servlet/servlet-5.0/src/test/java/org/WrappingFilter.java new file mode 100644 index 000000000..2b2d3f1b7 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/src/test/java/org/WrappingFilter.java @@ -0,0 +1,106 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; + +public class WrappingFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void destroy() {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + + ReqWrapper reqWrapper = new ReqWrapper(httpServletRequest); + RespWrapper respWrapper = new RespWrapper(httpServletResponse); + chain.doFilter(reqWrapper, respWrapper); + } + + static class ReqWrapper extends HttpServletRequestWrapper { + + private ServletInputStream servletInputStream; + private BufferedReader bufferedReader; + + public ReqWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (servletInputStream == null) { + servletInputStream = new DelegatingServletInputStream(super.getInputStream()); + } + return servletInputStream; + } + + @Override + public BufferedReader getReader() throws IOException { + if (bufferedReader == null) { + bufferedReader = new DelegatingBufferedReader(super.getReader()); + } + return bufferedReader; + } + } + + static class RespWrapper extends HttpServletResponseWrapper { + + private ServletOutputStream servletOutputStream; + private PrintWriter printWriter; + + public RespWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + if (servletOutputStream == null) { + servletOutputStream = new DelegatingServletOutputStream(super.getOutputStream()); + } + return servletOutputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (printWriter == null) { + printWriter = new DelegatingPrintWriter(super.getWriter()); + } + return printWriter; + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index abe38b05d..424409867 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -67,3 +67,5 @@ findProject(":instrumentation:undertow:undertow-1.4")?.name = "undertow-1.4" include("instrumentation:undertow:undertow-servlet-1.4") findProject(":instrumentation:undertow:undertow-servlet-1.4")?.name = "undertow-servlet-1.4" include("tests-extension") +include("instrumentation:servlet:servlet-5.0") +findProject(":instrumentation:servlet:servlet-5.0")?.name = "servlet-5.0" diff --git a/testing-common/build.gradle.kts b/testing-common/build.gradle.kts index eb5ad188b..8db17e03a 100644 --- a/testing-common/build.gradle.kts +++ b/testing-common/build.gradle.kts @@ -1,6 +1,7 @@ import com.google.protobuf.gradle.* plugins { + id("com.github.johnrengelman.shadow") `java-library` idea id("com.google.protobuf") version "0.9.4" @@ -47,3 +48,21 @@ dependencies { annotationProcessor("com.google.auto.service:auto-service:1.0") implementation("org.eclipse.jetty:jetty-server:8.0.0.v20110901") } + +tasks { + shadowJar { + dependencies{ + // exclude packages that live in the bootstrap classloader + exclude(project(":javaagent-core")) + exclude(project(":filter-api")) + exclude("io/opentelemetry/semconv/**") + exclude("io/opentelemetry/context/**") + exclude(dependency("io.opentelemetry:opentelemetry-api")) + exclude("io/opentelemetry/instrumentation/api/**") + // exclude bootstrap part of javaagent-extension-api + exclude("io/opentelemetry/javaagent/bootstrap/**") + } + // relocate jetty so that tests using jetty do not conflict with this one + relocate("org.eclipse.jetty", "io.opentelemetry.javaagent.shaded.org.eclipse.jetty") + } +}