Skip to content

Commit 8b4cecd

Browse files
šŸ’ 9323 - Improve RUM injection matching and avoid truncating responses (#9342)
* Make rum injector stream/writer more resilient to errors (cherry picked from commit 36246b4) * fix tests (cherry picked from commit 993bf57) * Improve RUM injection matching and avoid truncating responses (cherry picked from commit f46c8f1) * Use MH for setContentLengthLong since available from 3.1 (cherry picked from commit 13753c1) * Fix tomcat11 tests (cherry picked from commit 4fb4129) * Update dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy Co-authored-by: Bruce Bujon <[email protected]> (cherry picked from commit 4b44a38) * Update dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy Co-authored-by: Bruce Bujon <[email protected]> (cherry picked from commit d7e1fdf) * remove leftover (cherry picked from commit 22e5389) --------- Co-authored-by: Bruce Bujon <[email protected]>
1 parent 579165c commit 8b4cecd

File tree

17 files changed

+268
-150
lines changed

17 files changed

+268
-150
lines changed

ā€Ždd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import java.io.IOException;
44
import java.io.OutputStream;
5+
import javax.annotation.concurrent.NotThreadSafe;
56

67
/**
78
* An OutputStream containing a circular buffer with a lookbehind buffer of n bytes. The first time
89
* that the latest n bytes matches the marker, a content is injected before. In case of IOException
910
* thrown by the downstream, the buffer will be lost unless the error occurred when draining it. In
1011
* this case the draining will be resumed.
1112
*/
13+
@NotThreadSafe
1214
public class InjectingPipeOutputStream extends OutputStream {
1315
private final byte[] lookbehind;
1416
private int pos;
@@ -168,6 +170,13 @@ private void drain() throws IOException {
168170
}
169171
}
170172

173+
public void commit() throws IOException {
174+
if (filter || wasDraining) {
175+
filter = false;
176+
drain();
177+
}
178+
}
179+
171180
@Override
172181
public void flush() throws IOException {
173182
downstream.flush();
@@ -176,9 +185,7 @@ public void flush() throws IOException {
176185
@Override
177186
public void close() throws IOException {
178187
try {
179-
if (filter || wasDraining) {
180-
drain();
181-
}
188+
commit();
182189
} finally {
183190
downstream.close();
184191
}

ā€Ždd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import java.io.IOException;
44
import java.io.Writer;
5+
import javax.annotation.concurrent.NotThreadSafe;
56

67
/**
78
* A Writer containing a circular buffer with a lookbehind buffer of n bytes. The first time that
89
* the latest n bytes matches the marker, a content is injected before. In case of IOException
910
* thrown by the downstream, the buffer will be lost unless the error occurred when draining it. In
1011
* this case the draining will be resumed.
1112
*/
13+
@NotThreadSafe
1214
public class InjectingPipeWriter extends Writer {
1315
private final char[] lookbehind;
1416
private int pos;
@@ -169,6 +171,13 @@ private void drain() throws IOException {
169171
}
170172
}
171173

174+
public void commit() throws IOException {
175+
if (filter || wasDraining) {
176+
filter = false;
177+
drain();
178+
}
179+
}
180+
172181
@Override
173182
public void flush() throws IOException {
174183
downstream.flush();
@@ -177,9 +186,7 @@ public void flush() throws IOException {
177186
@Override
178187
public void close() throws IOException {
179188
try {
180-
if (filter || wasDraining) {
181-
drain();
182-
}
189+
commit();
183190
} finally {
184191
downstream.close();
185192
}

ā€Ždd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,37 @@
22

33
import datadog.trace.api.rum.RumInjector;
44
import datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeWriter;
5+
import datadog.trace.util.MethodHandles;
56
import java.io.IOException;
67
import java.io.PrintWriter;
8+
import java.lang.invoke.MethodHandle;
79
import java.nio.charset.Charset;
810
import javax.servlet.ServletOutputStream;
11+
import javax.servlet.ServletResponse;
912
import javax.servlet.http.HttpServletResponse;
1013
import javax.servlet.http.HttpServletResponseWrapper;
1114

1215
public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper {
1316
private final RumInjector rumInjector;
14-
private ServletOutputStream outputStream;
17+
private WrappedServletOutputStream outputStream;
1518
private PrintWriter printWriter;
16-
private boolean shouldInject = false;
19+
private InjectingPipeWriter wrappedPipeWriter;
20+
private boolean shouldInject = true;
21+
22+
private static final MethodHandle SET_CONTENT_LENGTH_LONG = getMh("setContentLengthLong");
23+
24+
private static MethodHandle getMh(final String name) {
25+
try {
26+
return new MethodHandles(ServletResponse.class.getClassLoader())
27+
.method(ServletResponse.class, name);
28+
} catch (Throwable ignored) {
29+
return null;
30+
}
31+
}
32+
33+
private static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
34+
throw (E) e;
35+
}
1736

1837
public RumHttpServletResponseWrapper(HttpServletResponse response) {
1938
super(response);
@@ -22,50 +41,68 @@ public RumHttpServletResponseWrapper(HttpServletResponse response) {
2241

2342
@Override
2443
public ServletOutputStream getOutputStream() throws IOException {
44+
if (outputStream != null) {
45+
return outputStream;
46+
}
2547
if (!shouldInject) {
2648
return super.getOutputStream();
2749
}
28-
if (outputStream == null) {
29-
String encoding = getCharacterEncoding();
30-
if (encoding == null) {
31-
encoding = Charset.defaultCharset().name();
32-
}
33-
outputStream =
34-
new WrappedServletOutputStream(
35-
super.getOutputStream(),
36-
rumInjector.getMarkerBytes(encoding),
37-
rumInjector.getSnippetBytes(encoding),
38-
this::onInjected);
50+
String encoding = getCharacterEncoding();
51+
if (encoding == null) {
52+
encoding = Charset.defaultCharset().name();
3953
}
54+
outputStream =
55+
new WrappedServletOutputStream(
56+
super.getOutputStream(),
57+
rumInjector.getMarkerBytes(encoding),
58+
rumInjector.getSnippetBytes(encoding),
59+
this::onInjected);
60+
4061
return outputStream;
4162
}
4263

4364
@Override
4465
public PrintWriter getWriter() throws IOException {
45-
final PrintWriter delegate = super.getWriter();
46-
if (!shouldInject) {
47-
return delegate;
66+
if (printWriter != null) {
67+
return printWriter;
4868
}
49-
if (printWriter == null) {
50-
printWriter =
51-
new PrintWriter(
52-
new InjectingPipeWriter(
53-
delegate,
54-
rumInjector.getMarkerChars(),
55-
rumInjector.getSnippetChars(),
56-
this::onInjected));
69+
if (!shouldInject) {
70+
return super.getWriter();
5771
}
72+
wrappedPipeWriter =
73+
new InjectingPipeWriter(
74+
super.getWriter(),
75+
rumInjector.getMarkerChars(),
76+
rumInjector.getSnippetChars(),
77+
this::onInjected);
78+
printWriter = new PrintWriter(wrappedPipeWriter);
79+
5880
return printWriter;
5981
}
6082

6183
@Override
6284
public void setContentLength(int len) {
6385
// don't set it since we don't know if we will inject
86+
if (!shouldInject) {
87+
super.setContentLength(len);
88+
}
89+
}
90+
91+
@Override
92+
public void setContentLengthLong(long len) {
93+
if (!shouldInject && SET_CONTENT_LENGTH_LONG != null) {
94+
try {
95+
SET_CONTENT_LENGTH_LONG.invoke(getResponse(), len);
96+
} catch (Throwable t) {
97+
sneakyThrow(t);
98+
}
99+
}
64100
}
65101

66102
@Override
67103
public void reset() {
68104
this.outputStream = null;
105+
this.wrappedPipeWriter = null;
69106
this.printWriter = null;
70107
this.shouldInject = false;
71108
super.reset();
@@ -74,8 +111,8 @@ public void reset() {
74111
@Override
75112
public void resetBuffer() {
76113
this.outputStream = null;
114+
this.wrappedPipeWriter = null;
77115
this.printWriter = null;
78-
this.shouldInject = false;
79116
super.resetBuffer();
80117
}
81118

@@ -89,7 +126,27 @@ public void onInjected() {
89126

90127
@Override
91128
public void setContentType(String type) {
92-
shouldInject = type != null && type.contains("text/html");
129+
if (shouldInject) {
130+
shouldInject = type != null && type.contains("text/html");
131+
}
132+
if (!shouldInject) {
133+
commit();
134+
}
93135
super.setContentType(type);
94136
}
137+
138+
public void commit() {
139+
if (wrappedPipeWriter != null) {
140+
try {
141+
wrappedPipeWriter.commit();
142+
} catch (Throwable ignored) {
143+
}
144+
}
145+
if (outputStream != null) {
146+
try {
147+
outputStream.commit();
148+
} catch (Throwable ignored) {
149+
}
150+
}
151+
}
95152
}

ā€Ždd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public static boolean onEnter(
3535
@Advice.Argument(value = 1, readOnly = false) ServletResponse response,
3636
@Advice.Local("isDispatch") boolean isDispatch,
3737
@Advice.Local("finishSpan") boolean finishSpan,
38-
@Advice.Local("contextScope") ContextScope scope) {
38+
@Advice.Local("contextScope") ContextScope scope,
39+
@Advice.Local("rumServletWrapper") RumHttpServletResponseWrapper rumServletWrapper) {
3940
final boolean invalidRequest =
4041
!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse);
4142
if (invalidRequest) {
@@ -47,7 +48,8 @@ public static boolean onEnter(
4748

4849
if (RumInjector.get().isEnabled() && httpServletRequest.getAttribute(DD_RUM_INJECTED) == null) {
4950
httpServletRequest.setAttribute(DD_RUM_INJECTED, Boolean.TRUE);
50-
httpServletResponse = new RumHttpServletResponseWrapper(httpServletResponse);
51+
rumServletWrapper = new RumHttpServletResponseWrapper(httpServletResponse);
52+
httpServletResponse = rumServletWrapper;
5153
response = httpServletResponse;
5254
}
5355

@@ -108,7 +110,11 @@ public static void stopSpan(
108110
@Advice.Local("contextScope") final ContextScope scope,
109111
@Advice.Local("isDispatch") boolean isDispatch,
110112
@Advice.Local("finishSpan") boolean finishSpan,
113+
@Advice.Local("rumServletWrapper") RumHttpServletResponseWrapper rumServletWrapper,
111114
@Advice.Thrown final Throwable throwable) {
115+
if (rumServletWrapper != null) {
116+
rumServletWrapper.commit();
117+
}
112118
// Set user.principal regardless of who created this span.
113119
final Object spanAttr = request.getAttribute(DD_SPAN_ATTRIBUTE);
114120
if (Config.get().isServletPrincipalEnabled()

ā€Ždd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
import datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeOutputStream;
44
import datadog.trace.util.MethodHandles;
55
import java.io.IOException;
6-
import java.io.OutputStream;
76
import java.lang.invoke.MethodHandle;
87
import javax.servlet.ServletOutputStream;
98
import javax.servlet.WriteListener;
109

1110
public class WrappedServletOutputStream extends ServletOutputStream {
12-
private final OutputStream filtered;
11+
private final InjectingPipeOutputStream filtered;
1312
private final ServletOutputStream delegate;
1413

1514
private static final MethodHandle IS_READY_MH = getMh("isReady");
@@ -83,4 +82,8 @@ public void setWriteListener(WriteListener writeListener) {
8382
sneakyThrow(e);
8483
}
8584
}
85+
86+
public void commit() throws IOException {
87+
filtered.commit();
88+
}
8689
}

ā€Ždd-java-agent/instrumentation/servlet/request-3/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/RumServlet.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class RumServlet extends HttpServlet {
1414

1515
@Override
1616
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
17-
resp.setContentType(mimeType)
1817
try (def writer = resp.getWriter()) {
18+
resp.setContentType(mimeType)
1919
writer.println("\n" +
2020
"<!doctype html>\n" +
2121
"<html>\n" +

ā€Ždd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/JakartaServletInstrumentation.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ public static class JakartaServletAdvice {
6868
@Advice.OnMethodEnter(suppress = Throwable.class)
6969
public static AgentSpan before(
7070
@Advice.Argument(0) final ServletRequest request,
71-
@Advice.Argument(value = 1, readOnly = false) ServletResponse response) {
71+
@Advice.Argument(value = 1, readOnly = false) ServletResponse response,
72+
@Advice.Local("rumServletWrapper") RumHttpServletResponseWrapper rumServletWrapper) {
7273
if (!(request instanceof HttpServletRequest)) {
7374
return null;
7475
}
@@ -79,7 +80,8 @@ public static AgentSpan before(
7980
if (RumInjector.get().isEnabled()
8081
&& httpServletRequest.getAttribute(DD_RUM_INJECTED) == null) {
8182
httpServletRequest.setAttribute(DD_RUM_INJECTED, Boolean.TRUE);
82-
response = new RumHttpServletResponseWrapper((HttpServletResponse) response);
83+
rumServletWrapper = new RumHttpServletResponseWrapper((HttpServletResponse) response);
84+
response = rumServletWrapper;
8385
}
8486
}
8587

@@ -95,10 +97,15 @@ public static AgentSpan before(
9597

9698
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
9799
public static void after(
98-
@Advice.Enter final AgentSpan span, @Advice.Argument(0) final ServletRequest request) {
100+
@Advice.Enter final AgentSpan span,
101+
@Advice.Argument(0) final ServletRequest request,
102+
@Advice.Local("rumServletWrapper") RumHttpServletResponseWrapper rumServletWrapper) {
99103
if (span == null) {
100104
return;
101105
}
106+
if (rumServletWrapper != null) {
107+
rumServletWrapper.commit();
108+
}
102109

103110
CallDepthThreadLocalMap.reset(HttpServletRequest.class);
104111
final HttpServletRequest httpServletRequest =

0 commit comments

Comments
Ā (0)