Skip to content

Commit a711a27

Browse files
authored
Merge pull request #139 from DataDog/tyler/errors
Capture and report errors and stacktraces
2 parents 1654a8a + 526d7de commit a711a27

File tree

9 files changed

+136
-7
lines changed

9 files changed

+136
-7
lines changed

dd-java-agent-ittests/src/test/groovy/com/datadoghq/agent/integration/servlet/JettyServletTest.groovy

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,44 @@ class JettyServletTest extends Specification {
114114
"sync" | "Hello Sync"
115115
}
116116

117+
@Unroll
118+
def "test #path error servlet call"() {
119+
setup:
120+
def request = new Request.Builder()
121+
.url("http://localhost:$PORT/$path?error=true")
122+
.get()
123+
.build()
124+
def response = client.newCall(request).execute()
125+
126+
expect:
127+
response.body().string().trim() != expectedResponse
128+
writer.size() == 2 // second (parent) trace is the okhttp call above...
129+
def trace = writer.firstTrace()
130+
trace.size() == 1
131+
def span = trace[0]
132+
133+
span.context().operationName == "servlet.request"
134+
span.context().getErrorFlag()
135+
span.context().parentId != 0 // parent should be the okhttp call.
136+
span.context().tags["http.url"] == "http://localhost:$PORT/$path"
137+
span.context().tags["http.method"] == "GET"
138+
span.context().tags["span.kind"] == "server"
139+
span.context().tags["component"] == "java-web-servlet"
140+
span.context().tags["http.status_code"] == 500
141+
span.context().tags["thread.name"] != null
142+
span.context().tags["thread.id"] != null
143+
span.context().tags["error"] == true
144+
span.context().tags["error.msg"] == "some $path error"
145+
span.context().tags["error.type"] == RuntimeException.getName()
146+
span.context().tags["error.stack"] != null
147+
span.context().tags.size() == 11
148+
149+
where:
150+
path | expectedResponse
151+
//"async" | "Hello Async" // FIXME: I can't seem get the async error handler to trigger
152+
"sync" | "Hello Sync"
153+
}
154+
117155
private static int randomOpenPort() {
118156
new ServerSocket(0).withCloseable {
119157
it.setReuseAddress(true)

dd-java-agent-ittests/src/test/groovy/com/datadoghq/agent/integration/servlet/TestServlet.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class TestServlet {
1212
static class Sync extends AbstractHttpServlet {
1313
@Override
1414
void doGet(HttpServletRequest req, HttpServletResponse resp) {
15+
if (req.getParameter("error") != null) {
16+
throw new RuntimeException("some sync error")
17+
}
1518
resp.writer.print("Hello Sync")
1619
}
1720
}

dd-java-agent-ittests/src/test/groovy/com/datadoghq/agent/integration/servlet/TomcatServletTest.groovy

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,44 @@ class TomcatServletTest extends Specification {
114114
"sync" | "Hello Sync"
115115
}
116116

117+
@Unroll
118+
def "test #path error servlet call"() {
119+
setup:
120+
def request = new Request.Builder()
121+
.url("http://localhost:$PORT/$path?error=true")
122+
.get()
123+
.build()
124+
def response = client.newCall(request).execute()
125+
126+
expect:
127+
response.body().string().trim() != expectedResponse
128+
writer.size() == 2 // second (parent) trace is the okhttp call above...
129+
def trace = writer.firstTrace()
130+
trace.size() == 1
131+
def span = trace[0]
132+
133+
span.context().operationName == "servlet.request"
134+
span.context().getErrorFlag()
135+
span.context().parentId != 0 // parent should be the okhttp call.
136+
span.context().tags["http.url"] == "http://localhost:$PORT/$path"
137+
span.context().tags["http.method"] == "GET"
138+
span.context().tags["span.kind"] == "server"
139+
span.context().tags["component"] == "java-web-servlet"
140+
span.context().tags["http.status_code"] == 500
141+
span.context().tags["thread.name"] != null
142+
span.context().tags["thread.id"] != null
143+
span.context().tags["error"] == true
144+
span.context().tags["error.msg"] == "some $path error"
145+
span.context().tags["error.type"] == RuntimeException.getName()
146+
span.context().tags["error.stack"] != null
147+
span.context().tags.size() == 11
148+
149+
where:
150+
path | expectedResponse
151+
//"async" | "Hello Async" // FIXME: I can't seem get the async error handler to trigger
152+
"sync" | "Hello Sync"
153+
}
154+
117155
private static int randomOpenPort() {
118156
new ServerSocket(0).withCloseable {
119157
it.setReuseAddress(true)

dd-java-agent-ittests/src/test/java/com/datadoghq/agent/TraceAnnotationsManagerTest.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44

55
import com.datadoghq.agent.test.SayTracedHello;
6+
import com.datadoghq.trace.DDBaseSpan;
67
import com.datadoghq.trace.DDTracer;
78
import com.datadoghq.trace.integration.ErrorFlag;
89
import com.datadoghq.trace.writer.ListWriter;
910
import io.opentracing.util.GlobalTracer;
11+
import java.io.PrintWriter;
12+
import java.io.StringWriter;
1013
import java.lang.reflect.Field;
1114
import org.junit.Before;
1215
import org.junit.Test;
@@ -67,13 +70,22 @@ public void testExceptionExit() {
6770

6871
tracer.addDecorator(new ErrorFlag());
6972

73+
Throwable error = null;
7074
try {
7175
SayTracedHello.sayERROR();
7276
} catch (final Throwable ex) {
73-
// DO NOTHING
77+
error = ex;
7478
}
75-
assertThat(writer.firstTrace().get(0).getOperationName()).isEqualTo("ERROR");
76-
assertThat(writer.firstTrace().get(0).getTags().get("error")).isEqualTo("true");
77-
assertThat(writer.firstTrace().get(0).getError()).isEqualTo(1);
79+
80+
final StringWriter errorString = new StringWriter();
81+
error.printStackTrace(new PrintWriter(errorString));
82+
83+
final DDBaseSpan<?> span = writer.firstTrace().get(0);
84+
assertThat(span.getOperationName()).isEqualTo("ERROR");
85+
assertThat(span.getTags().get("error")).isEqualTo("true");
86+
assertThat(span.getTags().get("error.msg")).isEqualTo(error.getMessage());
87+
assertThat(span.getTags().get("error.type")).isEqualTo(error.getClass().getName());
88+
assertThat(span.getTags().get("error.stack")).isEqualTo(errorString.toString());
89+
assertThat(span.getError()).isEqualTo(1);
7890
}
7991
}

dd-java-agent/integrations/helpers/src/main/java/com/datadoghq/agent/integration/Servlet2Helper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
99
import io.opentracing.propagation.Format;
1010
import io.opentracing.tag.Tags;
11+
import java.util.Collections;
1112
import javax.servlet.http.HttpServletRequest;
1213
import javax.servlet.http.HttpServletResponse;
1314
import lombok.extern.slf4j.Slf4j;
@@ -73,6 +74,7 @@ public void onError(
7374
final ActiveSpan span = tracer.activeSpan();
7475
if (span != null) {
7576
ServletFilterSpanDecorator.STANDARD_TAGS.onError(req, resp, ex, span);
77+
span.log(Collections.singletonMap("error.object", ex));
7678
span.deactivate();
7779
}
7880
}

dd-java-agent/integrations/helpers/src/main/java/com/datadoghq/agent/integration/Servlet3Helper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.opentracing.ActiveSpan;
44
import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
55
import java.io.IOException;
6+
import java.util.Collections;
67
import java.util.concurrent.atomic.AtomicBoolean;
78
import javax.servlet.AsyncEvent;
89
import javax.servlet.AsyncListener;
@@ -86,6 +87,7 @@ public void onError(final AsyncEvent event) throws IOException {
8687
(HttpServletResponse) event.getSuppliedResponse(),
8788
event.getThrowable(),
8889
span);
90+
span.log(Collections.singletonMap("error.object", event.getThrowable()));
8991
}
9092
}
9193
}

dd-java-agent/src/main/java/com/datadoghq/agent/TraceAnnotationsManager.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.io.StringWriter;
88
import java.lang.reflect.Method;
99
import java.util.Arrays;
10+
import java.util.Collections;
1011
import java.util.Set;
1112
import javassist.ClassPool;
1213
import javassist.CtClass;
@@ -43,7 +44,10 @@ public class TraceAnnotationsManager {
4344
+ "DO\n"
4445
+ "span.setTag("
4546
+ Tags.class.getName()
46-
+ " .ERROR.getKey(),\"true\");\n"
47+
+ ".ERROR.getKey(),\"true\");\n"
48+
+ "span.log("
49+
+ Collections.class.getName()
50+
+ ".singletonMap(\"error.object\",$^));\n"
4751
+ "span.deactivate();\n";
4852

4953
private final Retransformer transformer;

dd-trace/src/main/java/com/datadoghq/trace/DDBaseSpan.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.fasterxml.jackson.annotation.JsonGetter;
55
import com.fasterxml.jackson.annotation.JsonIgnore;
66
import io.opentracing.BaseSpan;
7+
import java.io.PrintWriter;
8+
import java.io.StringWriter;
79
import java.util.HashMap;
810
import java.util.Map;
911
import java.util.Map.Entry;
@@ -95,6 +97,26 @@ protected final boolean isRootSpan() {
9597
&& this.context.getTracer() != null;
9698
}
9799

100+
public void setErrorMeta(final Throwable error) {
101+
context.setErrorFlag(true);
102+
103+
setTag(DDTags.ERROR_MSG, error.getMessage());
104+
setTag(DDTags.ERROR_TYPE, error.getClass().getName());
105+
106+
final StringWriter errorString = new StringWriter();
107+
error.printStackTrace(new PrintWriter(errorString));
108+
setTag(DDTags.ERROR_STACK, errorString.toString());
109+
}
110+
111+
private boolean extractError(final Map<String, ?> map) {
112+
if (map.get("error.object") instanceof Throwable) {
113+
final Throwable error = (Throwable) map.get("error.object");
114+
setErrorMeta(error);
115+
return true;
116+
}
117+
return false;
118+
}
119+
98120
/* (non-Javadoc)
99121
* @see io.opentracing.BaseSpan#setTag(java.lang.String, java.lang.String)
100122
*/
@@ -161,7 +183,9 @@ public final S setOperationName(final String operationName) {
161183
*/
162184
@Override
163185
public final S log(final Map<String, ?> map) {
164-
log.debug("`log` method is not implemented. Doing nothing");
186+
if (!extractError(map)) {
187+
log.debug("`log` method is not implemented. Doing nothing");
188+
}
165189
return thisInstance();
166190
}
167191

@@ -170,7 +194,9 @@ public final S log(final Map<String, ?> map) {
170194
*/
171195
@Override
172196
public final S log(final long l, final Map<String, ?> map) {
173-
log.debug("`log` method is not implemented. Doing nothing");
197+
if (!extractError(map)) {
198+
log.debug("`log` method is not implemented. Doing nothing");
199+
}
174200
return thisInstance();
175201
}
176202

dd-trace/src/main/java/com/datadoghq/trace/DDTags.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ public class DDTags {
77
public static final String THREAD_NAME = "thread.name";
88
public static final String THREAD_ID = "thread.id";
99
public static final String DB_STATEMENT = "sql.query";
10+
11+
public static final String ERROR_MSG = "error.msg"; // string representing the error message
12+
public static final String ERROR_TYPE = "error.type"; // string representing the type of the error
13+
public static final String ERROR_STACK = "error.stack"; // human readable version of the stack
1014
}

0 commit comments

Comments
 (0)