diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts index a03906d1e9b4..fcbacd95c267 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts @@ -12,7 +12,7 @@ muzzle { skip("1.2.1", "1.2.2", "1.2.3", "1.2.4") // 3.2.1.RELEASE has transitive dependencies like spring-web as "provided" instead of "compile" skip("3.2.1.RELEASE") - extraDependency("javax.servlet:javax.servlet-api:3.0.1") + extraDependency("javax.servlet:javax.servlet-api:3.1.0") assertInverse.set(true) } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcInstrumentationModule.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcInstrumentationModule.java index a9f6c445727f..8240b14a321b 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcInstrumentationModule.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcInstrumentationModule.java @@ -21,7 +21,10 @@ public SpringWebMvcInstrumentationModule() { @Override public boolean isHelperClass(String className) { return className.startsWith( - "org.springframework.web.servlet.v3_1.OpenTelemetryHandlerMappingFilter"); + "org.springframework.web.servlet.v3_1.OpenTelemetryHandlerMappingFilter") + || className.startsWith("org.springframework.web.servlet.v3_1.ContentCachingRequestWrapper") + || className.startsWith( + "org.springframework.web.servlet.v3_1.ContentCachingResponseWrapper"); } @Override diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/ContentCachingRequestWrapper.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/ContentCachingRequestWrapper.java new file mode 100644 index 000000000000..5593d5ae96e6 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/ContentCachingRequestWrapper.java @@ -0,0 +1,258 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.springframework.web.servlet.v3_1; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.web.util.WebUtils; + +public class ContentCachingRequestWrapper extends HttpServletRequestWrapper { + + private final ByteArrayOutputStream cachedContent; + + @Nullable private final Integer contentCacheLimit; + + @Nullable private ServletInputStream inputStream; + + @Nullable private BufferedReader reader; + + /** + * Create a new ContentCachingRequestWrapper for the given servlet request. + * + * @param request the original servlet request + */ + public ContentCachingRequestWrapper(HttpServletRequest request) { + super(request); + int contentLength = request.getContentLength(); + this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024); + this.contentCacheLimit = null; + } + + /** + * Create a new ContentCachingRequestWrapper for the given servlet request. + * + * @param request the original servlet request + * @param contentCacheLimit the maximum number of bytes to cache per request + * @since 4.3.6 + * @see #handleContentOverflow(int) + */ + public ContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit) { + super(request); + this.cachedContent = new ByteArrayOutputStream(contentCacheLimit); + this.contentCacheLimit = contentCacheLimit; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (this.inputStream == null) { + this.inputStream = new ContentCachingInputStream(getRequest().getInputStream()); + } + return this.inputStream; + } + + @Override + public String getCharacterEncoding() { + String enc = super.getCharacterEncoding(); + return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING); + } + + @Override + public BufferedReader getReader() throws IOException { + if (this.reader == null) { + this.reader = + new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding())); + } + return this.reader; + } + + @Override + public String getParameter(String name) { + if (this.cachedContent.size() == 0 && isFormPost()) { + writeRequestParametersToCachedContent(); + } + return super.getParameter(name); + } + + @Override + public Map getParameterMap() { + if (this.cachedContent.size() == 0 && isFormPost()) { + writeRequestParametersToCachedContent(); + } + return super.getParameterMap(); + } + + @Override + public Enumeration getParameterNames() { + if (this.cachedContent.size() == 0 && isFormPost()) { + writeRequestParametersToCachedContent(); + } + return super.getParameterNames(); + } + + @Override + public String[] getParameterValues(String name) { + if (this.cachedContent.size() == 0 && isFormPost()) { + writeRequestParametersToCachedContent(); + } + return super.getParameterValues(name); + } + + private boolean isFormPost() { + String contentType = getContentType(); + return (contentType != null + && contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + && HttpMethod.POST.toString().equals(getMethod())); + } + + void writeRequestParametersToCachedContent() { + try { + if (this.cachedContent.size() == 0) { + String requestEncoding = getCharacterEncoding(); + Map form = super.getParameterMap(); + for (Iterator nameIterator = form.keySet().iterator(); nameIterator.hasNext(); ) { + String name = nameIterator.next(); + List values = Arrays.asList(form.get(name)); + for (Iterator valueIterator = values.iterator(); valueIterator.hasNext(); ) { + String value = valueIterator.next(); + this.cachedContent.write( + URLEncoder.encode(name, requestEncoding).getBytes(Charset.defaultCharset())); + if (value != null) { + this.cachedContent.write('='); + this.cachedContent.write( + URLEncoder.encode(value, requestEncoding).getBytes(Charset.defaultCharset())); + if (valueIterator.hasNext()) { + this.cachedContent.write('&'); + } + } + } + if (nameIterator.hasNext()) { + this.cachedContent.write('&'); + } + } + } + } catch (IOException ex) { + throw new IllegalStateException("Failed to write request parameters to cached content", ex); + } + } + + /** + * Return the cached request content as a byte array. + * + *

The returned array will never be larger than the content cache limit. + * + *

Note: The byte array returned from this method reflects the amount of + * content that has been read at the time when it is called. If the application does not read the + * content, this method returns an empty array. + * + * @see #ContentCachingRequestWrapper(HttpServletRequest, int) + */ + public byte[] getContentAsByteArray() { + return this.cachedContent.toByteArray(); + } + + /** + * Template method for handling a content overflow: specifically, a request body being read that + * exceeds the specified content cache limit. + * + *

The default implementation is empty. Subclasses may override this to throw a + * payload-too-large exception or the like. + * + * @param contentCacheLimit the maximum number of bytes to cache per request which has just been + * exceeded + * @since 4.3.6 + * @see #ContentCachingRequestWrapper(HttpServletRequest, int) + */ + protected void handleContentOverflow(int contentCacheLimit) {} + + private class ContentCachingInputStream extends ServletInputStream { + + private final ServletInputStream is; + + private boolean overflow = false; + + public ContentCachingInputStream(ServletInputStream is) { + this.is = is; + } + + private void writeToCache(byte[] b, int off, int count) { + if (!this.overflow && count > 0) { + if (contentCacheLimit != null && count + cachedContent.size() > contentCacheLimit) { + this.overflow = true; + cachedContent.write(b, off, contentCacheLimit - cachedContent.size()); + handleContentOverflow(contentCacheLimit); + return; + } + cachedContent.write(b, off, count); + } + } + + @Override + public int read() throws IOException { + int ch = this.is.read(); + if (ch != -1 && !this.overflow) { + if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) { + this.overflow = true; + handleContentOverflow(contentCacheLimit); + } else { + cachedContent.write(ch); + } + } + return ch; + } + + @Override + public int read(byte[] b) throws IOException { + int count = this.is.read(b); + writeToCache(b, 0, count); + return count; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int count = this.is.read(b, off, len); + writeToCache(b, off, count); + return count; + } + + @Override + public int readLine(byte[] b, int off, int len) throws IOException { + int count = this.is.readLine(b, off, len); + writeToCache(b, off, count); + return count; + } + + @Override + public boolean isFinished() { + return this.is.isFinished(); + } + + @Override + public boolean isReady() { + return this.is.isReady(); + } + + @Override + public void setReadListener(ReadListener readListener) { + this.is.setReadListener(readListener); + } + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/ContentCachingResponseWrapper.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/ContentCachingResponseWrapper.java new file mode 100644 index 000000000000..52cc092112e8 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/ContentCachingResponseWrapper.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.springframework.web.servlet.v3_1; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import org.springframework.web.util.WebUtils; + +public class ContentCachingResponseWrapper extends HttpServletResponseWrapper { + private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024); + + private final ServletOutputStream outputStream = new ResponseServletOutputStream(); + + private PrintWriter writer; + + private int statusCode = HttpServletResponse.SC_OK; + + public ContentCachingResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public void setStatus(int sc) { + super.setStatus(sc); + this.statusCode = sc; + } + + @SuppressWarnings("deprecation") // + @Override + public void setStatus(int sc, String sm) { + super.setStatus(sc, sm); + this.statusCode = sc; + } + + @Override + public void sendError(int sc) throws IOException { + copyBodyToResponse(); + super.sendError(sc); + this.statusCode = sc; + } + + @Override + public void sendError(int sc, String msg) throws IOException { + copyBodyToResponse(); + super.sendError(sc, msg); + this.statusCode = sc; + } + + @Override + public void sendRedirect(String location) throws IOException { + copyBodyToResponse(); + super.sendRedirect(location); + } + + @Override + public ServletOutputStream getOutputStream() { + return this.outputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (this.writer == null) { + String characterEncoding = getCharacterEncoding(); + this.writer = + (characterEncoding != null + ? new ResponsePrintWriter(characterEncoding) + : new ResponsePrintWriter(WebUtils.DEFAULT_CHARACTER_ENCODING)); + } + return this.writer; + } + + @Override + public void resetBuffer() { + this.content.reset(); + } + + @Override + public void reset() { + super.reset(); + this.content.reset(); + } + + /** Return the status code as specified on the response. */ + public int getStatusCode() { + return this.statusCode; + } + + /** Return the cached response content as a byte array. */ + public byte[] getContentAsByteArray() { + return this.content.toByteArray(); + } + + void copyBodyToResponse() throws IOException { + if (this.content.size() > 0) { + getResponse().setContentLength(this.content.size()); + getResponse().getOutputStream().write(this.content.toByteArray()); + this.content.reset(); + } + } + + private class ResponseServletOutputStream extends ServletOutputStream { + + @Override + public void write(int b) throws IOException { + content.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + content.write(b, off, len); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) {} + } + + private class ResponsePrintWriter extends PrintWriter { + + public ResponsePrintWriter(String characterEncoding) throws UnsupportedEncodingException { + super(new OutputStreamWriter(content, characterEncoding)); + } + + @Override + public void write(char[] buf, int off, int len) { + super.write(buf, off, len); + super.flush(); + } + + @Override + public void write(String s, int off, int len) { + super.write(s, off, len); + super.flush(); + } + + @Override + public void write(int c) { + super.write(c); + super.flush(); + } + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.java index c3f11894d4d2..157ab0bdfa40 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.java @@ -6,7 +6,9 @@ package org.springframework.web.servlet.v3_1; import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static java.nio.charset.StandardCharsets.UTF_8; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; @@ -79,17 +81,42 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha return; } + ContentCachingResponseWrapper responseWrapper = + new ContentCachingResponseWrapper((HttpServletResponse) response); + + ContentCachingRequestWrapper requestWrapper = + new ContentCachingRequestWrapper((HttpServletRequest) request); + try { - filterChain.doFilter(request, response); + filterChain.doFilter(requestWrapper, responseWrapper); } finally { if (handlerMappings != null) { Context context = Context.current(); + setAttributes(requestWrapper, responseWrapper, context); HttpRouteHolder.updateHttpRoute( context, CONTROLLER, serverSpanName, (HttpServletRequest) request); } } } + private static void setAttributes( + ContentCachingRequestWrapper requestWrapper, + ContentCachingResponseWrapper responseWrapper, + Context context) + throws IOException { + Span span = Span.fromContext(context); + requestWrapper.writeRequestParametersToCachedContent(); + byte[] requestContentAsByteArray = requestWrapper.getContentAsByteArray(); + if (requestContentAsByteArray != null && requestContentAsByteArray.length > 0) { + span.setAttribute("http.request.body", new String(requestContentAsByteArray, UTF_8)); + } + byte[] responseContentAsByteArray = responseWrapper.getContentAsByteArray(); + if (responseContentAsByteArray != null && responseContentAsByteArray.length > 0) { + span.setAttribute("http.response.body", new String(responseContentAsByteArray, UTF_8)); + responseWrapper.copyBodyToResponse(); + } + } + @Override public void destroy() {}