Skip to content

Commit f496a44

Browse files
feat(logging): add structured payload logging with masking and truncation
Provide structured DEBUG-level log events for A2A protocol messages including task requests, state changes, artifacts, negotiation payloads, authorization decisions, and notification subscriptions/deliveries. - Add A2APayloadLoggingConfig with system property support (enabled, max-size default 4KB, masking toggle, custom strategy/sensitive-keys) - Add PayloadMaskingStrategy interface with Default (recursive JSON traversal, regex fallback) and NoOp implementations; default masks token/credentials/password/secret/url etc. case-insensitively - Add PayloadTruncator with [truncated] marker - Add TraceCorrelation via OpenTelemetry reflection + MDC for trace_id/ span_id log correlation - Add StructuredPayloadLogger with fluent SLF4J keyValue fields and convenience methods for task/auth/push/negotiation events - Integrate server-side logging in DefaultRequestHandler (requests, results, auth, push subs), AgentEmitter (state/artifact/message), TaskManager (persisted state/artifact), BasePushNotificationSender (delivery) - Integrate client logging in JSONRPCTransport (requests/results, streaming, push, negotiation) and OpenTelemetry decorators - Add defaults to a2a-defaults.properties and documentation - Add unit tests for truncation, masking, config and structured logger Fixes #1221 Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
1 parent 86a0eb3 commit f496a44

30 files changed

Lines changed: 2389 additions & 18 deletions

File tree

‎client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java‎

Lines changed: 66 additions & 10 deletions
Large diffs are not rendered by default.

‎common/pom.xml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,34 @@
2121
<groupId>org.slf4j</groupId>
2222
<artifactId>slf4j-api</artifactId>
2323
</dependency>
24+
<dependency>
25+
<groupId>org.jspecify</groupId>
26+
<artifactId>jspecify</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>com.google.code.gson</groupId>
30+
<artifactId>gson</artifactId>
31+
</dependency>
2432
<dependency>
2533
<groupId>org.junit.jupiter</groupId>
2634
<artifactId>junit-jupiter-api</artifactId>
2735
<scope>test</scope>
2836
</dependency>
37+
<dependency>
38+
<groupId>org.junit.jupiter</groupId>
39+
<artifactId>junit-jupiter-params</artifactId>
40+
<scope>test</scope>
41+
</dependency>
42+
<dependency>
43+
<groupId>ch.qos.logback</groupId>
44+
<artifactId>logback-classic</artifactId>
45+
<scope>test</scope>
46+
</dependency>
47+
<dependency>
48+
<groupId>org.mockito</groupId>
49+
<artifactId>mockito-core</artifactId>
50+
<scope>test</scope>
51+
</dependency>
2952
</dependencies>
3053

3154
</project>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.a2aproject.sdk.common.logging;
2+
3+
/**
4+
* Enumeration of A2A protocol payload event types for structured logging.
5+
* <p>
6+
* Each event type corresponds to a category of protocol message that can be
7+
* logged with structured fields for observability and debugging.
8+
* </p>
9+
*/
10+
public enum A2AEventType {
11+
/**
12+
* Task request: message/send, message/stream, getTask, listTasks, cancelTask, etc.
13+
*/
14+
TASK_REQUEST("task.request"),
15+
16+
/**
17+
* Task state change: TaskStatusUpdateEvent and status transitions.
18+
*/
19+
STATE_CHANGE("task.state_change"),
20+
21+
/**
22+
* Result artifact: TaskArtifactUpdateEvent and artifact payloads.
23+
*/
24+
ARTIFACT("task.artifact"),
25+
26+
/**
27+
* Task result: final Task snapshot returned to client.
28+
*/
29+
TASK_RESULT("task.result"),
30+
31+
/**
32+
* Negotiation payload: extension negotiation, version negotiation, AgentCard capabilities.
33+
*/
34+
NEGOTIATION("negotiation"),
35+
36+
/**
37+
* Authorization decision: allow/deny from TaskAuthorizationProvider.
38+
*/
39+
AUTHORIZATION("authorization"),
40+
41+
/**
42+
* Notification subscription: push config create/get/list/delete, subscribeToTask.
43+
*/
44+
NOTIFICATION_SUBSCRIPTION("notification.subscription"),
45+
46+
/**
47+
* Notification delivery: push webhook dispatch to external URL.
48+
*/
49+
NOTIFICATION_DELIVERY("notification.delivery"),
50+
51+
/**
52+
* Streaming event: per-event SSE streaming payload.
53+
*/
54+
STREAMING_EVENT("streaming.event"),
55+
56+
/**
57+
* Generic payload for any other structured log.
58+
*/
59+
GENERIC("generic");
60+
61+
private final String value;
62+
63+
A2AEventType(String value) {
64+
this.value = value;
65+
}
66+
67+
public String value() {
68+
return value;
69+
}
70+
71+
@Override
72+
public String toString() {
73+
return value;
74+
}
75+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package org.a2aproject.sdk.common.logging;
2+
3+
import java.util.Set;
4+
5+
import org.jspecify.annotations.Nullable;
6+
7+
/**
8+
* Configuration for structured payload logging.
9+
* <p>
10+
* Supports both system properties and programmatic configuration via builder.
11+
* </p>
12+
* <h2>System Properties</h2>
13+
* <ul>
14+
* <li>{@code a2a.logging.payload.enabled} (boolean, default false) — enable payload logging</li>
15+
* <li>{@code a2a.logging.payload.max-size} (int, default 4096) — max payload size before truncation</li>
16+
* <li>{@code a2a.logging.payload.masking.enabled} (boolean, default true) — enable sensitive masking</li>
17+
* <li>{@code a2a.logging.payload.masking.strategy} (class name) — custom masking strategy</li>
18+
* </ul>
19+
*/
20+
public final class A2APayloadLoggingConfig {
21+
22+
public static final String ENABLED_PROPERTY = "a2a.logging.payload.enabled";
23+
public static final String MAX_SIZE_PROPERTY = "a2a.logging.payload.max-size";
24+
public static final String MASKING_ENABLED_PROPERTY = "a2a.logging.payload.masking.enabled";
25+
public static final String MASKING_STRATEGY_PROPERTY = "a2a.logging.payload.masking.strategy";
26+
public static final String MASKING_SENSITIVE_KEYS_PROPERTY = "a2a.logging.payload.masking.sensitive-keys";
27+
public static final int DEFAULT_MAX_SIZE = 4096;
28+
29+
private final boolean enabled;
30+
private final int maxSize;
31+
private final boolean maskingEnabled;
32+
private final @Nullable PayloadMaskingStrategy maskingStrategy;
33+
private final @Nullable Set<String> sensitiveKeys;
34+
35+
private A2APayloadLoggingConfig(Builder builder) {
36+
this.enabled = builder.enabled;
37+
this.maxSize = builder.maxSize;
38+
this.maskingEnabled = builder.maskingEnabled;
39+
this.maskingStrategy = builder.maskingStrategy;
40+
this.sensitiveKeys = builder.sensitiveKeys;
41+
}
42+
43+
public boolean isEnabled() {
44+
return enabled;
45+
}
46+
47+
public int getMaxSize() {
48+
return maxSize;
49+
}
50+
51+
public boolean isMaskingEnabled() {
52+
return maskingEnabled;
53+
}
54+
55+
public @Nullable PayloadMaskingStrategy getMaskingStrategy() {
56+
return maskingStrategy;
57+
}
58+
59+
public @Nullable Set<String> getSensitiveKeys() {
60+
return sensitiveKeys;
61+
}
62+
63+
/**
64+
* Creates config from system properties.
65+
*/
66+
public static A2APayloadLoggingConfig fromSystemProperties() {
67+
Builder builder = new Builder();
68+
String enabledProp = System.getProperty(ENABLED_PROPERTY);
69+
if (enabledProp != null) {
70+
builder.enabled(Boolean.parseBoolean(enabledProp));
71+
}
72+
String maxSizeProp = System.getProperty(MAX_SIZE_PROPERTY);
73+
if (maxSizeProp != null) {
74+
try {
75+
builder.maxSize(Integer.parseInt(maxSizeProp.trim()));
76+
} catch (NumberFormatException ignored) {
77+
// use default
78+
}
79+
}
80+
String maskingEnabledProp = System.getProperty(MASKING_ENABLED_PROPERTY);
81+
if (maskingEnabledProp != null) {
82+
builder.maskingEnabled(Boolean.parseBoolean(maskingEnabledProp));
83+
}
84+
// Strategy loaded separately by logger via ServiceLoader / reflection;
85+
// we don't instantiate here to avoid class loading issues in fromSystemProperties
86+
String strategyClass = System.getProperty(MASKING_STRATEGY_PROPERTY);
87+
if (strategyClass != null && !strategyClass.isBlank()) {
88+
try {
89+
Class<?> clazz = Class.forName(strategyClass.trim());
90+
Object instance = clazz.getDeclaredConstructor().newInstance();
91+
if (instance instanceof PayloadMaskingStrategy pms) {
92+
builder.maskingStrategy(pms);
93+
}
94+
} catch (Exception ignored) {
95+
}
96+
}
97+
String sensitiveKeysProp = System.getProperty(MASKING_SENSITIVE_KEYS_PROPERTY);
98+
if (sensitiveKeysProp != null && !sensitiveKeysProp.isBlank()) {
99+
Set<String> keys = Set.of(sensitiveKeysProp.split(","));
100+
// Trim each key
101+
Set<String> trimmed = new java.util.HashSet<>();
102+
for (String k : keys) {
103+
if (k != null && !k.isBlank()) {
104+
trimmed.add(k.trim());
105+
}
106+
}
107+
if (!trimmed.isEmpty()) {
108+
builder.sensitiveKeys(trimmed);
109+
}
110+
}
111+
return builder.build();
112+
}
113+
114+
public Builder toBuilder() {
115+
Builder b = new Builder();
116+
b.enabled = this.enabled;
117+
b.maxSize = this.maxSize;
118+
b.maskingEnabled = this.maskingEnabled;
119+
b.maskingStrategy = this.maskingStrategy;
120+
b.sensitiveKeys = this.sensitiveKeys;
121+
return b;
122+
}
123+
124+
public static Builder builder() {
125+
return new Builder();
126+
}
127+
128+
public static final class Builder {
129+
private boolean enabled = false;
130+
private int maxSize = DEFAULT_MAX_SIZE;
131+
private boolean maskingEnabled = true;
132+
private @Nullable PayloadMaskingStrategy maskingStrategy = null;
133+
private @Nullable Set<String> sensitiveKeys = null;
134+
135+
public Builder enabled(boolean enabled) {
136+
this.enabled = enabled;
137+
return this;
138+
}
139+
140+
public Builder maxSize(int maxSize) {
141+
this.maxSize = maxSize;
142+
return this;
143+
}
144+
145+
public Builder maskingEnabled(boolean maskingEnabled) {
146+
this.maskingEnabled = maskingEnabled;
147+
return this;
148+
}
149+
150+
public Builder maskingStrategy(@Nullable PayloadMaskingStrategy maskingStrategy) {
151+
this.maskingStrategy = maskingStrategy;
152+
return this;
153+
}
154+
155+
public Builder sensitiveKeys(@Nullable Set<String> sensitiveKeys) {
156+
this.sensitiveKeys = sensitiveKeys;
157+
return this;
158+
}
159+
160+
public A2APayloadLoggingConfig build() {
161+
return new A2APayloadLoggingConfig(this);
162+
}
163+
}
164+
165+
@Override
166+
public String toString() {
167+
return "A2APayloadLoggingConfig{"
168+
+ "enabled=" + enabled
169+
+ ", maxSize=" + maxSize
170+
+ ", maskingEnabled=" + maskingEnabled
171+
+ ", maskingStrategy=" + (maskingStrategy != null ? maskingStrategy.getClass().getName() : "default")
172+
+ '}';
173+
}
174+
}

0 commit comments

Comments
 (0)