Skip to content

feat(opentelemetry): add structured protocol logs - #1252

Open
ehsavoie wants to merge 1 commit into
a2aproject:mainfrom
ehsavoie:issue_1221
Open

ehsavoie wants to merge 1 commit into
a2aproject:mainfrom
ehsavoie:issue_1221

Conversation

@ehsavoie

Copy link
Copy Markdown
Collaborator

Opt-in client and server events cover requests, streams, transport selection, and push notifications, with trace correlation across async callbacks. Payload capture is separately opt-in with configurable masking and size limits.

This fixes #1221 🦕

Opt-in client and server events cover requests, streams, transport
selection, and push notifications, with trace correlation across async
callbacks. Payload capture is separately opt-in with configurable
masking and size limits.

This fixes a2aproject#1221

Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
@ehsavoie
ehsavoie requested a review from kabir October 29, 2026 14:30
@ehsavoie

Copy link
Copy Markdown
Collaborator Author

@brunobat May you take a look

@brunobat brunobat left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ehsavoie Did my best. This is a very big PR.
Hope it helps.

public interface A2AObservabilityNames {

// Protocol log events
String A2A_EVENT_AUTHORIZATION = "a2a.authorization";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't they all be final?

if (!budget.consume(inputBytes)) {
return "";
}
String sanitized =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sanitized runs after truncation which might prevent proper sanitation of chopped data that should have been picked up by the Sanitizer... It's probably an academic possibility with the a custom ProtocolPayloadSanitizer.

TaskPushNotificationConfig result = delegate.onCreateTaskPushNotificationConfig(params, context);
logProtocolMetadata(A2A_EVENT_NOTIFICATION_SUBSCRIPTION,
A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, A2A_LOG_DIRECTION_OUTBOUND,
A2A_KIND_CREATED, params);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are not logging anything about the result?

Span closingSpan = config.tracer().spanBuilder(config.spanName() + STREAMING_END_SUFFIX)
.setSpanKind(SpanKind.SERVER)
.addLink(config.parentSpanContext())
.setParent(config.context())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this on purpose?
A long SSE stream puts every event, possibly hours later, into one huge trace under a parent that closed long before.

.setAttribute(GENAI_STREAMING_EVENT, item.kind());
StreamingEventAttributes.apply(builder, item);
Span span = builder.startSpan();
try (Scope ignored = span.makeCurrent()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this on purpose?

String A2A_PAYLOAD_OMITTED = "a2a.payload.omitted";
String A2A_PAYLOAD_PROJECTED_SIZE = "a2a.payload.projected_size";
String A2A_PAYLOAD_TRUNCATED = "a2a.payload.truncated";
String A2A_TASK_ID = "a2a.task.id";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new logs use a2a.task.id, a2a.context.id, a2a.message.id, a2a.operation.name, ... The spans in the same module use gen_ai.agent.a2a.task_id, gen_ai.agent.a2a.context_id, gen_ai.agent.a2a.config_id and so on.
Semantic conventions expect one attribute name per concept, independently of the signal.

String event,
String operation,
String direction,
@Nullable String kind,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind MUST NOT be a string but an enum, otherwise all kinds of things from different domains can become a kind.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They must align with tracing.

logDelivery(A2A_KIND_ERROR, event);
}
String desc = t.getMessage() != null ? t.getMessage() : t.getClass().getName();
span.setAttribute(ERROR_TYPE, desc);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error type must be low carnality. It must not be a message.

Logger logger, @Nullable ProtocolPayloadFormatter payloadFormatter, Severity severity) {
this.logger = Assert.checkNotNullParam("logger", logger);
this.payloadFormatter = payloadFormatter;
this.severity = Assert.checkNotNullParam("severity", severity);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that you want to decide the severity in the constructor?
The severity is chosen once, when the emitter is built, and every record uses it whatever the event or outcome.

.setSeverityText(severity.name())
.setEventName(event)
.setBody(event)
.setAttribute(A2AObservabilityNames.A2A_EVENT_NAME, event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure you need to set the event in 3 different fields?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No structured payload logging

2 participants