Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]

### Bugs Fixed
- A365: centralize shared request attributes in the base scope, including session and conversation IDs, channel details, and `operationSource` as `service.name`, while retaining last-write-wins `recordAttributes()` behavior. [#243](https://github.com/microsoft/opentelemetry-distro-javascript/pull/243)
- Default `InvokeAgentScope` spans to `SpanKind.INTERNAL` while preserving explicit span-kind overrides. [#241](https://github.com/microsoft/opentelemetry-distro-javascript/pull/241)

### Features Added
Expand Down
2 changes: 2 additions & 0 deletions src/a365/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ export interface Channel {
export interface Request {
/** The input content (prompt / messages) for the request. */
content?: InputMessagesParam;
/** Name of the service or framework that initiated the operation. */
operationSource?: string;
/** Identifier of the session this request belongs to. */
sessionId?: string;
/** The channel the request originated from. */
Expand Down
6 changes: 1 addition & 5 deletions src/a365/scopes/ApplyGuardrailScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class ApplyGuardrailScope extends OpenTelemetryScope {
agentDetails,
resolvedSpanDetails,
userDetails,
request,
);

// Required attributes
Expand Down Expand Up @@ -119,11 +120,6 @@ export class ApplyGuardrailScope extends OpenTelemetryScope {
if (typeof request.content === "string") {
this.setTagMaybe(OpenTelemetryConstants.SECURITY_CONTENT_INPUT_VALUE_KEY, request.content);
}
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, request.conversationId);
if (request.channel) {
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_NAME_KEY, request.channel.name);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_LINK_KEY, request.channel.description);
}
}
}

Expand Down
6 changes: 1 addition & 5 deletions src/a365/scopes/ExecuteToolScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class ExecuteToolScope extends OpenTelemetryScope {
agentDetails,
resolvedSpanDetails,
userDetails,
request,
);

const { toolName, arguments: args, toolCallId, description, toolType, endpoint } = details;
Expand All @@ -74,11 +75,6 @@ export class ExecuteToolScope extends OpenTelemetryScope {
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_TOOL_CALL_ID_KEY, toolCallId);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_TOOL_DESCRIPTION_KEY, description);

this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, request.conversationId);
this.setTagMaybe(OpenTelemetryConstants.SESSION_ID_KEY, request.sessionId);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_NAME_KEY, request.channel?.name);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_LINK_KEY, request.channel?.description);

if (endpoint) {
this.setTagMaybe(OpenTelemetryConstants.SERVER_ADDRESS_KEY, endpoint.host);
if (endpoint.port && endpoint.port !== 443) {
Expand Down
7 changes: 1 addition & 6 deletions src/a365/scopes/InferenceScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class InferenceScope extends OpenTelemetryScope {
agentDetails,
resolvedSpanDetails,
userDetails,
request,
);

// Core inference information
Expand All @@ -73,12 +74,6 @@ export class InferenceScope extends OpenTelemetryScope {
details.thoughtProcess,
);

// Conversation and channel
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, request.conversationId);
this.setTagMaybe(OpenTelemetryConstants.SESSION_ID_KEY, request.sessionId);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_NAME_KEY, request.channel?.name);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_LINK_KEY, request.channel?.description);

// Endpoint
if (details.endpoint) {
this.setTagMaybe(OpenTelemetryConstants.SERVER_ADDRESS_KEY, details.endpoint.host);
Expand Down
13 changes: 1 addition & 12 deletions src/a365/scopes/InvokeAgentScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,9 @@ export class InvokeAgentScope extends OpenTelemetryScope {
agentDetails,
resolvedSpanDetails,
callerDetails?.userDetails,
request,
);

// Session ID
this.setTagMaybe(OpenTelemetryConstants.SESSION_ID_KEY, request.sessionId);

// Endpoint
if (invokeScopeDetails.endpoint) {
this.setTagMaybe(OpenTelemetryConstants.SERVER_ADDRESS_KEY, invokeScopeDetails.endpoint.host);
Expand All @@ -84,15 +82,6 @@ export class InvokeAgentScope extends OpenTelemetryScope {
}
}

// Channel
if (request.channel) {
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_NAME_KEY, request.channel.name);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_LINK_KEY, request.channel.description);
}

// Conversation ID
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, request.conversationId);

// Request content as input messages
if (request.content != null) {
this.recordInputMessages(request.content);
Expand Down
38 changes: 27 additions & 11 deletions src/a365/scopes/OpenTelemetryScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
AgentDetails,
UserDetails,
SpanDetails,
Request,
InputMessagesParam,
OutputMessagesParam,
} from "../contracts.js";
Expand Down Expand Up @@ -59,13 +60,15 @@ export abstract class OpenTelemetryScope {
* @param agentDetails Optional agent details. Tenant ID is read from `agentDetails.tenantId`.
* @param spanDetails Optional span configuration including parent context, start/end times, span kind, and span links.
* @param userDetails Optional human caller identity details.
* @param request Optional request context shared by all scope types.
*/
protected constructor(
operationName: string,
spanName: string,
agentDetails?: AgentDetails,
spanDetails?: SpanDetails,
userDetails?: UserDetails,
request?: Request,
) {
const parentContext = spanDetails?.parentContext;
const startTime = spanDetails?.startTime;
Expand Down Expand Up @@ -101,6 +104,13 @@ export abstract class OpenTelemetryScope {
}
this.customEndTime = endTime;

// Set shared request context
this.setTagMaybe(OpenTelemetryConstants.SESSION_ID_KEY, request?.sessionId);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, request?.conversationId);
this.setTagMaybe(OpenTelemetryConstants.SERVICE_NAME_KEY, request?.operationSource);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_NAME_KEY, request?.channel?.name);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_LINK_KEY, request?.channel?.description);

// Set agent details
if (agentDetails) {
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, agentDetails.agentId);
Expand Down Expand Up @@ -165,25 +175,17 @@ export abstract class OpenTelemetryScope {
this.span.recordException(error);
}

/** Records multiple attribute key/value pairs. */
/** Records multiple attribute key/value pairs using last-write-wins semantics. */
public recordAttributes(
attributes:
Iterable<[string, AttributeValue]> | Record<string, AttributeValue> | null | undefined,
): void {
if (!attributes) return;

if (Symbol.iterator in Object(attributes) && typeof attributes !== "string") {
for (const [key, value] of attributes as Iterable<[string, AttributeValue]>) {
if (key && typeof key === "string" && key.trim()) {
this.span.setAttribute(key, value);
}
}
this.recordAttributeEntries(attributes as Iterable<[string, AttributeValue]>);
} else if (typeof attributes === "object") {
for (const key of Object.keys(attributes as Record<string, AttributeValue>)) {
if (key && key.trim()) {
this.span.setAttribute(key, (attributes as Record<string, AttributeValue>)[key]);
}
}
this.recordAttributeEntries(Object.entries(attributes as Record<string, AttributeValue>));
}
}

Expand Down Expand Up @@ -211,6 +213,20 @@ export abstract class OpenTelemetryScope {
}
}

private recordAttributeEntries(attributes: Iterable<[string, AttributeValue]>): void {
for (const [key, value] of attributes) {
if (!OpenTelemetryScope.isNonBlankAttributeKey(key)) {
continue;
}

this.span.setAttribute(key, value);
}
}

private static isNonBlankAttributeKey(key: string): boolean {
return typeof key === "string" && key.trim().length > 0;
}

/**
* Adds an event to the current span.
* @param name The event name
Expand Down
7 changes: 1 addition & 6 deletions src/a365/scopes/OutputScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,10 @@ export class OutputScope extends OpenTelemetryScope {
agentDetails,
resolvedSpanDetails,
userDetails,
request,
);

// Set initial output messages
this._setOutput(response.messages);

// Conversation and channel
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, request.conversationId);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_NAME_KEY, request.channel?.name);
this.setTagMaybe(OpenTelemetryConstants.CHANNEL_LINK_KEY, request.channel?.description);
}

/**
Expand Down
27 changes: 27 additions & 0 deletions test/internal/unit/a365/applyGuardrailScope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@ describe("ApplyGuardrailScope", () => {
expect(getFinishedSpan().kind).toBe(SpanKind.CLIENT);
});

it("should write shared request context attributes", () => {
const scope = ApplyGuardrailScope.start(
{ targetType: GuardrailTargetType.LlmInput, decisionType: GuardrailDecisionType.Allow },
testAgentDetails,
{
content: "Guard this prompt",
conversationId: "guardrail-conversation",
sessionId: "guardrail-session",
operationSource: "guardrail-host",
channel: { name: "Teams", description: "https://teams.example" },
},
);
scope.dispose();

const attributes = getFinishedSpan().attributes;
expect(attributes[OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY]).toBe(
"guardrail-conversation",
);
expect(attributes[OpenTelemetryConstants.SESSION_ID_KEY]).toBe("guardrail-session");
expect(attributes[OpenTelemetryConstants.SERVICE_NAME_KEY]).toBe("guardrail-host");
expect(attributes[OpenTelemetryConstants.CHANNEL_NAME_KEY]).toBe("Teams");
expect(attributes[OpenTelemetryConstants.CHANNEL_LINK_KEY]).toBe("https://teams.example");
expect(attributes[OpenTelemetryConstants.SECURITY_CONTENT_INPUT_VALUE_KEY]).toBe(
"Guard this prompt",
);
});

it("should throw when agentDetails.tenantId is missing", () => {
expect(() =>
ApplyGuardrailScope.start(
Expand Down
Loading
Loading