Skip to content
Merged
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
4 changes: 3 additions & 1 deletion src/Observability/Runtime/Common/ExportFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.Agents.A365.Observability.Runtime.DTOs;
using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand Down Expand Up @@ -146,7 +147,8 @@ public string FormatLogData(IDictionary<string, object?> data)
StartTimeUnixNano = data.TryGetValue("StartTime", out var startTimeObj) && startTimeObj != null ? ToUnixNanos(((DateTimeOffset)startTimeObj).UtcDateTime) : 0,
EndTimeUnixNano = data.TryGetValue("EndTime", out var endTimeObj) && endTimeObj != null ? ToUnixNanos(((DateTimeOffset)endTimeObj).UtcDateTime) : 0,
SpanId = data["SpanId"],
ParentSpanId = data["ParentSpanId"]
ParentSpanId = data["ParentSpanId"],
Kind = data.TryGetValue("SpanKind", out var spanKindObj) && spanKindObj != null ? spanKindObj : SpanKindConstants.Client
};

return SerializePayload(payload);
Expand Down
11 changes: 10 additions & 1 deletion src/Observability/Runtime/DTOs/BaseData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@ public abstract class BaseData
/// <param name="endTime">Optional custom end time for the operation.</param>
/// <param name="spanId">Optional span ID for the operation. If not provided one will be created.</param>
/// <param name="parentSpanId">Optional parent span ID for distributed tracing.</param>
/// <param name="spanKind">Optional span kind for the operation. Use <see cref="SpanKindConstants"/> values (e.g., <see cref="SpanKindConstants.Client"/>, <see cref="SpanKindConstants.Server"/>, <see cref="SpanKindConstants.Internal"/>).</param>
public BaseData(
IDictionary<string, object?>? attributes = null,
DateTimeOffset? startTime = null,
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null)
string? parentSpanId = null,
string? spanKind = null)
{
Attributes = attributes ?? new Dictionary<string, object?>();
StartTime = startTime;
EndTime = endTime;
// Generate a random span ID if not provided. Use ActivitySpanId for consistency with tracing.
SpanId = spanId ?? ActivitySpanId.CreateRandom().ToString();
ParentSpanId = parentSpanId;
SpanKind = spanKind;
}

/// <summary>
Expand Down Expand Up @@ -64,6 +67,11 @@ public BaseData(
/// </summary>
public string? ParentSpanId { get; }

/// <summary>
/// Gets the span kind for the operation, if provided. See <see cref="SpanKindConstants"/> for valid values.
/// </summary>
public string? SpanKind { get; }

/// <summary>
/// Gets the duration of the operation if both start and end times are provided.
/// </summary>
Expand All @@ -84,6 +92,7 @@ public BaseData(
{ "EndTime", EndTime },
{ "SpanId", SpanId },
{ "ParentSpanId", ParentSpanId },
{ "SpanKind", SpanKind },
{ "Duration", Duration }
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class ExecuteToolDataBuilder : BaseDataBuilder<ExecuteToolData>
/// <param name="sourceMetadata">Optional source metadata for the operation.</param>
/// <param name="callerDetails">Optional details about the non-agentic caller.</param>
/// <param name="extraAttributes">Optional dictionary of extra attributes.</param>
/// <param name="spanKind">Optional span kind override. Use <see cref="SpanKindConstants.Internal"/> or <see cref="SpanKindConstants.Client"/> as appropriate.</param>
/// <returns>An ExecuteToolData object containing all telemetry data.</returns>
public static ExecuteToolData Build(
ToolCallDetails toolCallDetails,
Expand All @@ -44,11 +45,12 @@ public static ExecuteToolData Build(
string? parentSpanId = null,
SourceMetadata? sourceMetadata = null,
CallerDetails? callerDetails = null,
IDictionary<string, object?>? extraAttributes = null)
IDictionary<string, object?>? extraAttributes = null,
string? spanKind = null)
{
var attributes = BuildAttributes(toolCallDetails, agentDetails, tenantDetails, conversationId, responseContent, sourceMetadata, callerDetails, extraAttributes);

return new ExecuteToolData(attributes, startTime, endTime, spanId, parentSpanId);
return new ExecuteToolData(attributes, startTime, endTime, spanId, parentSpanId, spanKind);
}

private static Dictionary<string, object?> BuildAttributes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class InvokeAgentDataBuilder : BaseDataBuilder<InvokeAgentData>
/// <param name="spanId">Optional span ID for the operation.</param>
/// <param name="parentSpanId">Optional parent span ID for distributed tracing.</param>
/// <param name="extraAttributes">Optional dictionary of extra attributes.</param>
/// <param name="spanKind">Optional span kind override. Use <see cref="SpanKindConstants.Client"/> or <see cref="SpanKindConstants.Server"/> as appropriate.</param>
/// <returns>An InvokeAgentData object containing all telemetry data.</returns>
public static InvokeAgentData Build(
InvokeAgentDetails invokeAgentDetails,
Expand All @@ -45,7 +46,8 @@ public static InvokeAgentData Build(
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null,
IDictionary<string, object?>? extraAttributes = null)
IDictionary<string, object?>? extraAttributes = null,
string? spanKind = null)
{
var attributes = BuildAttributes(
invokeAgentDetails,
Expand All @@ -63,7 +65,8 @@ public static InvokeAgentData Build(
startTime,
endTime,
spanId,
parentSpanId);
parentSpanId,
spanKind);
}

/// <summary>
Expand Down
6 changes: 4 additions & 2 deletions src/Observability/Runtime/DTOs/ExecuteToolData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ public class ExecuteToolData : BaseData
/// <param name="endTime">Optional custom end time for the operation.</param>
/// <param name="spanId">Optional span ID for the operation. If not provided one will be created.</param>
/// <param name="parentSpanId">Optional parent span ID for distributed tracing.</param>
/// <param name="spanKind">Optional span kind override. Defaults to <c>null</c> (unset). Use <see cref="SpanKindConstants.Internal"/> or <see cref="SpanKindConstants.Client"/> as appropriate.</param>
public ExecuteToolData(
IDictionary<string, object?>? attributes = null,
DateTimeOffset? startTime = null,
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null)
: base(attributes, startTime, endTime, spanId, parentSpanId)
string? parentSpanId = null,
string? spanKind = null)
: base(attributes, startTime, endTime, spanId, parentSpanId, spanKind)
{ }

/// <summary>
Expand Down
6 changes: 4 additions & 2 deletions src/Observability/Runtime/DTOs/InvokeAgentData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ public class InvokeAgentData : BaseData
/// <param name="endTime">Optional custom end time for the operation.</param>
/// <param name="spanId">Optional span ID for the operation. If not provided one will be created.</param>
/// <param name="parentSpanId">Optional parent span ID for distributed tracing.</param>
/// <param name="spanKind">Optional span kind override. Defaults to <c>null</c> (unset). Use <see cref="SpanKindConstants.Client"/> or <see cref="SpanKindConstants.Server"/> as appropriate.</param>
public InvokeAgentData(
IDictionary<string, object?>? attributes = null,
DateTimeOffset? startTime = null,
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null)
: base(attributes, startTime, endTime, spanId, parentSpanId)
string? parentSpanId = null,
string? spanKind = null)
: base(attributes, startTime, endTime, spanId, parentSpanId, spanKind)
{
}

Expand Down
38 changes: 38 additions & 0 deletions src/Observability/Runtime/DTOs/SpanKindConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Agents.A365.Observability.Runtime.DTOs
{
/// <summary>
/// Provides string constants for OpenTelemetry span kind values.
/// These constants are used in DTOs and Builders to avoid taking a dependency
/// on <see cref="System.Diagnostics.ActivityKind"/>.
/// </summary>
public static class SpanKindConstants
{
/// <summary>
/// Indicates that the span covers server-side handling of a synchronous RPC or other remote request.
/// </summary>
public const string Server = "Server";

/// <summary>
/// Indicates that the span describes a request to some remote service.
/// </summary>
public const string Client = "Client";

/// <summary>
/// Indicates that the span describes a producer sending a message to a broker.
/// </summary>
public const string Producer = "Producer";

/// <summary>
/// Indicates that the span describes a consumer receiving a message from a broker.
/// </summary>
public const string Consumer = "Consumer";

/// <summary>
/// Default span kind. Indicates that the span represents an internal operation within an application.
/// </summary>
public const string Internal = "Internal";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public sealed class ExecuteToolScope : OpenTelemetryScope
/// <param name="callerDetails">Optional details about the non-agentic caller.</param>
/// <param name="startTime">Optional explicit start time. Useful when recording a tool call after execution has already completed.</param>
/// <param name="endTime">Optional explicit end time. When provided, the span will use this timestamp when disposed instead of the current wall-clock time.</param>
/// <param name="spanKind">Optional span kind override. Defaults to <see cref="ActivityKind.Internal"/>. Use <see cref="ActivityKind.Client"/> when the tool calls an external service.</param>
/// <returns>A new ExecuteToolScope instance.</returns>
/// <remarks>
/// <para>
Expand All @@ -47,11 +48,11 @@ public sealed class ExecuteToolScope : OpenTelemetryScope
/// <see href="https://go.microsoft.com/fwlink/?linkid=2344479">Learn more about certification requirements</see>
/// </para>
/// </remarks>
public static ExecuteToolScope Start(ToolCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, CallerDetails? callerDetails = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null) => new ExecuteToolScope(details, agentDetails, tenantDetails, parentId, conversationId, sourceMetadata, threatDiagnosticsSummary, callerDetails, startTime, endTime);
public static ExecuteToolScope Start(ToolCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, CallerDetails? callerDetails = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null, ActivityKind? spanKind = null) => new ExecuteToolScope(details, agentDetails, tenantDetails, parentId, conversationId, sourceMetadata, threatDiagnosticsSummary, callerDetails, startTime, endTime, spanKind);

private ExecuteToolScope(ToolCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, CallerDetails? callerDetails = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null)
private ExecuteToolScope(ToolCallDetails details, AgentDetails agentDetails, TenantDetails tenantDetails, string? parentId = null, string? conversationId = null, SourceMetadata? sourceMetadata = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, CallerDetails? callerDetails = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null, ActivityKind? spanKind = null)
: base(
kind: ActivityKind.Internal,
kind: spanKind ?? ActivityKind.Internal,
agentDetails: agentDetails,
tenantDetails: tenantDetails,
operationName: OperationName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public sealed class InvokeAgentScope : OpenTelemetryScope
/// <param name="startTime">Optional explicit start time. Useful when recording an agent invocation after execution has already completed.</param>
/// <param name="endTime">Optional explicit end time. When provided, the span will use this timestamp when disposed instead of the current wall-clock time.</param>
/// <param name="parentId">Optional parent Activity ID used to link this span to an upstream operation.</param>
/// <param name="spanKind">Optional span kind override. Defaults to <see cref="ActivityKind.Client"/>. Use <see cref="ActivityKind.Server"/> when the agent is receiving an inbound request.</param>
/// <returns>A new InvokeAgentScope instance.</returns>
/// <remarks>
/// <para>
Expand All @@ -54,11 +55,11 @@ public sealed class InvokeAgentScope : OpenTelemetryScope
/// </para>
/// </remarks>
public static InvokeAgentScope Start(
InvokeAgentDetails invokeAgentDetails, TenantDetails tenantDetails, Request? request = null, AgentDetails? callerAgentDetails = null, CallerDetails? callerDetails = null, string? conversationId = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null, string? parentId = null) => new InvokeAgentScope(invokeAgentDetails, tenantDetails, request, callerAgentDetails, callerDetails, conversationId, threatDiagnosticsSummary, startTime, endTime, parentId);
InvokeAgentDetails invokeAgentDetails, TenantDetails tenantDetails, Request? request = null, AgentDetails? callerAgentDetails = null, CallerDetails? callerDetails = null, string? conversationId = null, ThreatDiagnosticsSummary? threatDiagnosticsSummary = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null, string? parentId = null, ActivityKind? spanKind = null) => new InvokeAgentScope(invokeAgentDetails, tenantDetails, request, callerAgentDetails, callerDetails, conversationId, threatDiagnosticsSummary, startTime, endTime, parentId, spanKind);

private InvokeAgentScope(InvokeAgentDetails invokeAgentDetails, TenantDetails tenantDetails, Request? request, AgentDetails? callerAgentDetails, CallerDetails? callerDetails, string? conversationId, ThreatDiagnosticsSummary? threatDiagnosticsSummary, DateTimeOffset? startTime, DateTimeOffset? endTime, string? parentId)
private InvokeAgentScope(InvokeAgentDetails invokeAgentDetails, TenantDetails tenantDetails, Request? request, AgentDetails? callerAgentDetails, CallerDetails? callerDetails, string? conversationId, ThreatDiagnosticsSummary? threatDiagnosticsSummary, DateTimeOffset? startTime, DateTimeOffset? endTime, string? parentId, ActivityKind? spanKind)
: base(
kind: ActivityKind.Client,
kind: spanKind ?? ActivityKind.Client,
agentDetails: invokeAgentDetails.Details,
tenantDetails: tenantDetails,
operationName: OperationName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,8 @@ public void FormatLogData_WithAllFields_ProducesExpectedJson()
start,
end,
spanId,
parentSpanId);
parentSpanId,
spanKind: SpanKindConstants.Client);
var formatter = CreateFormatter();

// Act
Expand All @@ -453,6 +454,7 @@ public void FormatLogData_WithAllFields_ProducesExpectedJson()
root.GetProperty("Name").GetString().Should().Be("InvokeAgent");
root.GetProperty("SpanId").GetString().Should().Be(spanId);
root.GetProperty("ParentSpanId").GetString().Should().Be(parentSpanId);
root.GetProperty("Kind").GetString().Should().Be(SpanKindConstants.Client);

var attrs = root.GetProperty("Attributes");
attrs.GetProperty("attr1").GetString().Should().Be("value1");
Expand Down Expand Up @@ -497,6 +499,9 @@ public void FormatLogData_WithMissingOptionalFields_ProducesDefaults()
// ParentSpanId should be omitted due to null (ignore when writing null)
root.TryGetProperty("ParentSpanId", out _).Should().BeFalse();

// Kind defaults to Client when SpanKind is null
root.GetProperty("Kind").GetString().Should().Be(SpanKindConstants.Client);

var attrs = root.GetProperty("Attributes");
attrs.GetProperty("key").GetString().Should().Be("val");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public TestData(
DateTimeOffset? startTime = null,
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null) : base(attributes, startTime, endTime, spanId, parentSpanId) { }
string? parentSpanId = null,
string? spanKind = null) : base(attributes, startTime, endTime, spanId, parentSpanId, spanKind) { }
public override string Name => "Test";
}

Expand Down Expand Up @@ -131,5 +132,35 @@ public void Attributes_PreserveTypes()
data.Attributes["bool"].Should().BeOfType<bool>();
data.Attributes["null"].Should().BeNull();
}

[TestMethod]
public void SpanKind_DefaultsToNull()
{
var data = new TestData();
data.SpanKind.Should().BeNull();
}

[TestMethod]
public void SpanKind_UsesProvidedValue()
{
var data = new TestData(spanKind: SpanKindConstants.Client);
data.SpanKind.Should().Be(SpanKindConstants.Client);
}

[TestMethod]
public void SpanKind_IncludedInToDictionary()
{
var data = new TestData(spanKind: SpanKindConstants.Server);
var dict = data.ToDictionary();
dict.Should().ContainKey("SpanKind").WhoseValue.Should().Be(SpanKindConstants.Server);
}

[TestMethod]
public void SpanKind_NullInToDictionary_WhenNotProvided()
{
var data = new TestData();
var dict = data.ToDictionary();
dict.Should().ContainKey("SpanKind").WhoseValue.Should().BeNull();
}
}
}
Loading
Loading