Skip to content

Commit fc56979

Browse files
committed
Add watches support through probe tags
Watches collect specific (deep) fields to add them exclusively into the snapshot instead of locals/arguments/statics/... Watches are defined using probe definition predefined tags: - dd_watches_dsl - dd_watches_json When you just want to reference deep fields just a.b.c you can use the dsl syntax (like Expression Language). But if you need more advanced (accessing map or list, using filter, any, ... functions) you need to use dd_watches_json and put the Json AST for Expression Language (like log message template with segments). Once the probe is executed, it interprets watches tags to evaluate the expression then add it in the snapshot as special attributes watches.
1 parent 2d2c029 commit fc56979

8 files changed

Lines changed: 196 additions & 9 deletions

File tree

dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class CapturedContext implements ValueReferenceResolver {
3535
private String spanId;
3636
private long duration;
3737
private final Map<String, Status> statusByProbeId = new LinkedHashMap<>();
38+
private Map<String, CapturedValue> watches;
3839

3940
public CapturedContext() {}
4041

@@ -267,6 +268,10 @@ public Map<String, CapturedValue> getStaticFields() {
267268
return staticFields;
268269
}
269270

271+
public Map<String, CapturedValue> getWatches() {
272+
return watches;
273+
}
274+
270275
public Limits getLimits() {
271276
return limits;
272277
}
@@ -288,6 +293,11 @@ public String getSpanId() {
288293
* instance representation into the corresponding string value.
289294
*/
290295
public void freeze(TimeoutChecker timeoutChecker) {
296+
if (watches != null) {
297+
// freeze only watches
298+
watches.values().forEach(capturedValue -> capturedValue.freeze(timeoutChecker));
299+
return;
300+
}
291301
if (arguments != null) {
292302
arguments.values().forEach(capturedValue -> capturedValue.freeze(timeoutChecker));
293303
}
@@ -383,6 +393,13 @@ private void putInStaticFields(String name, CapturedValue value) {
383393
staticFields.put(name, value);
384394
}
385395

396+
public void addWatch(CapturedValue value) {
397+
if (watches == null) {
398+
watches = new HashMap<>();
399+
}
400+
watches.put(value.name, value);
401+
}
402+
386403
public static class Status {
387404
public static final Status EMPTY_STATUS = new Status(ProbeImplementation.UNKNOWN);
388405
public static final Status EMPTY_CAPTURING_STATUS =

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/probe/LogProbe.java

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@
77
import com.datadog.debugger.agent.StringTemplateBuilder;
88
import com.datadog.debugger.el.EvaluationException;
99
import com.datadog.debugger.el.ProbeCondition;
10+
import com.datadog.debugger.el.Value;
1011
import com.datadog.debugger.el.ValueScript;
1112
import com.datadog.debugger.instrumentation.CapturedContextInstrumentor;
1213
import com.datadog.debugger.instrumentation.DiagnosticMessage;
1314
import com.datadog.debugger.instrumentation.InstrumentationResult;
1415
import com.datadog.debugger.instrumentation.MethodInfo;
1516
import com.datadog.debugger.sink.DebuggerSink;
1617
import com.datadog.debugger.sink.Snapshot;
18+
import com.datadog.debugger.util.MoshiHelper;
1719
import com.squareup.moshi.Json;
1820
import com.squareup.moshi.JsonAdapter;
1921
import com.squareup.moshi.JsonReader;
2022
import com.squareup.moshi.JsonWriter;
23+
import com.squareup.moshi.Types;
2124
import datadog.trace.api.Config;
2225
import datadog.trace.bootstrap.debugger.CapturedContext;
2326
import datadog.trace.bootstrap.debugger.DebuggerContext;
@@ -29,9 +32,12 @@
2932
import datadog.trace.bootstrap.debugger.ProbeRateLimiter;
3033
import datadog.trace.bootstrap.debugger.util.TimeoutChecker;
3134
import java.io.IOException;
35+
import java.lang.reflect.ParameterizedType;
3236
import java.time.Duration;
3337
import java.time.temporal.ChronoUnit;
38+
import java.util.ArrayList;
3439
import java.util.Arrays;
40+
import java.util.Collections;
3541
import java.util.List;
3642
import java.util.Objects;
3743
import org.slf4j.Logger;
@@ -249,6 +255,7 @@ public String toString() {
249255
private final String template;
250256
private final List<Segment> segments;
251257
private final boolean captureSnapshot;
258+
private transient List<ValueScript> watches;
252259

253260
@Json(name = "when")
254261
private final ProbeCondition probeCondition;
@@ -320,6 +327,35 @@ private LogProbe(
320327
this.sampling = sampling;
321328
}
322329

330+
private static List<ValueScript> parseWatchesFromTags(Tag[] tags) {
331+
if (tags == null || tags.length == 0) {
332+
return Collections.emptyList();
333+
}
334+
List<ValueScript> result = new ArrayList<>();
335+
for (Tag tag : tags) {
336+
if ("dd_watches_dsl".equals(tag.getKey())) {
337+
String ddWatches = tag.getValue();
338+
// this for POC only, parsing is not robust!
339+
String[] splitWatches = ddWatches.split(",");
340+
for (String watchDef : splitWatches) {
341+
// remove curly braces
342+
String refPath = watchDef.substring(1, watchDef.length() - 1);
343+
result.add(new ValueScript(ValueScript.parseRefPath(refPath), refPath));
344+
}
345+
} else if ("dd_watches_json".equals(tag.getKey())) {
346+
String json = tag.getValue();
347+
try {
348+
ParameterizedType type = Types.newParameterizedType(List.class, ValueScript.class);
349+
result.addAll(
350+
MoshiHelper.createMoshiWatches().<List<ValueScript>>adapter(type).fromJson(json));
351+
} catch (IOException e) {
352+
throw new RuntimeException(e);
353+
}
354+
}
355+
}
356+
return result;
357+
}
358+
323359
public LogProbe copy() {
324360
return new LogProbe(
325361
language,
@@ -347,6 +383,10 @@ public boolean isCaptureSnapshot() {
347383
return captureSnapshot;
348384
}
349385

386+
public List<ValueScript> getWatches() {
387+
return watches;
388+
}
389+
350390
public ProbeCondition getProbeCondition() {
351391
return probeCondition;
352392
}
@@ -390,16 +430,53 @@ public void evaluate(
390430
// sample if probe has condition and condition is true or has error
391431
sample(logStatus, methodLocation);
392432
}
393-
if (logStatus.isSampled() && logStatus.getCondition()) {
394-
StringTemplateBuilder logMessageBuilder = new StringTemplateBuilder(segments, LIMITS);
395-
String msg = logMessageBuilder.evaluate(context, logStatus);
396-
if (msg != null && msg.length() > LOG_MSG_LIMIT) {
397-
StringBuilder sb = new StringBuilder(LOG_MSG_LIMIT + 3);
398-
sb.append(msg, 0, LOG_MSG_LIMIT);
399-
sb.append("...");
400-
msg = sb.toString();
433+
processMsgTemplate(context, logStatus);
434+
processWatches(context, logStatus);
435+
}
436+
437+
private void processMsgTemplate(CapturedContext context, LogStatus logStatus) {
438+
if (!logStatus.isSampled() || !logStatus.getCondition()) {
439+
return;
440+
}
441+
StringTemplateBuilder logMessageBuilder = new StringTemplateBuilder(segments, LIMITS);
442+
String msg = logMessageBuilder.evaluate(context, logStatus);
443+
if (msg != null && msg.length() > LOG_MSG_LIMIT) {
444+
StringBuilder sb = new StringBuilder(LOG_MSG_LIMIT + 3);
445+
sb.append(msg, 0, LOG_MSG_LIMIT);
446+
sb.append("...");
447+
msg = sb.toString();
448+
}
449+
logStatus.setMessage(msg);
450+
}
451+
452+
private void processWatches(CapturedContext context, LogStatus logStatus) {
453+
if (watches == null) {
454+
watches = parseWatchesFromTags(tags);
455+
}
456+
if (watches.isEmpty()) {
457+
return;
458+
}
459+
if (!logStatus.isSampled()) {
460+
return;
461+
}
462+
for (ValueScript watch : watches) {
463+
try {
464+
Value<?> result = watch.execute(context);
465+
if (result.isUndefined()) {
466+
throw new EvaluationException("UNDEFINED", watch.getDsl());
467+
}
468+
if (result.isNull()) {
469+
context.addWatch(
470+
CapturedContext.CapturedValue.of(watch.getDsl(), Object.class.getTypeName(), null));
471+
} else {
472+
context.addWatch(
473+
CapturedContext.CapturedValue.of(
474+
watch.getDsl(), Object.class.getTypeName(), result.getValue()));
475+
}
476+
} catch (EvaluationException ex) {
477+
logStatus.addError(new EvaluationError(ex.getExpr(), ex.getMessage()));
478+
logStatus.setLogTemplateErrors(true);
401479
}
402-
logStatus.setMessage(msg);
403480
}
404481
}
405482

@@ -759,6 +836,7 @@ public static class Builder extends ProbeDefinition.Builder<Builder> {
759836
private String template;
760837
private List<Segment> segments;
761838
private boolean captureSnapshot;
839+
private List<ValueScript> watches;
762840
private ProbeCondition probeCondition;
763841
private Capture capture;
764842
private Sampling sampling;

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/MoshiHelper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,8 @@ public static JsonAdapter<Map<String, Object>> createGenericAdapter() {
5050
public static Moshi createMoshiSymbol() {
5151
return new Moshi.Builder().build();
5252
}
53+
54+
public static Moshi createMoshiWatches() {
55+
return new Moshi.Builder().add(ValueScript.class, new ValueScript.ValueScriptAdapter()).build();
56+
}
5357
}

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/MoshiSnapshotHelper.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class MoshiSnapshotHelper {
3737
public static final String CAUGHT_EXCEPTIONS = "caughtExceptions";
3838
public static final String ARGUMENTS = "arguments";
3939
public static final String LOCALS = "locals";
40+
public static final String WATCHES = "watches";
4041
public static final String THROWABLE = "throwable";
4142
public static final String STATIC_FIELDS = "staticFields";
4243
public static final String THIS = "this";
@@ -152,6 +153,20 @@ public void toJson(JsonWriter jsonWriter, CapturedContext capturedContext) throw
152153
return;
153154
}
154155
jsonWriter.beginObject();
156+
if (capturedContext.getWatches() != null) {
157+
// only watches are serialized into the snapshot
158+
jsonWriter.name(WATCHES);
159+
jsonWriter.beginObject();
160+
SerializationResult resultWatches =
161+
toJsonCapturedValues(
162+
jsonWriter,
163+
capturedContext.getWatches(),
164+
capturedContext.getLimits(),
165+
timeoutChecker);
166+
jsonWriter.endObject(); // / watches
167+
jsonWriter.endObject();
168+
return;
169+
}
155170
jsonWriter.name(ARGUMENTS);
156171
jsonWriter.beginObject();
157172
SerializationResult resultArgs =

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2422,6 +2422,36 @@ public void allProbesSameMethod() throws IOException, URISyntaxException {
24222422
}
24232423
}
24242424

2425+
@Test
2426+
public void watches() throws IOException, URISyntaxException {
2427+
final String CLASS_NAME = "CapturedSnapshot08";
2428+
LogProbe probe =
2429+
createProbeBuilder(PROBE_ID, CLASS_NAME, "doit", null, null)
2430+
.evaluateAt(MethodLocation.EXIT)
2431+
.tags(
2432+
"dd_watches_dsl:{typed.fld.fld.msg},{nullTyped.fld}",
2433+
"dd_watches_json:[{\"dsl\":\"typed.fld.fld.msg_json\",\"json\":{\"getmember\":[{\"getmember\":[{\"getmember\":[{\"ref\":\"typed\"},\"fld\"]},\"fld\"]},\"msg\"]}}]")
2434+
.build();
2435+
TestSnapshotListener listener = installProbes(CLASS_NAME, probe);
2436+
Class<?> testClass = compileAndLoadClass(CLASS_NAME);
2437+
int result = Reflect.onClass(testClass).call("main", "1").get();
2438+
assertEquals(3, result);
2439+
Snapshot snapshot = assertOneSnapshot(listener);
2440+
assertEquals(3, snapshot.getCaptures().getReturn().getWatches().size());
2441+
assertCaptureWatches(
2442+
snapshot.getCaptures().getReturn(),
2443+
"typed.fld.fld.msg",
2444+
String.class.getTypeName(),
2445+
"hello");
2446+
assertCaptureWatches(
2447+
snapshot.getCaptures().getReturn(), "nullTyped.fld", Object.class.getTypeName(), null);
2448+
assertCaptureWatches(
2449+
snapshot.getCaptures().getReturn(),
2450+
"typed.fld.fld.msg_json",
2451+
String.class.getTypeName(),
2452+
"hello");
2453+
}
2454+
24252455
private TestSnapshotListener setupInstrumentTheWorldTransformer(String excludeFileName) {
24262456
Config config = mock(Config.class);
24272457
when(config.isDebuggerEnabled()).thenReturn(true);
@@ -2655,6 +2685,13 @@ private void assertCaptureReturnValue(
26552685
}
26562686
}
26572687

2688+
private void assertCaptureWatches(
2689+
CapturedContext context, String name, String typeName, String value) {
2690+
CapturedContext.CapturedValue watch = context.getWatches().get(name);
2691+
assertEquals(typeName, watch.getType());
2692+
assertEquals(value, MoshiSnapshotTestHelper.getValue(watch));
2693+
}
2694+
26582695
private void assertCaptureThrowable(
26592696
CapturedContext context, String typeName, String message, String methodName, int lineNumber) {
26602697
CapturedContext.CapturedThrowable throwable = context.getCapturedThrowable();

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ public void deserializeLogProbes() throws Exception {
121121
ArrayList<LogProbe> logProbes = new ArrayList<>(config.getLogProbes());
122122
assertEquals(1, logProbes.size());
123123
LogProbe logProbe0 = logProbes.get(0);
124+
assertEquals(2, logProbe0.getTags().length);
125+
assertEquals("dd_watches_dsl", logProbe0.getTags()[0].getKey());
126+
assertEquals("{object.objField.intField}", logProbe0.getTags()[0].getValue());
127+
assertEquals("env", logProbe0.getTags()[1].getKey());
128+
assertEquals("staging", logProbe0.getTags()[1].getValue());
124129
assertEquals(8, logProbe0.getSegments().size());
125130
assertEquals("this is a log line customized! uuid=", logProbe0.getSegments().get(0).getStr());
126131
assertEquals("uuid", logProbe0.getSegments().get(1).getExpr());

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/SnapshotSerializationTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.datadog.debugger.util.MoshiSnapshotHelper.TRUNCATED;
2121
import static com.datadog.debugger.util.MoshiSnapshotHelper.TYPE;
2222
import static com.datadog.debugger.util.MoshiSnapshotHelper.VALUE;
23+
import static com.datadog.debugger.util.MoshiSnapshotHelper.WATCHES;
2324
import static org.junit.jupiter.api.Assertions.assertEquals;
2425
import static org.junit.jupiter.api.Assertions.assertNull;
2526
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -928,6 +929,35 @@ public void enumValues() throws IOException {
928929
assertEquals("TWO", enumValueJson.get("value"));
929930
}
930931

932+
@Test
933+
public void watches() throws IOException {
934+
JsonAdapter<Snapshot> adapter = createSnapshotAdapter();
935+
Snapshot snapshot = createSnapshot();
936+
CapturedContext context = new CapturedContext();
937+
Map<String, String> map = new HashMap<>();
938+
map.put("foo1", "bar1");
939+
map.put("foo2", "bar2");
940+
map.put("foo3", "bar3");
941+
context.addWatch(CapturedContext.CapturedValue.of("watch1", Map.class.getTypeName(), map));
942+
context.addWatch(
943+
CapturedContext.CapturedValue.of(
944+
"watch2", List.class.getTypeName(), Arrays.asList("1", "2", "3")));
945+
context.addWatch(CapturedContext.CapturedValue.of("watch3", Integer.TYPE.getTypeName(), 42));
946+
snapshot.setExit(context);
947+
String buffer = adapter.toJson(snapshot);
948+
System.out.println(buffer);
949+
Map<String, Object> json = MoshiHelper.createGenericAdapter().fromJson(buffer);
950+
Map<String, Object> capturesJson = (Map<String, Object>) json.get(CAPTURES);
951+
Map<String, Object> returnJson = (Map<String, Object>) capturesJson.get(RETURN);
952+
Map<String, Object> watches = (Map<String, Object>) returnJson.get(WATCHES);
953+
assertNull(returnJson.get(LOCALS));
954+
assertNull(returnJson.get(ARGUMENTS));
955+
assertEquals(3, watches.size());
956+
assertMapItems(watches, "watch1", "foo1", "bar1", "foo2", "bar2", "foo3", "bar3");
957+
assertArrayItem(watches, "watch2", "1", "2", "3");
958+
assertPrimitiveValue(watches, "watch3", Integer.TYPE.getTypeName(), "42");
959+
}
960+
931961
private Map<String, Object> doFieldCount(int maxFieldCount) throws IOException {
932962
JsonAdapter<Snapshot> adapter = createSnapshotAdapter();
933963
Snapshot snapshot = createSnapshotForFieldCount(maxFieldCount);

dd-java-agent/agent-debugger/src/test/resources/test_log_probe.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"created": "2021-03-31T13:26:52.519150+00:00",
1515
"active": true,
1616
"language": "java",
17+
"tags": ["dd_watches_dsl:{object.objField.intField}", "env:staging"],
1718
"where": {
1819
"typeName": "VetController",
1920
"methodName": "showVetList"

0 commit comments

Comments
 (0)