diff --git a/instrumentation/build.gradle.kts b/instrumentation/build.gradle.kts index 10fde0cb1..28a6c3be1 100644 --- a/instrumentation/build.gradle.kts +++ b/instrumentation/build.gradle.kts @@ -33,6 +33,8 @@ dependencies{ implementation(project(":instrumentation:servlet:servlet-2.3")) implementation(project(":instrumentation:servlet:servlet-3.0")) implementation(project(":instrumentation:servlet:servlet-3.1")) + implementation(project(":instrumentation:servlet:servlet-rw")) + implementation(project(":instrumentation:servlet:servlet-3.0-no-wrapping")) implementation(project(":instrumentation:spark-2.3")) implementation(project(":instrumentation:grpc-1.5")) implementation(project(":instrumentation:okhttp:okhttp-3.0")) diff --git a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModule.java b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModule.java index 5aab856f1..7db461bd9 100644 --- a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModule.java +++ b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModule.java @@ -17,13 +17,16 @@ package io.opentelemetry.javaagent.instrumentation.hypertrace.java.inputstream; import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; 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.not; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; import io.opentelemetry.javaagent.tooling.InstrumentationModule; import io.opentelemetry.javaagent.tooling.TypeInstrumentation; import java.io.IOException; @@ -68,7 +71,8 @@ static class InputStreamInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { - return extendsClass(named(InputStream.class.getName())); + return extendsClass(named(InputStream.class.getName())) + .and(not(safeHasSuperType(named("javax.servlet.ServletInputStream")))); } @Override @@ -116,14 +120,20 @@ public static SpanAndBuffer enter(@Advice.This InputStream thizz) { return InputStreamUtils.check(thizz); } - @Advice.OnMethodExit(suppress = Throwable.class) + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) public static void exit( @Advice.This InputStream thizz, @Advice.Return int read, @Advice.Enter SpanAndBuffer spanAndBuffer) { - if (spanAndBuffer != null) { - InputStreamUtils.read(thizz, spanAndBuffer, read); + if (spanAndBuffer == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(InputStream.class); + if (callDepth > 0) { + return; } + + InputStreamUtils.read(thizz, spanAndBuffer, read); } } @@ -133,15 +143,21 @@ public static SpanAndBuffer enter(@Advice.This InputStream thizz) { return InputStreamUtils.check(thizz); } - @Advice.OnMethodExit(suppress = Throwable.class) + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) public static void exit( @Advice.This InputStream thizz, @Advice.Return int read, @Advice.Argument(0) byte b[], @Advice.Enter SpanAndBuffer spanAndBuffer) { - if (spanAndBuffer != null) { - InputStreamUtils.read(thizz, spanAndBuffer, read, b); + if (spanAndBuffer == null) { + return; } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(InputStream.class); + if (callDepth > 0) { + return; + } + + InputStreamUtils.read(thizz, spanAndBuffer, read, b); } } @@ -151,7 +167,7 @@ public static SpanAndBuffer enter(@Advice.This InputStream thizz) { return InputStreamUtils.check(thizz); } - @Advice.OnMethodExit(suppress = Throwable.class) + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) public static void exit( @Advice.This InputStream thizz, @Advice.Return int read, @@ -159,9 +175,15 @@ public static void exit( @Advice.Argument(1) int off, @Advice.Argument(2) int len, @Advice.Enter SpanAndBuffer spanAndBuffer) { - if (spanAndBuffer != null) { - InputStreamUtils.read(thizz, spanAndBuffer, read, b, off, len); + if (spanAndBuffer == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(InputStream.class); + if (callDepth > 0) { + return; } + + InputStreamUtils.read(thizz, spanAndBuffer, read, b, off, len); } } @@ -171,15 +193,21 @@ public static SpanAndBuffer enter(@Advice.This InputStream thizz) { return InputStreamUtils.check(thizz); } - @Advice.OnMethodExit(suppress = Throwable.class) + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) public static void exit( @Advice.This InputStream thizz, @Advice.Return byte[] b, @Advice.Enter SpanAndBuffer spanAndBuffer) throws IOException { - if (spanAndBuffer != null) { - InputStreamUtils.readAll(thizz, spanAndBuffer, b); + if (spanAndBuffer == null) { + return; } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(InputStream.class); + if (callDepth > 0) { + return; + } + + InputStreamUtils.readAll(thizz, spanAndBuffer, b); } } @@ -189,7 +217,7 @@ public static SpanAndBuffer enter(@Advice.This InputStream thizz) { return InputStreamUtils.check(thizz); } - @Advice.OnMethodExit(suppress = Throwable.class) + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) public static void exit( @Advice.This InputStream thizz, @Advice.Return int read, @@ -197,6 +225,13 @@ public static void exit( @Advice.Argument(1) int off, @Advice.Argument(2) int len, @Advice.Enter SpanAndBuffer spanAndBuffer) { + if (spanAndBuffer == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(InputStream.class); + if (callDepth > 0) { + return; + } InputStreamUtils.readNBytes(thizz, spanAndBuffer, read, b, off, len); } } diff --git a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java index 2d43ff7b5..db37914fc 100644 --- a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java +++ b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java @@ -76,10 +76,7 @@ public static SpanAndBuffer check(InputStream inputStream) { return null; } - int callDepth = CallDepthThreadLocalMap.incrementCallDepth(InputStream.class); - if (callDepth > 0) { - return null; - } + CallDepthThreadLocalMap.incrementCallDepth(InputStream.class); return spanAndBuffer; } @@ -94,7 +91,6 @@ public static void read(InputStream inputStream, SpanAndBuffer spanAndBuffer, in spanAndBuffer.charset); GlobalObjectRegistry.inputStreamToSpanAndBufferMap.remove(inputStream); } - CallDepthThreadLocalMap.reset(InputStream.class); } public static void read( @@ -109,7 +105,6 @@ public static void read( spanAndBuffer.charset); GlobalObjectRegistry.inputStreamToSpanAndBufferMap.remove(inputStream); } - CallDepthThreadLocalMap.reset(InputStream.class); } public static void read( @@ -124,14 +119,12 @@ public static void read( spanAndBuffer.charset); GlobalObjectRegistry.inputStreamToSpanAndBufferMap.remove(inputStream); } - CallDepthThreadLocalMap.reset(InputStream.class); } public static void readAll(InputStream inputStream, SpanAndBuffer spanAndBuffer, byte[] b) throws IOException { spanAndBuffer.byteArrayBuffer.write(b); GlobalObjectRegistry.inputStreamToSpanAndBufferMap.remove(inputStream); - CallDepthThreadLocalMap.reset(InputStream.class); } public static void readNBytes( @@ -146,7 +139,6 @@ public static void readNBytes( } else { spanAndBuffer.byteArrayBuffer.write(b, off, read); } - CallDepthThreadLocalMap.reset(InputStream.class); } public static void available(InputStream inputStream, int available) { diff --git a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/outputstream/OutputStreamInstrumentationModule.java b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/outputstream/OutputStreamInstrumentationModule.java index d12ed42f3..9e851eea4 100644 --- a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/outputstream/OutputStreamInstrumentationModule.java +++ b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/outputstream/OutputStreamInstrumentationModule.java @@ -24,6 +24,7 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; import io.opentelemetry.javaagent.tooling.InstrumentationModule; import io.opentelemetry.javaagent.tooling.TypeInstrumentation; import java.io.IOException; @@ -37,6 +38,7 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; import org.hypertrace.agent.core.instrumentation.GlobalObjectRegistry; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; /** * {@link OutputStream} instrumentation. The type matcher applies to all implementations. However @@ -96,51 +98,79 @@ public Map, String> transfor static class OutputStream_WriteIntAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static boolean enter(@Advice.This OutputStream thizz) { - return OutputStreamUtils.check(thizz); + public static BoundedByteArrayOutputStream enter( + @Advice.This OutputStream thizz, @Advice.Argument(0) int b) { + BoundedByteArrayOutputStream buffer = GlobalObjectRegistry.outputStreamToBufferMap.get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(OutputStream.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(b); + return buffer; } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void exit( - @Advice.This OutputStream thizz, - @Advice.Argument(0) int b, - @Advice.Enter boolean retEnterAdvice) - throws IOException { - OutputStreamUtils.write(thizz, retEnterAdvice, b); + public static void exit(@Advice.Enter BoundedByteArrayOutputStream buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(OutputStream.class); + } } } static class OutputStream_WriteByteArrAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static boolean enter(@Advice.This OutputStream thizz) { - return OutputStreamUtils.check(thizz); + public static BoundedByteArrayOutputStream enter( + @Advice.This OutputStream thizz, @Advice.Argument(0) byte b[]) throws IOException { + BoundedByteArrayOutputStream buffer = GlobalObjectRegistry.outputStreamToBufferMap.get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(OutputStream.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(b); + return buffer; } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void exit( - @Advice.This OutputStream thizz, - @Advice.Argument(0) byte b[], - @Advice.Enter boolean retEnterAdvice) - throws IOException { - OutputStreamUtils.write(thizz, retEnterAdvice, b); + public static void exit(@Advice.Enter BoundedByteArrayOutputStream buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(OutputStream.class); + } } } static class OutputStream_WriteByteArrOffsetAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static boolean enter(@Advice.This OutputStream thizz) { - return OutputStreamUtils.check(thizz); - } - - @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void exit( + public static BoundedByteArrayOutputStream enter( @Advice.This OutputStream thizz, @Advice.Argument(0) byte b[], @Advice.Argument(1) int off, - @Advice.Argument(2) int len, - @Advice.Enter boolean retEnterAdvice) - throws IOException { - OutputStreamUtils.write(thizz, retEnterAdvice, b, off, len); + @Advice.Argument(2) int len) { + BoundedByteArrayOutputStream buffer = GlobalObjectRegistry.outputStreamToBufferMap.get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(OutputStream.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) { + CallDepthThreadLocalMap.decrementCallDepth(OutputStream.class); + } } } } diff --git a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/outputstream/OutputStreamUtils.java b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/outputstream/OutputStreamUtils.java deleted file mode 100644 index ee777b4d8..000000000 --- a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/outputstream/OutputStreamUtils.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.java.outputstream; - -import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; -import java.io.IOException; -import java.io.OutputStream; -import org.hypertrace.agent.core.instrumentation.GlobalObjectRegistry; - -public class OutputStreamUtils { - - private OutputStreamUtils() {} - - public static boolean check(OutputStream outputStream) { - Object outStream = GlobalObjectRegistry.outputStreamToBufferMap.get(outputStream); - if (outStream == null) { - return false; - } - - int callDepth = CallDepthThreadLocalMap.incrementCallDepth(OutputStream.class); - if (callDepth > 0) { - return false; - } - return true; - } - - public static void write(OutputStream thizzOutputStream, boolean retEnterAdvice, int b) - throws IOException { - if (!retEnterAdvice) { - return; - } - - Object outStream = GlobalObjectRegistry.outputStreamToBufferMap.get(thizzOutputStream); - if (outStream == null) { - return; - } - OutputStream outputStream = (OutputStream) outStream; - outputStream.write(b); - CallDepthThreadLocalMap.reset(OutputStream.class); - } - - public static void write(OutputStream thizzOutputStream, boolean retEnterAdvice, byte[] b) - throws IOException { - if (!retEnterAdvice) { - return; - } - - Object outStream = GlobalObjectRegistry.outputStreamToBufferMap.get(thizzOutputStream); - if (outStream == null) { - return; - } - OutputStream outputStream = (OutputStream) outStream; - outputStream.write(b); - CallDepthThreadLocalMap.reset(OutputStream.class); - } - - public static void write( - OutputStream thizzOutputStream, boolean retEnterAdvice, byte[] b, int off, int len) - throws IOException { - if (!retEnterAdvice) { - return; - } - Object outStream = GlobalObjectRegistry.outputStreamToBufferMap.get(thizzOutputStream); - if (outStream == null) { - return; - } - OutputStream outputStream = (OutputStream) outStream; - outputStream.write(b, off, len); - CallDepthThreadLocalMap.reset(OutputStream.class); - } -} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/build.gradle.kts b/instrumentation/servlet/servlet-3.0-no-wrapping/build.gradle.kts new file mode 100644 index 000000000..f77e96167 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + `java-library` + id("net.bytebuddy.byte-buddy") + id("io.opentelemetry.instrumentation.auto-instrumentation") + muzzle +} + +muzzle { + pass { + group = "javax.servlet" + module = "javax.servlet-api" + versions = "[3.0.0,)" + } + // fail on all old servlet-api. This groupId was changed in 3.x to javax.servlet-api + fail { + group = "javax.servlet" + module = "servlet-api" + versions = "(,)" + } +} + +afterEvaluate{ + io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project, + sourceSets.main.get(), + "io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin", + project(":javaagent-tooling").configurations["instrumentationMuzzle"] + configurations.runtimeClasspath + ).configure() +} + +val versions: Map by extra + +dependencies { + api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-3.0:${versions["opentelemetry_java_agent"]}") + + compileOnly("javax.servlet:javax.servlet-api:3.1.0") + + testImplementation(project(":instrumentation:servlet:servlet-rw")) + testImplementation(project(":testing-common")) { + exclude(group ="org.eclipse.jetty", module= "jetty-server") + } + testImplementation("org.eclipse.jetty:jetty-server:8.1.22.v20160922") + testImplementation("org.eclipse.jetty:jetty-servlet:8.1.22.v20160922") +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/BodyCaptureAsyncListener.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/BodyCaptureAsyncListener.java new file mode 100644 index 000000000..d9b108277 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/BodyCaptureAsyncListener.java @@ -0,0 +1,93 @@ +/* + * 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.v3_0.nowrapping; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import java.io.PrintWriter; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.config.HypertraceConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public class BodyCaptureAsyncListener implements AsyncListener { + + private final AtomicBoolean responseHandled; + private final Span span; + private final ContextStore streamContextStore; + private final ContextStore writerContextStore; + + private final AgentConfig agentConfig = HypertraceConfig.get(); + + public BodyCaptureAsyncListener( + AtomicBoolean responseHandled, + Span span, + ContextStore streamContextStore, + ContextStore writerContextStore) { + this.responseHandled = responseHandled; + this.span = span; + this.streamContextStore = streamContextStore; + this.writerContextStore = writerContextStore; + } + + @Override + public void onComplete(AsyncEvent event) { + if (responseHandled.compareAndSet(false, true)) { + captureResponseData(event.getSuppliedResponse()); + } + } + + @Override + public void onError(AsyncEvent event) { + if (responseHandled.compareAndSet(false, true)) { + captureResponseData(event.getSuppliedResponse()); + } + } + + @Override + public void onTimeout(AsyncEvent event) {} + + @Override + public void onStartAsync(AsyncEvent event) {} + + private void captureResponseData(ServletResponse servletResponse) { + if (servletResponse instanceof HttpServletResponse) { + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + + if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue() + && ContentTypeUtils.shouldCapture(httpResponse.getContentType())) { + Utils.captureResponseBody(span, streamContextStore, writerContextStore, httpResponse); + } + + if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) { + for (String headerName : httpResponse.getHeaderNames()) { + String headerValue = httpResponse.getHeader(headerName); + span.setAttribute( + HypertraceSemanticAttributes.httpResponseHeader(headerName), headerValue); + } + } + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31InstrumentationName.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31InstrumentationName.java new file mode 100644 index 000000000..7654413b5 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31InstrumentationName.java @@ -0,0 +1,30 @@ +/* + * 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.v3_0.nowrapping; + +public class Servlet31InstrumentationName { + public static final String PRIMARY = "servlet"; + public static final String[] OTHER = { + "servlet-3", + "ht", + "servlet-ht", + "servlet-3-ht", + "servlet-3-no-wrapping", + "servlet-no-wrapping", + "servlet-no-wrapping" + }; +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentation.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentation.java new file mode 100644 index 000000000..39145c36f --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentation.java @@ -0,0 +1,201 @@ +/* + * 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.v3_0.nowrapping; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.config.HypertraceConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +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.utils.ContentTypeUtils; +import org.hypertrace.agent.filter.FilterRegistry; + +public class Servlet31NoWrappingInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(namedOneOf("javax.servlet.Filter", "javax.servlet.http.HttpServlet")); + } + + @Override + public Map, String> transformers() { + Map, String> matchers = new HashMap<>(); + matchers.put( + namedOneOf("doFilter", "service") + .and(takesArgument(0, named("javax.servlet.ServletRequest"))) + .and(takesArgument(1, named("javax.servlet.ServletResponse"))) + .and(isPublic()), + Servlet31NoWrappingInstrumentation.class.getName() + "$ServletAdvice"); + return matchers; + } + + 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 = + CallDepthThreadLocalMap.incrementCallDepth(Servlet31InstrumentationName.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(); + + AgentConfig agentConfig = HypertraceConfig.get(); + String contentType = httpRequest.getContentType(); + if (agentConfig.getDataCapture().getHttpBody().getRequest().getValue() + && ContentTypeUtils.shouldCapture(contentType)) { + // The HttpServletRequest instrumentation uses this to + // enable the instrumentation + InstrumentationContext.get(HttpServletRequest.class, Span.class) + .put(httpRequest, currentSpan); + } + + 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 (HypertraceConfig.get().getDataCapture().getHttpHeaders().getRequest().getValue()) { + currentSpan.setAttribute(attributeKey, headerValue); + } + headers.put(attributeKey.getKey(), headerValue); + } + + if (FilterRegistry.getFilter().evaluateRequestHeaders(currentSpan, headers)) { + httpResponse.setStatus(403); + // skip execution of the user code + return true; + } + return false; + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void exit( + @Advice.Argument(0) ServletRequest request, + @Advice.Argument(1) ServletResponse response, + @Advice.Local("currentSpan") Span currentSpan) + throws IOException { + int callDepth = + CallDepthThreadLocalMap.decrementCallDepth(Servlet31InstrumentationName.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; + AgentConfig agentConfig = HypertraceConfig.get(); + + ContextStore outputStreamContext = + InstrumentationContext.get(ServletOutputStream.class, BoundedByteArrayOutputStream.class); + ContextStore writerContext = + InstrumentationContext.get(PrintWriter.class, BoundedCharArrayWriter.class); + + // remove request body buffers from context stores, otherwise they might get reused + if (agentConfig.getDataCapture().getHttpBody().getRequest().getValue() + && ContentTypeUtils.shouldCapture(httpRequest.getContentType())) { + ContextStore inputStreamContext = + InstrumentationContext.get(ServletInputStream.class, ByteBufferSpanPair.class); + ContextStore readerContext = + InstrumentationContext.get(BufferedReader.class, CharBufferSpanPair.class); + Utils.resetRequestBodyBuffers(inputStreamContext, readerContext, httpRequest); + } + + AtomicBoolean responseHandled = new AtomicBoolean(false); + if (request.isAsyncStarted()) { + try { + request + .getAsyncContext() + .addListener( + new BodyCaptureAsyncListener( + responseHandled, currentSpan, outputStreamContext, writerContext)); + } catch (IllegalStateException e) { + // org.eclipse.jetty.server.Request may throw an exception here if request became + // finished after check above. We just ignore that exception and move on. + } + } + + if (!request.isAsyncStarted() && responseHandled.compareAndSet(false, true)) { + if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) { + for (String headerName : httpResponse.getHeaderNames()) { + String headerValue = httpResponse.getHeader(headerName); + currentSpan.setAttribute( + HypertraceSemanticAttributes.httpResponseHeader(headerName), headerValue); + } + } + + // capture response body + if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue() + && ContentTypeUtils.shouldCapture(httpResponse.getContentType())) { + Utils.captureResponseBody(currentSpan, outputStreamContext, writerContext, httpResponse); + } + } + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentationModule.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentationModule.java new file mode 100644 index 000000000..d67da8938 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentationModule.java @@ -0,0 +1,79 @@ +/* + * 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.v3_0.nowrapping; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed; + +import com.google.auto.service.AutoService; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.request.ServletInputStreamInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.request.ServletRequestInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.response.ServletOutputStreamInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.response.ServletResponseInstrumentation; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.bytebuddy.matcher.ElementMatcher; +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; + +@AutoService(InstrumentationModule.class) +public class Servlet31NoWrappingInstrumentationModule extends InstrumentationModule { + + public Servlet31NoWrappingInstrumentationModule() { + super(Servlet31InstrumentationName.PRIMARY, Servlet31InstrumentationName.OTHER); + } + + @Override + public int getOrder() { + return 1; + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("javax.servlet.http.HttpServlet"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new Servlet31NoWrappingInstrumentation(), + new ServletRequestInstrumentation(), + new ServletInputStreamInstrumentation(), + new ServletResponseInstrumentation(), + new ServletOutputStreamInstrumentation()); + } + + @Override + protected Map contextStore() { + Map context = new HashMap<>(); + // capture request body + context.put("javax.servlet.http.HttpServletRequest", Span.class.getName()); + context.put("javax.servlet.ServletInputStream", ByteBufferSpanPair.class.getName()); + context.put("java.io.BufferedReader", CharBufferSpanPair.class.getName()); + + // capture response body + context.put("javax.servlet.ServletOutputStream", BoundedByteArrayOutputStream.class.getName()); + context.put("java.io.PrintWriter", BoundedCharArrayWriter.class.getName()); + return context; + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java new file mode 100644 index 000000000..6b6e8be15 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java @@ -0,0 +1,98 @@ +/* + * 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.v3_0.nowrapping; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +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; + +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, + ContextStore streamContextStore, + ContextStore writerContextStore, + HttpServletResponse httpResponse) { + + try { + ServletOutputStream outputStream = httpResponse.getOutputStream(); + BoundedByteArrayOutputStream buffer = streamContextStore.get(outputStream); + if (buffer != null) { + span.setAttribute( + HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, buffer.toStringWithSuppliedCharset()); + streamContextStore.put(outputStream, null); + } + } catch (IllegalStateException | IOException exOutStream) { + // getWriter was called + try { + PrintWriter writer = httpResponse.getWriter(); + BoundedCharArrayWriter buffer = writerContextStore.get(writer); + if (buffer != null) { + span.setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, buffer.toString()); + writerContextStore.put(writer, null); + } + } catch (IllegalStateException | IOException exPrintWriter) { + } + } + } + + public static void resetRequestBodyBuffers( + ContextStore streamContextStore, + ContextStore printContextStore, + HttpServletRequest httpRequest) { + try { + ServletInputStream inputStream = httpRequest.getInputStream(); + ByteBufferSpanPair bufferSpanPair = streamContextStore.get(inputStream); + if (bufferSpanPair != null) { + streamContextStore.put(inputStream, null); + } + } catch (IllegalStateException | IOException exOutStream) { + // getWriter was called + try { + BufferedReader reader = httpRequest.getReader(); + CharBufferSpanPair bufferSpanPair = printContextStore.get(reader); + if (bufferSpanPair != null) { + printContextStore.put(reader, null); + } + } catch (IllegalStateException | IOException exPrintWriter) { + } + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamInstrumentation.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamInstrumentation.java new file mode 100644 index 000000000..afa0b6f6d --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamInstrumentation.java @@ -0,0 +1,259 @@ +/* + * 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.v3_0.nowrapping.request; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; +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.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletInputStream; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +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 safeHasSuperType(named("javax.servlet.ServletInputStream")); + } + + @Override + public Map, String> transformers() { + Map, String> transformers = new HashMap<>(); + transformers.put( + named("read").and(takesArguments(0)).and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadNoArgs"); + transformers.put( + named("read") + .and(takesArguments(1)) + .and(takesArgument(0, is(byte[].class))) + .and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadByteArray"); + transformers.put( + 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"); + transformers.put( + named("readAllBytes").and(takesArguments(0)).and(isPublic()), + ServletInputStreamInstrumentation.class.getName() + "$InputStream_ReadAllBytes"); + transformers.put( + 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 + transformers.put( + 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"); + return transformers; + } + + static class InputStream_ReadNoArgs { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + InstrumentationContext.get(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return int read, @Advice.Enter ByteBufferSpanPair bufferSpanPair) { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.buffer.write((byte) read); + } + } + } + + public static class InputStream_ReadByteArray { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + InstrumentationContext.get(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return int read, + @Advice.Argument(0) byte b[], + @Advice.Enter ByteBufferSpanPair bufferSpanPair) { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.buffer.write(b, 0, read); + } + } + } + + public static class InputStream_ReadByteArrayOffset { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + InstrumentationContext.get(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return int read, + @Advice.Argument(0) byte b[], + @Advice.Argument(1) int off, + @Advice.Argument(2) int len, + @Advice.Enter ByteBufferSpanPair bufferSpanPair) { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.buffer.write(b, off, read); + } + } + } + + public static class InputStream_ReadAllBytes { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + InstrumentationContext.get(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return byte[] b, @Advice.Enter ByteBufferSpanPair bufferSpanPair) + throws IOException { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + bufferSpanPair.buffer.write(b); + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } + } + + public static class InputStream_ReadNBytes { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static ByteBufferSpanPair enter(@Advice.This ServletInputStream thizz) { + ByteBufferSpanPair bufferSpanPair = + InstrumentationContext.get(ServletInputStream.class, ByteBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(ServletInputStream.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return int read, + @Advice.Argument(0) byte[] b, + @Advice.Argument(1) int off, + @Advice.Argument(2) int len, + @Advice.Enter ByteBufferSpanPair bufferSpanPair) { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletInputStream.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.buffer.write(b, off, read); + } + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletRequestInstrumentation.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletRequestInstrumentation.java new file mode 100644 index 000000000..7fd563039 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletRequestInstrumentation.java @@ -0,0 +1,164 @@ +/* + * 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.v3_0.nowrapping.request; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; +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.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.io.BufferedReader; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; + +public class ServletRequestInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(named("javax.servlet.ServletRequest")); + } + + @Override + public Map, String> transformers() { + Map, String> matchers = new HashMap<>(); + matchers.put( + named("getInputStream") + .and(takesArguments(0)) + .and(returns(named("javax.servlet.ServletInputStream"))) + .and(isPublic()), + ServletRequestInstrumentation.class.getName() + "$ServletRequest_getInputStream_advice"); + matchers.put( + named("getReader") + .and(takesArguments(0)) + // .and(returns(BufferedReader.class)) + .and(isPublic()), + ServletRequestInstrumentation.class.getName() + "$ServletRequest_getReader_advice"); + return matchers; + } + + static class ServletRequest_getInputStream_advice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Span enter(@Advice.This ServletRequest servletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + // span is added in servlet/filter instrumentation if data capture is enabled + Span requestSpan = + InstrumentationContext.get(HttpServletRequest.class, Span.class).get(httpServletRequest); + if (requestSpan == null) { + return null; + } + + // the getReader method might call getInputStream + CallDepthThreadLocalMap.incrementCallDepth(ServletRequest.class); + return requestSpan; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.This ServletRequest servletRequest, + @Advice.Return ServletInputStream servletInputStream, + @Advice.Enter Span requestSpan) { + + if (requestSpan == null) { + return; + } + + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletRequest.class); + if (callDepth > 0) { + return; + } + + if (!(servletRequest instanceof HttpServletRequest)) { + return; + } + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + ContextStore contextStore = + InstrumentationContext.get(ServletInputStream.class, ByteBufferSpanPair.class); + if (contextStore.get(servletInputStream) != null) { + // getInputStream() can be called multiple times + return; + } + + ByteBufferSpanPair bufferSpanPair = + Utils.createRequestByteBufferSpanPair(httpServletRequest, requestSpan); + contextStore.put(servletInputStream, bufferSpanPair); + } + } + + static class ServletRequest_getReader_advice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Span enter(@Advice.This ServletRequest servletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + Span requestSpan = + InstrumentationContext.get(HttpServletRequest.class, Span.class).get(httpServletRequest); + if (requestSpan == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(ServletRequest.class); + return requestSpan; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.This ServletRequest servletRequest, + @Advice.Return BufferedReader reader, + @Advice.Enter Span requestSpan) { + + if (requestSpan == null) { + return; + } + + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletRequest.class); + if (callDepth > 0) { + return; + } + + if (!(servletRequest instanceof HttpServletRequest)) { + return; + } + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + ContextStore contextStore = + InstrumentationContext.get(BufferedReader.class, CharBufferSpanPair.class); + if (contextStore.get(reader) != null) { + // getReader() can be called multiple times + return; + } + + CharBufferSpanPair bufferSpanPair = + Utils.createRequestCharBufferSpanPair(httpServletRequest, requestSpan); + contextStore.put(reader, bufferSpanPair); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/Utils.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/Utils.java new file mode 100644 index 000000000..357ecf107 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/Utils.java @@ -0,0 +1,51 @@ +/* + * 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.v3_0.nowrapping.request; + +import io.opentelemetry.api.trace.Span; +import java.nio.charset.Charset; +import javax.servlet.http.HttpServletRequest; +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.utils.ContentLengthUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeCharsetUtils; + +public class Utils { + + private Utils() {} + + public static ByteBufferSpanPair createRequestByteBufferSpanPair( + HttpServletRequest httpServletRequest, Span span) { + 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)); + } + + public static CharBufferSpanPair createRequestCharBufferSpanPair( + HttpServletRequest httpServletRequest, Span span) { + int contentLength = httpServletRequest.getContentLength(); + if (contentLength < 0) { + contentLength = ContentLengthUtils.DEFAULT; + } + return new CharBufferSpanPair(span, BoundedBuffersFactory.createWriter(contentLength)); + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamInstrumentation.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamInstrumentation.java new file mode 100644 index 000000000..e133e3eaa --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamInstrumentation.java @@ -0,0 +1,199 @@ +/* + * 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.v3_0.nowrapping.response; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; +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.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletOutputStream; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; + +public class ServletOutputStreamInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(named("javax.servlet.ServletOutputStream")); + } + + @Override + public Map, String> transformers() { + Map, String> transformers = new HashMap<>(); + transformers.put( + 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 + transformers.put( + named("write").and(takesArguments(1)).and(takesArgument(0, is(int.class))).and(isPublic()), + ServletOutputStreamInstrumentation.class.getName() + "$OutputStream_write"); + transformers.put( + named("write") + .and(takesArguments(1)) + .and(takesArgument(0, is(byte[].class))) + .and(isPublic()), + ServletOutputStreamInstrumentation.class.getName() + "$OutputStream_writeByteArr"); + transformers.put( + 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) + // transformers.put( + // named("close").and(takesArguments(0)) + // .and(isPublic()), + // ServletOutputStreamInstrumentation.class.getName() + "$OutputStream_close"); + return transformers; + } + + static class OutputStream_write { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedByteArrayOutputStream enter( + @Advice.This ServletOutputStream thizz, @Advice.Argument(0) int b) { + BoundedByteArrayOutputStream buffer = + InstrumentationContext.get(ServletOutputStream.class, BoundedByteArrayOutputStream.class) + .get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.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) { + CallDepthThreadLocalMap.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 = + InstrumentationContext.get(ServletOutputStream.class, BoundedByteArrayOutputStream.class) + .get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.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) { + CallDepthThreadLocalMap.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 = + InstrumentationContext.get(ServletOutputStream.class, BoundedByteArrayOutputStream.class) + .get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.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) { + CallDepthThreadLocalMap.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 = + InstrumentationContext.get(ServletOutputStream.class, BoundedByteArrayOutputStream.class) + .get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.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) { + CallDepthThreadLocalMap.decrementCallDepth(ServletOutputStream.class); + } + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletResponseInstrumentation.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletResponseInstrumentation.java new file mode 100644 index 000000000..701c1bd31 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletResponseInstrumentation.java @@ -0,0 +1,176 @@ +/* + * 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.v3_0.nowrapping.response; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; +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.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.config.HypertraceConfig; +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 safeHasSuperType(named("javax.servlet.ServletResponse")); + } + + @Override + public Map, String> transformers() { + Map, String> matchers = new HashMap<>(); + matchers.put( + named("getOutputStream") + .and(takesArguments(0)) + .and(returns(named("javax.servlet.ServletOutputStream"))) + .and(isPublic()), + ServletResponseInstrumentation.class.getName() + "$ServletResponse_getOutputStream"); + matchers.put( + named("getWriter").and(takesArguments(0)).and(returns(PrintWriter.class)).and(isPublic()), + ServletResponseInstrumentation.class.getName() + "$ServletResponse_getWriter_advice"); + return matchers; + } + + 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 + CallDepthThreadLocalMap.incrementCallDepth(ServletResponse.class); + return httpServletResponse; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Enter HttpServletResponse httpServletResponse, + @Advice.Return ServletOutputStream servletOutputStream) { + + if (httpServletResponse == null) { + return; + } + + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletResponse.class); + if (callDepth > 0) { + return; + } + + ContextStore contextStore = + InstrumentationContext.get(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 + AgentConfig agentConfig = HypertraceConfig.get(); + String contentType = httpServletResponse.getContentType(); + if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue() + && ContentTypeUtils.shouldCapture(contentType)) { + + String charsetStr = httpServletResponse.getCharacterEncoding(); + Charset charset = ContentTypeCharsetUtils.toCharset(charsetStr); + BoundedByteArrayOutputStream buffer = BoundedBuffersFactory.createStream(charset); + contextStore.put(servletOutputStream, buffer); + // override the metadata that is used by the OutputStream instrumentation + } + } + } + + 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 + CallDepthThreadLocalMap.incrementCallDepth(ServletResponse.class); + return httpServletResponse; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Enter HttpServletResponse httpServletResponse, + @Advice.Return PrintWriter printWriter) { + + if (httpServletResponse == null) { + return; + } + + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(ServletResponse.class); + if (callDepth > 0) { + return; + } + + ContextStore contextStore = + InstrumentationContext.get(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 + AgentConfig agentConfig = HypertraceConfig.get(); + String contentType = httpServletResponse.getContentType(); + if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue() + && ContentTypeUtils.shouldCapture(contentType)) { + + BoundedCharArrayWriter writer = BoundedBuffersFactory.createWriter(); + contextStore.put(printWriter, writer); + } + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet30NoWrappingInstrumentationTest.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet30NoWrappingInstrumentationTest.java new file mode 100644 index 000000000..aecab2970 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet30NoWrappingInstrumentationTest.java @@ -0,0 +1,275 @@ +/* + * 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.v3_0.nowrapping; + +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.TestServlets.EchoStream_arr; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.TestServlets.EchoStream_arr_offset; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.TestServlets.EchoStream_readLine_print; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.TestServlets.EchoStream_single_byte; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.TestServlets.EchoWriter_single_char; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.TestServlets.GetHello; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.EnumSet; +import java.util.List; +import javax.servlet.DispatcherType; +import okhttp3.FormBody; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +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 Servlet30NoWrappingInstrumentationTest 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; + + @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.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_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(TestServlets.EchoAsyncResponse.class, "/echo_async_response"); + server.setHandler(handler); + server.start(); + serverPort = 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() throws Exception { + postJson(String.format("http://localhost:%d/echo_async_response", 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_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_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.getTraces(); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + Assertions.assertEquals( + REQUEST_HEADER_VALUE, + spanData + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER))); + Assertions.assertEquals( + TestServlets.RESPONSE_HEADER_VALUE, + spanData + .getAttributes() + .get(HypertraceSemanticAttributes.httpResponseHeader(TestServlets.RESPONSE_HEADER))); + Assertions.assertEquals( + "key1=value1&key2=value2", + spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertEquals( + TestServlets.RESPONSE_BODY, + spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } + + @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.getTraces(); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + Assertions.assertEquals( + REQUEST_HEADER_VALUE, + spanData + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER))); + Assertions.assertEquals( + TestServlets.RESPONSE_HEADER_VALUE, + spanData + .getAttributes() + .get(HypertraceSemanticAttributes.httpResponseHeader(TestServlets.RESPONSE_HEADER))); + Assertions.assertNull( + spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + spanData.getAttributes().get(HypertraceSemanticAttributes.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()); + } + + TEST_WRITER.waitForTraces(1); + List> traces = TEST_WRITER.getTraces(); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + Assertions.assertNull( + spanData + .getAttributes() + .get(HypertraceSemanticAttributes.httpResponseHeader(TestServlets.RESPONSE_HEADER))); + Assertions.assertNull( + spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + spanData.getAttributes().get(HypertraceSemanticAttributes.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.getTraces(); + Assertions.assertEquals(1, traces.size()); + List spans = traces.get(0); + Assertions.assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + Assertions.assertEquals( + REQUEST_HEADER_VALUE, + spanData + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER))); + Assertions.assertEquals( + TestServlets.RESPONSE_HEADER_VALUE, + spanData + .getAttributes() + .get(HypertraceSemanticAttributes.httpResponseHeader(TestServlets.RESPONSE_HEADER))); + Assertions.assertEquals( + REQUEST_BODY, spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertEquals( + TestServlets.RESPONSE_BODY, + spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/TestServlets.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/TestServlets.java new file mode 100644 index 000000000..279c6533d --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/TestServlets.java @@ -0,0 +1,211 @@ +/* + * 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.v3_0.nowrapping; + +import java.io.IOException; +import javax.servlet.AsyncContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +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_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 extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + while (req.getReader().readLine() != null) {} + + AsyncContext asyncContext = req.startAsync(); + asyncContext.start( + () -> { + 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.toCharArray()); + } 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); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/WrappingFilter.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/WrappingFilter.java new file mode 100644 index 000000000..58f00cff6 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/WrappingFilter.java @@ -0,0 +1,110 @@ +/* + * 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.v3_0.nowrapping; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import org.DelegatingBufferedReader; +import org.DelegatingPrintWriter; +import org.DelegatingServletInputStream; +import org.DelegatingServletOutputStream; + +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/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamContextAccessInstrumentationModule.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamContextAccessInstrumentationModule.java new file mode 100644 index 000000000..3996c4a79 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamContextAccessInstrumentationModule.java @@ -0,0 +1,85 @@ +/* + * 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.v3_0.nowrapping.request; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.servlet.ServletInputStream; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; + +// SPI explicitly added in META-INF/services/... +public class ServletInputStreamContextAccessInstrumentationModule extends InstrumentationModule { + + public ServletInputStreamContextAccessInstrumentationModule() { + super("test-servlet-input-stream"); + } + + @Override + protected Map contextStore() { + Map context = new HashMap<>(); + context.put("javax.servlet.ServletInputStream", ByteBufferSpanPair.class.getName()); + return context; + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new InputStreamTriggerInstrumentation()); + } + + class InputStreamTriggerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.ServletStreamContextAccess"); + } + + @Override + public Map, String> transformers() { + Map, String> matchers = new HashMap<>(); + matchers.put( + named("addToInputStreamContext").and(takesArguments(2)).and(isPublic()), + ServletInputStreamContextAccessInstrumentationModule.class.getName() + "$TestAdvice"); + return matchers; + } + } + + static class TestAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter( + @Advice.Argument(0) ServletInputStream servletInputStream, + @Advice.Argument(1) ByteBufferSpanPair metadata) { + ContextStore contextStore = + InstrumentationContext.get(ServletInputStream.class, ByteBufferSpanPair.class); + contextStore.put(servletInputStream, metadata); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamInstrumentationTest.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamInstrumentationTest.java new file mode 100644 index 000000000..07097715b --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/request/ServletInputStreamInstrumentationTest.java @@ -0,0 +1,87 @@ +/* + * 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.v3_0.nowrapping.request; + +import io.opentelemetry.api.trace.Span; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import javax.servlet.ServletInputStream; +import org.ServletStreamContextAccess; +import org.TestServletInputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair; +import org.hypertrace.agent.testing.AbstractInstrumenterTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ServletInputStreamInstrumentationTest extends AbstractInstrumenterTest { + + private static final String TEST_SPAN_NAME = "foo"; + private static final String BODY = "boobar"; + + @Test + public void read() throws IOException { + Span span = TEST_TRACER.spanBuilder(TEST_SPAN_NAME).startSpan(); + + ServletInputStream servletInputStream = + new TestServletInputStream(new ByteArrayInputStream(BODY.getBytes())); + + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.UTF_8); + ByteBufferSpanPair bufferSpanPair = new ByteBufferSpanPair(span, buffer); + ServletStreamContextAccess.addToInputStreamContext(servletInputStream, bufferSpanPair); + + while (servletInputStream.read() != -1) {} + Assertions.assertEquals(BODY, buffer.toStringWithSuppliedCharset()); + } + + @Test + public void read_callDepth_is_cleared() throws IOException { + Span span = TEST_TRACER.spanBuilder(TEST_SPAN_NAME).startSpan(); + + ServletInputStream servletInputStream = + new TestServletInputStream(new ByteArrayInputStream(BODY.getBytes())); + servletInputStream.read(); + + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.UTF_8); + ByteBufferSpanPair bufferSpanPair = new ByteBufferSpanPair(span, buffer); + ServletStreamContextAccess.addToInputStreamContext(servletInputStream, bufferSpanPair); + + while (servletInputStream.read() != -1) {} + Assertions.assertEquals(BODY.substring(1), buffer.toStringWithSuppliedCharset()); + } + + @Test + public void read_call_depth_read_calls_read() throws IOException { + Span span = TEST_TRACER.spanBuilder(TEST_SPAN_NAME).startSpan(); + + ServletInputStream servletInputStream = + new TestServletInputStream(new ByteArrayInputStream(BODY.getBytes())); + servletInputStream.read(new byte[2]); + + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.UTF_8); + ByteBufferSpanPair bufferSpanPair = new ByteBufferSpanPair(span, buffer); + ServletStreamContextAccess.addToInputStreamContext(servletInputStream, bufferSpanPair); + + servletInputStream.read(new byte[BODY.length()]); + Assertions.assertEquals(BODY.substring(2), buffer.toStringWithSuppliedCharset()); + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamContextAccessInstrumentationModule.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamContextAccessInstrumentationModule.java new file mode 100644 index 000000000..696ad1e08 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamContextAccessInstrumentationModule.java @@ -0,0 +1,85 @@ +/* + * 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.v3_0.nowrapping.response; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.servlet.ServletOutputStream; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; + +public class ServletOutputStreamContextAccessInstrumentationModule extends InstrumentationModule { + + public ServletOutputStreamContextAccessInstrumentationModule() { + super("test-servlet-output-stream"); + } + + @Override + protected Map contextStore() { + Map context = new HashMap<>(); + context.put("javax.servlet.ServletOutputStream", BoundedByteArrayOutputStream.class.getName()); + return context; + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new OutputStreamTriggerInstrumentation()); + } + + class OutputStreamTriggerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.ServletStreamContextAccess"); + } + + @Override + public Map, String> transformers() { + Map, String> matchers = new HashMap<>(); + matchers.put( + named("addToOutputStreamContext").and(takesArguments(2)).and(isPublic()), + ServletOutputStreamContextAccessInstrumentationModule.class.getName() + "$TestAdvice"); + return matchers; + } + } + + static class TestAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter( + @Advice.Argument(0) ServletOutputStream servletOutputStream, + @Advice.Argument(1) BoundedByteArrayOutputStream buffer) { + System.out.println("adding to context"); + ContextStore contextStore = + InstrumentationContext.get(ServletOutputStream.class, BoundedByteArrayOutputStream.class); + contextStore.put(servletOutputStream, buffer); + } + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamInstrumentationTest.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamInstrumentationTest.java new file mode 100644 index 000000000..d4b2be599 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/response/ServletOutputStreamInstrumentationTest.java @@ -0,0 +1,93 @@ +/* + * 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.v3_0.nowrapping.response; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import javax.servlet.ServletOutputStream; +import org.ServletStreamContextAccess; +import org.TestServletOutputStream; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.testing.AbstractInstrumenterTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ServletOutputStreamInstrumentationTest extends AbstractInstrumenterTest { + + private static final String BODY = "boobar"; + + @Test + public void write_single_byte() throws IOException { + ServletOutputStream servletOutputStream = new TestServletOutputStream(); + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.UTF_8); + ServletStreamContextAccess.addToOutputStreamContext(servletOutputStream, buffer); + + byte[] bytes = BODY.getBytes(); + for (int i = 0; i < BODY.length(); i++) { + servletOutputStream.write(bytes[i]); + } + Assertions.assertEquals(BODY, buffer.toStringWithSuppliedCharset()); + } + + @Test + public void write_arr() throws IOException { + ServletOutputStream servletOutputStream = new TestServletOutputStream(); + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.UTF_8); + ServletStreamContextAccess.addToOutputStreamContext(servletOutputStream, buffer); + + servletOutputStream.write(BODY.getBytes()); + Assertions.assertEquals(BODY, buffer.toStringWithSuppliedCharset()); + } + + @Test + public void write_arr_offset() throws IOException { + ServletOutputStream servletOutputStream = new TestServletOutputStream(); + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.UTF_8); + ServletStreamContextAccess.addToOutputStreamContext(servletOutputStream, buffer); + + byte[] bytes = BODY.getBytes(); + servletOutputStream.write(bytes, 0, 2); + servletOutputStream.write(bytes, 2, bytes.length - 2); + Assertions.assertEquals(BODY, buffer.toStringWithSuppliedCharset()); + } + + @Test + public void print_str() throws IOException { + ServletOutputStream servletOutputStream = new TestServletOutputStream(); + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.UTF_8); + ServletStreamContextAccess.addToOutputStreamContext(servletOutputStream, buffer); + + servletOutputStream.print(BODY); + Assertions.assertEquals(BODY, buffer.toStringWithSuppliedCharset()); + } + + @Test + public void println_str() throws IOException { + ServletOutputStream servletOutputStream = new TestServletOutputStream(); + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.UTF_8); + ServletStreamContextAccess.addToOutputStreamContext(servletOutputStream, buffer); + + servletOutputStream.println(BODY); + Assertions.assertEquals(BODY + "\r\n", buffer.toStringWithSuppliedCharset()); + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/DelegatingBufferedReader.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/DelegatingBufferedReader.java new file mode 100644 index 000000000..ebc67937f --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/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-3.0-no-wrapping/src/test/java/org/DelegatingPrintWriter.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/DelegatingPrintWriter.java new file mode 100644 index 000000000..6522c50b6 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/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-3.0-no-wrapping/src/test/java/org/DelegatingServletInputStream.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/DelegatingServletInputStream.java new file mode 100644 index 000000000..c348860f7 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/DelegatingServletInputStream.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 java.io.IOException; +import javax.servlet.ServletInputStream; + +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); + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/DelegatingServletOutputStream.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/DelegatingServletOutputStream.java new file mode 100644 index 000000000..1a9d17039 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/DelegatingServletOutputStream.java @@ -0,0 +1,54 @@ +/* + * 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 javax.servlet.ServletOutputStream; + +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(); + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/ServletStreamContextAccess.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/ServletStreamContextAccess.java new file mode 100644 index 000000000..58c532e43 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/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 javax.servlet.ServletInputStream; +import javax.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-3.0-no-wrapping/src/test/java/org/TestServletInputStream.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/TestServletInputStream.java new file mode 100644 index 000000000..650994b14 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/TestServletInputStream.java @@ -0,0 +1,35 @@ +/* + * 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.InputStream; +import javax.servlet.ServletInputStream; + +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(); + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/TestServletOutputStream.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/TestServletOutputStream.java new file mode 100644 index 000000000..813643bd0 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/org/TestServletOutputStream.java @@ -0,0 +1,28 @@ +/* + * 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 javax.servlet.ServletOutputStream; + +public class TestServletOutputStream extends ServletOutputStream { + + @Override + public void write(int b) throws IOException { + // noop + } +} diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/resources/META-INF/services/io.opentelemetry.javaagent.tooling.InstrumentationModule b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/resources/META-INF/services/io.opentelemetry.javaagent.tooling.InstrumentationModule new file mode 100644 index 000000000..d764364bb --- /dev/null +++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/resources/META-INF/services/io.opentelemetry.javaagent.tooling.InstrumentationModule @@ -0,0 +1,2 @@ +io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.request.ServletInputStreamContextAccessInstrumentationModule +io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.response.ServletOutputStreamContextAccessInstrumentationModule diff --git a/instrumentation/servlet/servlet-3.0/build.gradle.kts b/instrumentation/servlet/servlet-3.0/build.gradle.kts index ecdef1780..2ab49b4ee 100644 --- a/instrumentation/servlet/servlet-3.0/build.gradle.kts +++ b/instrumentation/servlet/servlet-3.0/build.gradle.kts @@ -42,3 +42,7 @@ dependencies { testImplementation("org.eclipse.jetty:jetty-servlet:8.1.22.v20160922") } + +tasks.withType { + jvmArgs = mutableListOf("-Dotel.instrumentation.servlet.enabled=true") +} diff --git a/instrumentation/servlet/servlet-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/Servlet30BodyInstrumentationModule.java b/instrumentation/servlet/servlet-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/Servlet30BodyInstrumentationModule.java index 2ad9b4b4b..1b6885f77 100644 --- a/instrumentation/servlet/servlet-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/Servlet30BodyInstrumentationModule.java +++ b/instrumentation/servlet/servlet-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/Servlet30BodyInstrumentationModule.java @@ -58,6 +58,11 @@ public Servlet30BodyInstrumentationModule() { super(Servlet30InstrumentationName.PRIMARY, Servlet30InstrumentationName.OTHER); } + @Override + protected boolean defaultEnabled() { + return false; + } + @Override public int getOrder() { /** diff --git a/instrumentation/servlet/servlet-3.1/build.gradle.kts b/instrumentation/servlet/servlet-3.1/build.gradle.kts index 6bc7c818a..64db46fe6 100644 --- a/instrumentation/servlet/servlet-3.1/build.gradle.kts +++ b/instrumentation/servlet/servlet-3.1/build.gradle.kts @@ -42,3 +42,7 @@ dependencies { testImplementation("org.eclipse.jetty:jetty-server:9.4.32.v20200930") testImplementation("org.eclipse.jetty:jetty-servlet:9.4.32.v20200930") } + +tasks.withType { + jvmArgs = mutableListOf("-Dotel.instrumentation.servlet.enabled=true") +} diff --git a/instrumentation/servlet/servlet-3.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_1/Servlet31BodyInstrumentationModule.java b/instrumentation/servlet/servlet-3.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_1/Servlet31BodyInstrumentationModule.java index 095887cfb..7731fe5b8 100644 --- a/instrumentation/servlet/servlet-3.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_1/Servlet31BodyInstrumentationModule.java +++ b/instrumentation/servlet/servlet-3.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_1/Servlet31BodyInstrumentationModule.java @@ -50,6 +50,11 @@ public Servlet31BodyInstrumentationModule( super(mainInstrumentationName, otherInstrumentationNames); } + @Override + protected boolean defaultEnabled() { + return false; + } + @Override public int getOrder() { /** diff --git a/instrumentation/servlet/servlet-rw/build.gradle.kts b/instrumentation/servlet/servlet-rw/build.gradle.kts new file mode 100644 index 000000000..18d9b5507 --- /dev/null +++ b/instrumentation/servlet/servlet-rw/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + `java-library` + id("net.bytebuddy.byte-buddy") + id("io.opentelemetry.instrumentation.auto-instrumentation") + muzzle +} + +muzzle { + pass { + coreJdk() + } +} + +afterEvaluate{ + io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project, + sourceSets.main.get(), + "io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin", + project(":javaagent-tooling").configurations["instrumentationMuzzle"] + configurations.runtimeClasspath + ).configure() +} + +dependencies { + testImplementation(project(":testing-common")) +} diff --git a/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/reader/BufferedReaderInstrumentation.java b/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/reader/BufferedReaderInstrumentation.java new file mode 100644 index 000000000..fb4712ae0 --- /dev/null +++ b/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/reader/BufferedReaderInstrumentation.java @@ -0,0 +1,210 @@ +/* + * 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.rw.reader; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; +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.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; + +public class BufferedReaderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(named("java.io.BufferedReader")).or(named("java.io.BufferedReader")); + } + + @Override + public Map, String> transformers() { + Map, String> transformers = new HashMap<>(); + transformers.put( + named("read").and(takesArguments(0)).and(isPublic()), + BufferedReaderInstrumentation.class.getName() + "$Reader_readNoArgs"); + transformers.put( + named("read") + .and(takesArguments(1)) + .and(takesArgument(0, is(char[].class))) + .and(isPublic()), + BufferedReaderInstrumentation.class.getName() + "$Reader_readCharArray"); + transformers.put( + named("read") + .and(takesArguments(3)) + .and(takesArgument(0, is(char[].class))) + .and(takesArgument(1, is(int.class))) + .and(takesArgument(2, is(int.class))) + .and(isPublic()), + BufferedReaderInstrumentation.class.getName() + "$Reader_readByteArrayOffset"); + transformers.put( + named("readLine").and(takesArguments(0)).and(isPublic()), + BufferedReaderInstrumentation.class.getName() + "$BufferedReader_readLine"); + return transformers; + } + + static class Reader_readNoArgs { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static CharBufferSpanPair enter(@Advice.This BufferedReader thizz) { + CharBufferSpanPair bufferSpanPair = + InstrumentationContext.get(BufferedReader.class, CharBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(BufferedReader.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.This BufferedReader thizz, + @Advice.Return int read, + @Advice.Enter CharBufferSpanPair bufferSpanPair) { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(BufferedReader.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.buffer.write(read); + } + } + } + + static class Reader_readCharArray { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static CharBufferSpanPair enter(@Advice.This BufferedReader thizz) { + CharBufferSpanPair bufferSpanPair = + InstrumentationContext.get(BufferedReader.class, CharBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(BufferedReader.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return int read, + @Advice.Argument(0) char c[], + @Advice.Enter CharBufferSpanPair bufferSpanPair) { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(BufferedReader.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.buffer.write(c, 0, read); + } + } + } + + static class Reader_readByteArrayOffset { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static CharBufferSpanPair enter(@Advice.This BufferedReader thizz) { + CharBufferSpanPair bufferSpanPair = + InstrumentationContext.get(BufferedReader.class, CharBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(BufferedReader.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return int read, + @Advice.Argument(0) char c[], + @Advice.Argument(1) int off, + @Advice.Argument(2) int len, + @Advice.Enter CharBufferSpanPair bufferSpanPair) { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(BufferedReader.class); + if (callDepth > 0) { + return; + } + + if (read == -1) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.buffer.write(c, off, read); + } + } + } + + static class BufferedReader_readLine { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static CharBufferSpanPair enter(@Advice.This BufferedReader thizz) { + CharBufferSpanPair bufferSpanPair = + InstrumentationContext.get(BufferedReader.class, CharBufferSpanPair.class).get(thizz); + if (bufferSpanPair == null) { + return null; + } + + CallDepthThreadLocalMap.incrementCallDepth(BufferedReader.class); + return bufferSpanPair; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return String line, @Advice.Enter CharBufferSpanPair bufferSpanPair) + throws IOException { + if (bufferSpanPair == null) { + return; + } + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(BufferedReader.class); + if (callDepth > 0) { + return; + } + + if (line == null) { + bufferSpanPair.captureBody(HypertraceSemanticAttributes.HTTP_REQUEST_BODY); + } else { + bufferSpanPair.buffer.write(line); + } + } + } +} diff --git a/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/reader/BufferedReaderInstrumentationModule.java b/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/reader/BufferedReaderInstrumentationModule.java new file mode 100644 index 000000000..aed4e7930 --- /dev/null +++ b/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/reader/BufferedReaderInstrumentationModule.java @@ -0,0 +1,48 @@ +/* + * 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.rw.reader; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair; + +/** + * Instrumentation module for {@link java.io.BufferedReader}. It must be be defined in a separate + * module because servlet non-wrapping instrumentation runs only on classloaders that have servlet + * classes and the reader is in the bootstrap classloader. + */ +@AutoService(InstrumentationModule.class) +public class BufferedReaderInstrumentationModule extends InstrumentationModule { + + public BufferedReaderInstrumentationModule() { + super("bufferedreader", "servlet", "servlet-3", "ht", "servlet-no-wrapping"); + } + + @Override + protected Map contextStore() { + return Collections.singletonMap("java.io.BufferedReader", CharBufferSpanPair.class.getName()); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new BufferedReaderInstrumentation()); + } +} diff --git a/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/writer/PrintWriterInstrumentation.java b/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/writer/PrintWriterInstrumentation.java new file mode 100644 index 000000000..3cba35d11 --- /dev/null +++ b/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/writer/PrintWriterInstrumentation.java @@ -0,0 +1,290 @@ +/* + * 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.rw.writer; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType; +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.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatcher.Junction; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; + +public class PrintWriterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(named("java.io.PrintWriter")).or(named("java.io.PrintWriter")); + } + + @Override + public Map, String> transformers() { + Map, String> transformers = new HashMap<>(); + transformers.put( + named("write").and(takesArguments(1)).and(takesArgument(0, is(int.class))).and(isPublic()), + PrintWriterInstrumentation.class.getName() + "$Writer_writeChar"); + transformers.put( + named("write") + .and(takesArguments(1)) + .and(takesArgument(0, is(char[].class))) + .and(isPublic()), + PrintWriterInstrumentation.class.getName() + "$Writer_writeArr"); + transformers.put( + named("write") + .and(takesArguments(3)) + .and(takesArgument(0, is(char[].class))) + .and(takesArgument(1, is(int.class))) + .and(takesArgument(2, is(int.class))) + .and(isPublic()), + PrintWriterInstrumentation.class.getName() + "$Writer_writeOffset"); + transformers.put( + named("write") + .and(takesArguments(1)) + .and(takesArgument(0, is(String.class))) + .and(isPublic()), + PrintWriterInstrumentation.class.getName() + "$PrintWriter_print"); + transformers.put( + named("write") + .and(takesArguments(3)) + .and(takesArgument(0, is(String.class))) + .and(takesArgument(1, is(int.class))) + .and(takesArgument(2, is(int.class))) + .and(isPublic()), + PrintWriterInstrumentation.class.getName() + "$Writer_writeOffset_str"); + transformers.put( + named("print") + .and(takesArguments(1)) + .and(takesArgument(0, is(String.class))) + .and(isPublic()), + PrintWriterInstrumentation.class.getName() + "$PrintWriter_print"); + transformers.put( + named("println").and(takesArguments(0)).and(isPublic()), + PrintWriterInstrumentation.class.getName() + "$PrintWriter_println"); + transformers.put( + named("println") + .and(takesArguments(1)) + .and(takesArgument(0, is(String.class))) + .and(isPublic()), + PrintWriterInstrumentation.class.getName() + "$PrintWriter_printlnStr"); + return transformers; + } + + static class Writer_writeChar { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedCharArrayWriter enter( + @Advice.This PrintWriter thizz, @Advice.Argument(0) int ch) { + BoundedCharArrayWriter buffer = + InstrumentationContext.get(PrintWriter.class, BoundedCharArrayWriter.class).get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(PrintWriter.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(ch); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedCharArrayWriter buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(PrintWriter.class); + } + } + } + + static class Writer_writeArr { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedCharArrayWriter enter( + @Advice.This PrintWriter thizz, @Advice.Argument(0) char[] buf) throws IOException { + + BoundedCharArrayWriter buffer = + InstrumentationContext.get(PrintWriter.class, BoundedCharArrayWriter.class).get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(PrintWriter.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(buf); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedCharArrayWriter buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(PrintWriter.class); + } + } + } + + static class Writer_writeOffset { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedCharArrayWriter enter( + @Advice.This PrintWriter thizz, + @Advice.Argument(0) char[] buf, + @Advice.Argument(1) int offset, + @Advice.Argument(2) int len) { + + BoundedCharArrayWriter buffer = + InstrumentationContext.get(PrintWriter.class, BoundedCharArrayWriter.class).get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(PrintWriter.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(buf, offset, len); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedCharArrayWriter buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(PrintWriter.class); + } + } + } + + static class Writer_writeOffset_str { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedCharArrayWriter enter( + @Advice.This PrintWriter thizz, + @Advice.Argument(0) String str, + @Advice.Argument(1) int offset, + @Advice.Argument(2) int len) { + + BoundedCharArrayWriter buffer = + InstrumentationContext.get(PrintWriter.class, BoundedCharArrayWriter.class).get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(PrintWriter.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(str, offset, len); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedCharArrayWriter buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(PrintWriter.class); + } + } + } + + static class PrintWriter_print { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedCharArrayWriter enter( + @Advice.This PrintWriter thizz, @Advice.Argument(0) String str) throws IOException { + + BoundedCharArrayWriter buffer = + InstrumentationContext.get(PrintWriter.class, BoundedCharArrayWriter.class).get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(PrintWriter.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(str); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedCharArrayWriter buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(PrintWriter.class); + } + } + } + + static class PrintWriter_println { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedCharArrayWriter enter(@Advice.This PrintWriter thizz) { + BoundedCharArrayWriter buffer = + InstrumentationContext.get(PrintWriter.class, BoundedCharArrayWriter.class).get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(PrintWriter.class); + if (callDepth > 0) { + return buffer; + } + + buffer.append('\n'); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedCharArrayWriter buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(PrintWriter.class); + } + } + } + + static class PrintWriter_printlnStr { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static BoundedCharArrayWriter enter( + @Advice.This PrintWriter thizz, @Advice.Argument(0) String str) throws IOException { + BoundedCharArrayWriter buffer = + InstrumentationContext.get(PrintWriter.class, BoundedCharArrayWriter.class).get(thizz); + if (buffer == null) { + return null; + } + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(PrintWriter.class); + if (callDepth > 0) { + return buffer; + } + + buffer.write(str); + buffer.append('\n'); + return buffer; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Enter BoundedCharArrayWriter buffer) { + if (buffer != null) { + CallDepthThreadLocalMap.decrementCallDepth(PrintWriter.class); + } + } + } +} diff --git a/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/writer/PrintWriterInstrumentationModule.java b/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/writer/PrintWriterInstrumentationModule.java new file mode 100644 index 000000000..4e7ea8ae1 --- /dev/null +++ b/instrumentation/servlet/servlet-rw/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/rw/writer/PrintWriterInstrumentationModule.java @@ -0,0 +1,43 @@ +/* + * 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.rw.writer; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; + +@AutoService(InstrumentationModule.class) +public class PrintWriterInstrumentationModule extends InstrumentationModule { + + public PrintWriterInstrumentationModule() { + super("printwriter", "servlet", "servlet-3", "ht", "servlet-no-wrapping"); + } + + @Override + protected Map contextStore() { + return Collections.singletonMap("java.io.PrintWriter", BoundedCharArrayWriter.class.getName()); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new PrintWriterInstrumentation()); + } +} diff --git a/instrumentation/spark-2.3/build.gradle.kts b/instrumentation/spark-2.3/build.gradle.kts index d9f3c309e..135ef15e4 100644 --- a/instrumentation/spark-2.3/build.gradle.kts +++ b/instrumentation/spark-2.3/build.gradle.kts @@ -25,7 +25,7 @@ afterEvaluate{ val versions: Map by extra dependencies { - api(project(":instrumentation:servlet:servlet-3.1")) + api(project(":instrumentation:servlet:servlet-3.0-no-wrapping")) api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-spark-2.3:${versions["opentelemetry_java_agent"]}") api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-3.0:${versions["opentelemetry_java_agent"]}") @@ -33,6 +33,7 @@ dependencies { compileOnly("com.sparkjava:spark-core:2.3") + testImplementation(project(":instrumentation:servlet:servlet-rw")) testImplementation(project(":testing-common")) testImplementation("com.sparkjava:spark-core:2.3") } diff --git a/instrumentation/spark-2.3/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/sparkjava/SparkJavaBodyInstrumentationModule.java b/instrumentation/spark-2.3/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/sparkjava/SparkJavaBodyInstrumentationModule.java index 84beb6445..41f0828fb 100644 --- a/instrumentation/spark-2.3/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/sparkjava/SparkJavaBodyInstrumentationModule.java +++ b/instrumentation/spark-2.3/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/sparkjava/SparkJavaBodyInstrumentationModule.java @@ -23,12 +23,15 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import com.google.auto.service.AutoService; -import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_1.Servlet31Advice; -import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_1.Servlet31BodyInstrumentationModule; -import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_1.Servlet31InstrumentationName; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.Servlet31NoWrappingInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.Servlet31NoWrappingInstrumentationModule; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.request.ServletInputStreamInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.request.ServletRequestInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.response.ServletOutputStreamInstrumentation; +import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v3_0.nowrapping.response.ServletResponseInstrumentation; import io.opentelemetry.javaagent.tooling.InstrumentationModule; import io.opentelemetry.javaagent.tooling.TypeInstrumentation; -import java.util.Collections; +import java.util.Arrays; import java.util.List; import java.util.Map; import net.bytebuddy.description.method.MethodDescription; @@ -41,11 +44,7 @@ * might be fine as on exception there is usually not body send to users. */ @AutoService(InstrumentationModule.class) -public class SparkJavaBodyInstrumentationModule extends Servlet31BodyInstrumentationModule { - - public SparkJavaBodyInstrumentationModule() { - super(Servlet31InstrumentationName.PRIMARY, Servlet31InstrumentationName.OTHER); - } +public class SparkJavaBodyInstrumentationModule extends Servlet31NoWrappingInstrumentationModule { @Override public int getOrder() { @@ -54,7 +53,13 @@ public int getOrder() { @Override public List typeInstrumentations() { - return Collections.singletonList(new SparkJavaBodyInstrumentation()); + return Arrays.asList( + new SparkJavaBodyInstrumentation(), + new Servlet31NoWrappingInstrumentation(), + new ServletRequestInstrumentation(), + new ServletInputStreamInstrumentation(), + new ServletResponseInstrumentation(), + new ServletOutputStreamInstrumentation()); } private static class SparkJavaBodyInstrumentation implements TypeInstrumentation { @@ -70,7 +75,7 @@ public Map, String> transfor .and(takesArgument(0, named("javax.servlet.ServletRequest"))) .and(takesArgument(1, named("javax.servlet.ServletResponse"))) .and(isPublic()), - Servlet31Advice.class.getName()); + Servlet31NoWrappingInstrumentation.ServletAdvice.class.getName()); } } } diff --git a/instrumentation/spark-2.3/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/sparkjava/SparkJavaInstrumentationTest.java b/instrumentation/spark-2.3/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/sparkjava/SparkJavaInstrumentationTest.java index 180c27851..0375277dc 100644 --- a/instrumentation/spark-2.3/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/sparkjava/SparkJavaInstrumentationTest.java +++ b/instrumentation/spark-2.3/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/sparkjava/SparkJavaInstrumentationTest.java @@ -50,6 +50,8 @@ public static void postJson() { Spark.post( "/", (req, res) -> { + System.out.printf("Spark received: %s\n", req.body()); + res.header(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); res.type("application/json"); return RESPONSE_BODY; diff --git a/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/BoundedCharArrayWriter.java b/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/BoundedCharArrayWriter.java index 7ec057272..8925c618b 100644 --- a/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/BoundedCharArrayWriter.java +++ b/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/BoundedCharArrayWriter.java @@ -95,6 +95,9 @@ public void write(char[] cbuf) throws IOException { @Override public void write(String str) throws IOException { + if (str == null) { + str = "null"; + } this.write(str, 0, str.length()); } } diff --git a/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/ByteBufferSpanPair.java b/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/ByteBufferSpanPair.java new file mode 100644 index 000000000..17fa2d998 --- /dev/null +++ b/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/ByteBufferSpanPair.java @@ -0,0 +1,48 @@ +/* + * 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.hypertrace.agent.core.instrumentation.buffer; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import java.io.UnsupportedEncodingException; + +public class ByteBufferSpanPair { + + public final Span span; + public final BoundedByteArrayOutputStream buffer; + private boolean bufferCaptured; + + public ByteBufferSpanPair(Span span, BoundedByteArrayOutputStream buffer) { + this.span = span; + this.buffer = buffer; + } + + public void captureBody(AttributeKey attributeKey) { + if (bufferCaptured) { + return; + } + bufferCaptured = true; + + String requestBody = null; + try { + requestBody = buffer.toStringWithSuppliedCharset(); + } catch (UnsupportedEncodingException e) { + // ignore charset has been parsed before + } + span.setAttribute(attributeKey, requestBody); + } +} diff --git a/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/CharBufferSpanPair.java b/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/CharBufferSpanPair.java new file mode 100644 index 000000000..baf430390 --- /dev/null +++ b/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/buffer/CharBufferSpanPair.java @@ -0,0 +1,46 @@ +/* + * 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.hypertrace.agent.core.instrumentation.buffer; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; + +public class CharBufferSpanPair { + + public final Span span; + public final BoundedCharArrayWriter buffer; + + /** + * A flag to signalize that buffer has been added to span. For instance Jetty calls reader#read in + * recycle method and this flag prevents capturing the payload twice. + */ + private boolean bufferCaptured; + + public CharBufferSpanPair(Span span, BoundedCharArrayWriter buffer) { + this.span = span; + this.buffer = buffer; + } + + public void captureBody(AttributeKey attributeKey) { + if (bufferCaptured) { + return; + } + bufferCaptured = true; + String requestBody = buffer.toString(); + span.setAttribute(attributeKey, requestBody); + } +} diff --git a/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/HypertraceGlobalIgnoreMatcher.java b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/HypertraceGlobalIgnoreMatcher.java index a54fe0912..e86f450e3 100644 --- a/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/HypertraceGlobalIgnoreMatcher.java +++ b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/HypertraceGlobalIgnoreMatcher.java @@ -29,7 +29,10 @@ public Result type(net.bytebuddy.description.type.TypeDescription target) { if (actualName.equals("java.io.InputStream") || actualName.equals("java.io.OutputStream") || actualName.equals("java.io.ByteArrayInputStream") - || actualName.equals("java.io.ByteArrayOutputStream")) { + || actualName.equals("java.io.ByteArrayOutputStream") + // servlet request/response body capture instrumentation + || actualName.equals("java.io.BufferedReader") + || actualName.equals("java.io.PrintWriter")) { return Result.ALLOW; } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 607d04683..56a32c81d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -55,3 +55,7 @@ include("instrumentation:spring:spring-webflux-5.0") findProject(":instrumentation:spring:spring-webflux-5.0")?.name = "spring-webflux-5.0" include("instrumentation:micronaut-1.0") findProject(":instrumentation:micronaut-1.0")?.name = "micronaut-1.0" +include("instrumentation:servlet:servlet-3.0-no-wrapping") +findProject(":instrumentation:servlet:servlet-3.0-no-wrapping")?.name = "servlet-3.0-no-wrapping" +include("instrumentation:servlet:servlet-rw") +findProject(":instrumentation:servlet:servlet-rw")?.name = "servlet-rw"