Skip to content

Commit 036f183

Browse files
Add telemetry to AI Guard evaluations
1 parent d86fe55 commit 036f183

File tree

4 files changed

+169
-25
lines changed

4 files changed

+169
-25
lines changed

dd-java-agent/agent-aiguard/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies {
1818
implementation libs.okhttp
1919

2020
api project(':dd-trace-api')
21+
api project(':utils:version-utils')
2122
implementation project(':internal-api')
2223
implementation project(':communication')
2324

dd-java-agent/agent-aiguard/src/main/java/com/datadog/aiguard/AIGuardInternal.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.datadog.aiguard;
22

3+
import static datadog.trace.api.telemetry.WafMetricCollector.AIGuardTruncationType.CONTENT;
4+
import static datadog.trace.api.telemetry.WafMetricCollector.AIGuardTruncationType.MESSAGES;
35
import static datadog.trace.util.Strings.isBlank;
46
import static java.util.Collections.singletonMap;
57

@@ -8,6 +10,7 @@
810
import com.squareup.moshi.JsonWriter;
911
import com.squareup.moshi.Moshi;
1012
import com.squareup.moshi.Types;
13+
import datadog.common.version.VersionInfo;
1114
import datadog.communication.http.OkHttpUtils;
1215
import datadog.trace.api.Config;
1316
import datadog.trace.api.aiguard.AIGuard;
@@ -21,6 +24,7 @@
2124
import datadog.trace.api.aiguard.AIGuard.ToolCall.Function;
2225
import datadog.trace.api.aiguard.Evaluator;
2326
import datadog.trace.api.aiguard.noop.NoOpEvaluator;
27+
import datadog.trace.api.telemetry.WafMetricCollector;
2428
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
2529
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
2630
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
@@ -79,7 +83,18 @@ public static void install() {
7983
if (isBlank(endpoint)) {
8084
endpoint = String.format("https://app.%s/api/v2/ai-guard", config.getSite());
8185
}
82-
final Map<String, String> headers = mapOf("DD-API-KEY", apiKey, "DD-APPLICATION-KEY", appKey);
86+
final Map<String, String> headers =
87+
mapOf(
88+
"DD-API-KEY",
89+
apiKey,
90+
"DD-APPLICATION-KEY",
91+
appKey,
92+
"DD-AI-GUARD-VERSION",
93+
VersionInfo.VERSION,
94+
"DD-AI-GUARD-SOURCE",
95+
"SDK",
96+
"DD-AI-GUARD-LANGUAGE",
97+
"jvm");
8398
final HttpUrl url = HttpUrl.get(endpoint).newBuilder().addPathSegment("evaluate").build();
8499
final int timeout = config.getAiGuardTimeout();
85100
final OkHttpClient client = buildClient(url, timeout);
@@ -113,12 +128,17 @@ static void uninstall() {
113128
private static List<Message> messagesForMetaStruct(List<Message> messages) {
114129
final Config config = Config.get();
115130
final int size = Math.min(messages.size(), config.getAiGuardMaxMessagesLength());
131+
if (size < messages.size()) {
132+
WafMetricCollector.get().aiGuardTruncated(MESSAGES);
133+
}
116134
final List<Message> result = new ArrayList<>(size);
117135
final int maxContent = config.getAiGuardMaxContentSize();
136+
boolean contentTruncated = false;
118137
for (int i = 0; i < size; i++) {
119138
Message source = messages.get(i);
120139
final String content = source.getContent();
121140
if (content != null && content.length() > maxContent) {
141+
contentTruncated = true;
122142
source =
123143
new Message(
124144
source.getRole(),
@@ -128,6 +148,9 @@ private static List<Message> messagesForMetaStruct(List<Message> messages) {
128148
}
129149
result.add(source);
130150
}
151+
if (contentTruncated) {
152+
WafMetricCollector.get().aiGuardTruncated(CONTENT);
153+
}
131154
return result;
132155
}
133156

@@ -203,20 +226,27 @@ public Evaluation evaluate(final List<Message> messages, final Options options)
203226
final String reason = (String) result.get("reason");
204227
span.setTag(ACTION_TAG, action);
205228
span.setTag(REASON_TAG, reason);
206-
final boolean blockingEnabled =
207-
isBlockingEnabled(options, result.get("is_blocking_enabled"));
208-
if (blockingEnabled && action != Action.ALLOW) {
229+
final boolean shouldBlock =
230+
isBlockingEnabled(options, result.get("is_blocking_enabled")) && action != Action.ALLOW;
231+
WafMetricCollector.get().aiGuardRequest(action, shouldBlock);
232+
if (shouldBlock) {
209233
span.setTag(BLOCKED_TAG, true);
210234
throw new AIGuardAbortError(action, reason);
211235
}
212236
return new Evaluation(action, reason);
213237
}
214-
} catch (AIGuardAbortError | AIGuardClientError e) {
238+
} catch (AIGuardAbortError e) {
239+
span.addThrowable(e);
240+
throw e;
241+
} catch (AIGuardClientError e) {
242+
WafMetricCollector.get().aiGuardError();
215243
span.addThrowable(e);
216244
throw e;
217245
} catch (final Exception e) {
246+
WafMetricCollector.get().aiGuardError();
218247
final AIGuardClientError error =
219-
new AIGuardClientError("AI Guard service returned unexpected response", e);
248+
new AIGuardClientError(
249+
"AI Guard service returned unexpected response: " + e.getMessage(), e);
220250
span.addThrowable(error);
221251
throw error;
222252
} finally {
@@ -248,11 +278,12 @@ private static OkHttpClient buildClient(final HttpUrl url, final long timeout) {
248278
return OkHttpUtils.buildHttpClient(url, timeout).newBuilder().build();
249279
}
250280

251-
private static Map<String, String> mapOf(
252-
final String key1, final String prop1, final String key2, final String prop2) {
253-
final Map<String, String> map = new HashMap<>(2);
254-
map.put(key1, prop1);
255-
map.put(key2, prop2);
281+
private static Map<String, String> mapOf(final String... props) {
282+
final Map<String, String> map = new HashMap<>(props.length << 1);
283+
int index = 0;
284+
while (index < props.length) {
285+
map.put(props[index++], props[index++]);
286+
}
256287
return map;
257288
}
258289

dd-java-agent/agent-aiguard/src/test/groovy/com/datadog/aiguard/AIGuardInternalTests.groovy

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import com.fasterxml.jackson.annotation.JsonInclude
44
import com.fasterxml.jackson.databind.ObjectMapper
55
import com.fasterxml.jackson.databind.PropertyNamingStrategies
66
import com.squareup.moshi.Moshi
7+
import datadog.common.version.VersionInfo
78
import datadog.trace.api.Config
89
import datadog.trace.api.aiguard.AIGuard
10+
import datadog.trace.api.telemetry.WafMetricCollector
911
import datadog.trace.bootstrap.instrumentation.api.AgentSpan
1012
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
1113
import datadog.trace.test.util.DDSpecification
@@ -35,7 +37,11 @@ class AIGuardInternalTests extends DDSpecification {
3537
protected static final URL = HttpUrl.parse('https://app.datadoghq.com/api/v2/ai-guard/evaluate')
3638

3739
@Shared
38-
protected static final HEADERS = ['DD-API-KEY': 'api', 'DD-APPLICATION-KEY': 'app']
40+
protected static final HEADERS = ['DD-API-KEY': 'api',
41+
'DD-APPLICATION-KEY': 'app',
42+
'DD-AI-GUARD-VERSION': VersionInfo.VERSION,
43+
'DD-AI-GUARD-SOURCE': 'SDK',
44+
'DD-AI-GUARD-LANGUAGE': 'jvm']
3945

4046
@Shared
4147
protected static final ORIGINAL_TRACER = AgentTracer.get()
@@ -79,6 +85,11 @@ class AIGuardInternalTests extends DDSpecification {
7985
buildSpan(_ as String, _ as String) >> builder
8086
}
8187
AgentTracer.forceRegister(tracer)
88+
89+
WafMetricCollector.get().tap {
90+
prepareMetrics()
91+
drain()
92+
}
8293
}
8394

8495
void cleanup() {
@@ -193,6 +204,7 @@ class AIGuardInternalTests extends DDSpecification {
193204
eval.action == suite.action
194205
eval.reason == suite.reason
195206
}
207+
assertTelemetry("ai_guard.requests", "action:$suite.action", "block:$throwAbortError")
196208

197209
where:
198210
suite << TestSuite.build()
@@ -222,6 +234,7 @@ class AIGuardInternalTests extends DDSpecification {
222234
final exception = thrown(AIGuard.AIGuardClientError)
223235
exception.errors == errors
224236
1 * span.addThrowable(_ as AIGuard.AIGuardClientError)
237+
assertTelemetry("ai_guard.requests", "error:true")
225238
}
226239

227240
void 'test evaluate with invalid JSON'() {
@@ -246,6 +259,7 @@ class AIGuardInternalTests extends DDSpecification {
246259
then:
247260
thrown(AIGuard.AIGuardClientError)
248261
1 * span.addThrowable(_ as AIGuard.AIGuardClientError)
262+
assertTelemetry("ai_guard.requests", "error:true")
249263
}
250264

251265
void 'test evaluate with missing action'() {
@@ -270,6 +284,7 @@ class AIGuardInternalTests extends DDSpecification {
270284
then:
271285
thrown(AIGuard.AIGuardClientError)
272286
1 * span.addThrowable(_ as AIGuard.AIGuardClientError)
287+
assertTelemetry("ai_guard.requests", "error:true")
273288
}
274289

275290
void 'test evaluate with non JSON response'() {
@@ -294,6 +309,7 @@ class AIGuardInternalTests extends DDSpecification {
294309
then:
295310
thrown(AIGuard.AIGuardClientError)
296311
1 * span.addThrowable(_ as AIGuard.AIGuardClientError)
312+
assertTelemetry("ai_guard.requests", "error:true")
297313
}
298314

299315
void 'test evaluate with empty response'() {
@@ -318,6 +334,7 @@ class AIGuardInternalTests extends DDSpecification {
318334
then:
319335
thrown(AIGuard.AIGuardClientError)
320336
1 * span.addThrowable(_ as AIGuard.AIGuardClientError)
337+
assertTelemetry("ai_guard.requests", "error:true")
321338
}
322339

323340
void 'test message length truncation'() {
@@ -349,6 +366,7 @@ class AIGuardInternalTests extends DDSpecification {
349366
assert received.size() == maxMessages
350367
assert received.size() < messages.size()
351368
}
369+
assertTelemetry("ai_guard.truncated", "type:messages")
352370
}
353371

354372
void 'test message content truncation'() {
@@ -380,6 +398,7 @@ class AIGuardInternalTests extends DDSpecification {
380398
assert it.content.length() < message.content.length()
381399
}
382400
}
401+
assertTelemetry("ai_guard.truncated", "type:content")
383402
}
384403

385404
void 'test no messages'() {
@@ -425,6 +444,21 @@ class AIGuardInternalTests extends DDSpecification {
425444
0 * span.setTag(AIGuardInternal.TOOL_TAG, _)
426445
}
427446

447+
private static assertTelemetry(final String metric, final String...tags) {
448+
final metrics = WafMetricCollector.get().with {
449+
prepareMetrics()
450+
drain()
451+
}
452+
final filtered = metrics.findAll {
453+
it.namespace == 'appsec'
454+
&& it.metricName == metric
455+
&& it.tags == tags.toList()
456+
}
457+
assert filtered.size() == 1 : metrics
458+
assert filtered*.value.sum() == 1
459+
return true
460+
}
461+
428462
private static assertRequest(final Request request, final List<AIGuard.Message> messages) {
429463
assert request.url() == URL
430464
assert request.method() == 'POST'
@@ -452,12 +486,12 @@ class AIGuardInternalTests extends DDSpecification {
452486

453487
private static Response mockResponse(final Request request, final int status, final Object body) {
454488
return new Response.Builder()
455-
.protocol(Protocol.HTTP_1_1)
456-
.message('ok')
457-
.request(request)
458-
.code(status)
459-
.body(body == null ? null : ResponseBody.create(MediaType.parse('application/json'), MOSHI.adapter(Object).toJson(body)))
460-
.build()
489+
.protocol(Protocol.HTTP_1_1)
490+
.message('ok')
491+
.request(request)
492+
.code(status)
493+
.body(body == null ? null : ResponseBody.create(MediaType.parse('application/json'), MOSHI.adapter(Object).toJson(body)))
494+
.build()
461495
}
462496

463497
private static class TestSuite {
@@ -495,13 +529,13 @@ class AIGuardInternalTests extends DDSpecification {
495529
@Override
496530
String toString() {
497531
return "TestSuite{" +
498-
"description='" + description + '\'' +
499-
", action=" + action +
500-
", reason='" + reason + '\'' +
501-
", blocking=" + blocking +
502-
", target='" + target + '\'' +
503-
", messages=" + messages +
504-
'}'
532+
"description='" + description + '\'' +
533+
", action=" + action +
534+
", reason='" + reason + '\'' +
535+
", blocking=" + blocking +
536+
", target='" + target + '\'' +
537+
", messages=" + messages +
538+
'}'
505539
}
506540
}
507541
}

0 commit comments

Comments
 (0)