diff --git a/src/Observability/Runtime/Common/ExportFormatter.cs b/src/Observability/Runtime/Common/ExportFormatter.cs index 6c2e1c50..e49bf310 100644 --- a/src/Observability/Runtime/Common/ExportFormatter.cs +++ b/src/Observability/Runtime/Common/ExportFormatter.cs @@ -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; @@ -146,7 +147,8 @@ public string FormatLogData(IDictionary 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); diff --git a/src/Observability/Runtime/DTOs/BaseData.cs b/src/Observability/Runtime/DTOs/BaseData.cs index 82154b17..4ddb0175 100644 --- a/src/Observability/Runtime/DTOs/BaseData.cs +++ b/src/Observability/Runtime/DTOs/BaseData.cs @@ -19,12 +19,14 @@ public abstract class BaseData /// Optional custom end time for the operation. /// Optional span ID for the operation. If not provided one will be created. /// Optional parent span ID for distributed tracing. + /// Optional span kind for the operation. Use values (e.g., , , ). public BaseData( IDictionary? attributes = null, DateTimeOffset? startTime = null, DateTimeOffset? endTime = null, string? spanId = null, - string? parentSpanId = null) + string? parentSpanId = null, + string? spanKind = null) { Attributes = attributes ?? new Dictionary(); StartTime = startTime; @@ -32,6 +34,7 @@ public BaseData( // Generate a random span ID if not provided. Use ActivitySpanId for consistency with tracing. SpanId = spanId ?? ActivitySpanId.CreateRandom().ToString(); ParentSpanId = parentSpanId; + SpanKind = spanKind; } /// @@ -64,6 +67,11 @@ public BaseData( /// public string? ParentSpanId { get; } + /// + /// Gets the span kind for the operation, if provided. See for valid values. + /// + public string? SpanKind { get; } + /// /// Gets the duration of the operation if both start and end times are provided. /// @@ -84,6 +92,7 @@ public BaseData( { "EndTime", EndTime }, { "SpanId", SpanId }, { "ParentSpanId", ParentSpanId }, + { "SpanKind", SpanKind }, { "Duration", Duration } }; diff --git a/src/Observability/Runtime/DTOs/Builders/ExecuteToolDataBuilder.cs b/src/Observability/Runtime/DTOs/Builders/ExecuteToolDataBuilder.cs index 533a2027..716cee7e 100644 --- a/src/Observability/Runtime/DTOs/Builders/ExecuteToolDataBuilder.cs +++ b/src/Observability/Runtime/DTOs/Builders/ExecuteToolDataBuilder.cs @@ -31,6 +31,7 @@ public class ExecuteToolDataBuilder : BaseDataBuilder /// Optional source metadata for the operation. /// Optional details about the non-agentic caller. /// Optional dictionary of extra attributes. + /// Optional span kind override. Use or as appropriate. /// An ExecuteToolData object containing all telemetry data. public static ExecuteToolData Build( ToolCallDetails toolCallDetails, @@ -44,11 +45,12 @@ public static ExecuteToolData Build( string? parentSpanId = null, SourceMetadata? sourceMetadata = null, CallerDetails? callerDetails = null, - IDictionary? extraAttributes = null) + IDictionary? 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 BuildAttributes( diff --git a/src/Observability/Runtime/DTOs/Builders/InvokeAgentDataBuilder.cs b/src/Observability/Runtime/DTOs/Builders/InvokeAgentDataBuilder.cs index 7ea5dc23..bde57e81 100644 --- a/src/Observability/Runtime/DTOs/Builders/InvokeAgentDataBuilder.cs +++ b/src/Observability/Runtime/DTOs/Builders/InvokeAgentDataBuilder.cs @@ -31,6 +31,7 @@ public class InvokeAgentDataBuilder : BaseDataBuilder /// Optional span ID for the operation. /// Optional parent span ID for distributed tracing. /// Optional dictionary of extra attributes. + /// Optional span kind override. Use or as appropriate. /// An InvokeAgentData object containing all telemetry data. public static InvokeAgentData Build( InvokeAgentDetails invokeAgentDetails, @@ -45,7 +46,8 @@ public static InvokeAgentData Build( DateTimeOffset? endTime = null, string? spanId = null, string? parentSpanId = null, - IDictionary? extraAttributes = null) + IDictionary? extraAttributes = null, + string? spanKind = null) { var attributes = BuildAttributes( invokeAgentDetails, @@ -63,7 +65,8 @@ public static InvokeAgentData Build( startTime, endTime, spanId, - parentSpanId); + parentSpanId, + spanKind); } /// diff --git a/src/Observability/Runtime/DTOs/ExecuteToolData.cs b/src/Observability/Runtime/DTOs/ExecuteToolData.cs index 44526868..e55e1eb8 100644 --- a/src/Observability/Runtime/DTOs/ExecuteToolData.cs +++ b/src/Observability/Runtime/DTOs/ExecuteToolData.cs @@ -20,13 +20,15 @@ public class ExecuteToolData : BaseData /// Optional custom end time for the operation. /// Optional span ID for the operation. If not provided one will be created. /// Optional parent span ID for distributed tracing. + /// Optional span kind override. Defaults to null (unset). Use or as appropriate. public ExecuteToolData( IDictionary? 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) { } /// diff --git a/src/Observability/Runtime/DTOs/InvokeAgentData.cs b/src/Observability/Runtime/DTOs/InvokeAgentData.cs index 4ba3d88f..0bf75116 100644 --- a/src/Observability/Runtime/DTOs/InvokeAgentData.cs +++ b/src/Observability/Runtime/DTOs/InvokeAgentData.cs @@ -20,13 +20,15 @@ public class InvokeAgentData : BaseData /// Optional custom end time for the operation. /// Optional span ID for the operation. If not provided one will be created. /// Optional parent span ID for distributed tracing. + /// Optional span kind override. Defaults to null (unset). Use or as appropriate. public InvokeAgentData( IDictionary? 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) { } diff --git a/src/Observability/Runtime/DTOs/SpanKindConstants.cs b/src/Observability/Runtime/DTOs/SpanKindConstants.cs new file mode 100644 index 00000000..3c18df91 --- /dev/null +++ b/src/Observability/Runtime/DTOs/SpanKindConstants.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Agents.A365.Observability.Runtime.DTOs +{ + /// + /// Provides string constants for OpenTelemetry span kind values. + /// These constants are used in DTOs and Builders to avoid taking a dependency + /// on . + /// + public static class SpanKindConstants + { + /// + /// Indicates that the span covers server-side handling of a synchronous RPC or other remote request. + /// + public const string Server = "Server"; + + /// + /// Indicates that the span describes a request to some remote service. + /// + public const string Client = "Client"; + + /// + /// Indicates that the span describes a producer sending a message to a broker. + /// + public const string Producer = "Producer"; + + /// + /// Indicates that the span describes a consumer receiving a message from a broker. + /// + public const string Consumer = "Consumer"; + + /// + /// Default span kind. Indicates that the span represents an internal operation within an application. + /// + public const string Internal = "Internal"; + } +} diff --git a/src/Observability/Runtime/Tracing/Scopes/ExecuteToolScope.cs b/src/Observability/Runtime/Tracing/Scopes/ExecuteToolScope.cs index 25855523..0d4706b1 100644 --- a/src/Observability/Runtime/Tracing/Scopes/ExecuteToolScope.cs +++ b/src/Observability/Runtime/Tracing/Scopes/ExecuteToolScope.cs @@ -33,6 +33,7 @@ public sealed class ExecuteToolScope : OpenTelemetryScope /// Optional details about the non-agentic caller. /// Optional explicit start time. Useful when recording a tool call after execution has already completed. /// Optional explicit end time. When provided, the span will use this timestamp when disposed instead of the current wall-clock time. + /// Optional span kind override. Defaults to . Use when the tool calls an external service. /// A new ExecuteToolScope instance. /// /// @@ -47,11 +48,11 @@ public sealed class ExecuteToolScope : OpenTelemetryScope /// Learn more about certification requirements /// /// - 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, diff --git a/src/Observability/Runtime/Tracing/Scopes/InvokeAgentScope.cs b/src/Observability/Runtime/Tracing/Scopes/InvokeAgentScope.cs index 84ae8855..1f78e8d4 100644 --- a/src/Observability/Runtime/Tracing/Scopes/InvokeAgentScope.cs +++ b/src/Observability/Runtime/Tracing/Scopes/InvokeAgentScope.cs @@ -35,6 +35,7 @@ public sealed class InvokeAgentScope : OpenTelemetryScope /// Optional explicit start time. Useful when recording an agent invocation after execution has already completed. /// Optional explicit end time. When provided, the span will use this timestamp when disposed instead of the current wall-clock time. /// Optional parent Activity ID used to link this span to an upstream operation. + /// Optional span kind override. Defaults to . Use when the agent is receiving an inbound request. /// A new InvokeAgentScope instance. /// /// @@ -54,11 +55,11 @@ public sealed class InvokeAgentScope : OpenTelemetryScope /// /// 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, diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Common/ExportFormatterTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Common/ExportFormatterTests.cs index 945a56a9..2c0fc069 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Common/ExportFormatterTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Common/ExportFormatterTests.cs @@ -439,7 +439,8 @@ public void FormatLogData_WithAllFields_ProducesExpectedJson() start, end, spanId, - parentSpanId); + parentSpanId, + spanKind: SpanKindConstants.Client); var formatter = CreateFormatter(); // Act @@ -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"); @@ -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"); } diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/BaseDataTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/BaseDataTests.cs index f696ed46..a7c915e0 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/BaseDataTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/BaseDataTests.cs @@ -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"; } @@ -131,5 +132,35 @@ public void Attributes_PreserveTypes() data.Attributes["bool"].Should().BeOfType(); 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(); + } } } diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/Builders/ExecuteToolDataBuilderTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/Builders/ExecuteToolDataBuilderTests.cs index 04417a1a..34f6b22a 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/Builders/ExecuteToolDataBuilderTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/Builders/ExecuteToolDataBuilderTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Microsoft.Agents.A365.Observability.Runtime.DTOs; using Microsoft.Agents.A365.Observability.Runtime.DTOs.Builders; using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; @@ -332,5 +333,39 @@ public void Build_IgnoresNullValues_InExtraAttributes() data.Attributes.Should().NotContainKey("tool.null"); data.Attributes.Should().ContainKey("tool.valid").WhoseValue.Should().Be("yes"); } + + [TestMethod] + public void Build_SpanKind_DefaultsToNull() + { + // Arrange + var tool = new ToolCallDetails("toolSK", null); + var agent = new AgentDetails("agent-sk"); + var tenant = new TenantDetails(Guid.NewGuid()); + var conversationId = "conv-sk-default"; + + // Act + var data = ExecuteToolDataBuilder.Build(tool, agent, tenant, conversationId); + + // Assert + data.SpanKind.Should().BeNull(); + } + + [TestMethod] + public void Build_SpanKind_PassesThroughProvidedValue() + { + // Arrange + var tool = new ToolCallDetails("toolSK", null); + var agent = new AgentDetails("agent-sk"); + var tenant = new TenantDetails(Guid.NewGuid()); + var conversationId = "conv-sk-client"; + + // Act + var data = ExecuteToolDataBuilder.Build( + tool, agent, tenant, conversationId, + spanKind: SpanKindConstants.Client); + + // Assert + data.SpanKind.Should().Be(SpanKindConstants.Client); + } } } diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/Builders/InvokeAgentDataBuilderTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/Builders/InvokeAgentDataBuilderTests.cs index 7e3686ce..f5c22275 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/Builders/InvokeAgentDataBuilderTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/Builders/InvokeAgentDataBuilderTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Microsoft.Agents.A365.Observability.Runtime.DTOs; using Microsoft.Agents.A365.Observability.Runtime.DTOs.Builders; using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; @@ -498,5 +499,43 @@ public void Build_WithAgentPlatformId_SetsExpectedAttributes() telemetry.Attributes.Should().ContainKey(OpenTelemetryConstants.GenAiInputMessagesKey); telemetry.Attributes[OpenTelemetryConstants.GenAiInputMessagesKey].Should().Be("Hello"); } + + [TestMethod] + public void Build_SpanKind_DefaultsToNull() + { + // Arrange + var endpoint = new Uri("https://example.com"); + var agentDetails = new AgentDetails("agent-123", "TestAgent"); + var invokeAgentDetails = new InvokeAgentDetails(endpoint: endpoint, details: agentDetails); + var tenantDetails = new TenantDetails(Guid.NewGuid()); + var conversationId = "conv-sk-default"; + + // Act + var data = InvokeAgentDataBuilder.Build(invokeAgentDetails, tenantDetails, conversationId); + + // Assert + data.SpanKind.Should().BeNull(); + } + + [TestMethod] + public void Build_SpanKind_PassesThroughProvidedValue() + { + // Arrange + var endpoint = new Uri("https://example.com"); + var agentDetails = new AgentDetails("agent-123", "TestAgent"); + var invokeAgentDetails = new InvokeAgentDetails(endpoint: endpoint, details: agentDetails); + var tenantDetails = new TenantDetails(Guid.NewGuid()); + var conversationId = "conv-sk-server"; + + // Act + var data = InvokeAgentDataBuilder.Build( + invokeAgentDetails, + tenantDetails, + conversationId, + spanKind: SpanKindConstants.Server); + + // Assert + data.SpanKind.Should().Be(SpanKindConstants.Server); + } } } diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/ExecuteToolDataTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/ExecuteToolDataTests.cs index a6fcc4bc..56c8d623 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/ExecuteToolDataTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/ExecuteToolDataTests.cs @@ -16,5 +16,19 @@ public void Name_ReturnsExecuteTool() var data = new ExecuteToolData(); data.Name.Should().Be(OpenTelemetryConstants.OperationNames.ExecuteTool.ToString()); } + + [TestMethod] + public void SpanKind_DefaultsToNull() + { + var data = new ExecuteToolData(); + data.SpanKind.Should().BeNull(); + } + + [TestMethod] + public void SpanKind_UsesProvidedValue() + { + var data = new ExecuteToolData(spanKind: SpanKindConstants.Client); + data.SpanKind.Should().Be(SpanKindConstants.Client); + } } } diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/InvokeAgentDataTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/InvokeAgentDataTests.cs index d5ff09c6..f287d5ed 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/InvokeAgentDataTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/DTOs/InvokeAgentDataTests.cs @@ -13,5 +13,19 @@ public void Name_ReturnsInvokeAgent() var data = new InvokeAgentData(); data.Name.Should().Be(OpenTelemetryConstants.OperationNames.InvokeAgent.ToString()); } + + [TestMethod] + public void SpanKind_DefaultsToNull() + { + var data = new InvokeAgentData(); + data.SpanKind.Should().BeNull(); + } + + [TestMethod] + public void SpanKind_UsesProvidedValue() + { + var data = new InvokeAgentData(spanKind: SpanKindConstants.Server); + data.SpanKind.Should().Be(SpanKindConstants.Server); + } } } diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/ExecuteToolScopeTest.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/ExecuteToolScopeTest.cs index 25fc44e9..05f09eb2 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/ExecuteToolScopeTest.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/ExecuteToolScopeTest.cs @@ -303,4 +303,37 @@ public void SetEndTime_OverridesEndTime() var startTime = new DateTimeOffset(activity.StartTimeUtc); startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); } + + [TestMethod] + public void SpanKind_DefaultsToInternal() + { + // Act + var activity = ListenForActivity(() => + { + using var scope = ExecuteToolScope.Start( + new ToolCallDetails("TestTool", "args"), + Util.GetAgentDetails(), + Util.GetTenantDetails()); + }); + + // Assert + activity.Kind.Should().Be(System.Diagnostics.ActivityKind.Internal); + } + + [TestMethod] + public void SpanKind_OverrideToClient() + { + // Act + var activity = ListenForActivity(() => + { + using var scope = ExecuteToolScope.Start( + new ToolCallDetails("TestTool", "args"), + Util.GetAgentDetails(), + Util.GetTenantDetails(), + spanKind: System.Diagnostics.ActivityKind.Client); + }); + + // Assert + activity.Kind.Should().Be(System.Diagnostics.ActivityKind.Client); + } } \ No newline at end of file diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InvokeAgentScopeTest.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InvokeAgentScopeTest.cs index 5b8cdffb..68dbc8a3 100644 --- a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InvokeAgentScopeTest.cs +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Scopes/InvokeAgentScopeTest.cs @@ -316,4 +316,33 @@ public void SetEndTime_OverridesEndTime() var startTime = new DateTimeOffset(activity.StartTimeUtc); startTime.Should().BeCloseTo(customStartTime, TimeSpan.FromMilliseconds(100)); } + + [TestMethod] + public void SpanKind_DefaultsToClient() + { + // Act + var activity = ListenForActivity(() => + { + using var scope = InvokeAgentScope.Start(Details, Util.GetTenantDetails()); + }); + + // Assert + activity.Kind.Should().Be(System.Diagnostics.ActivityKind.Client); + } + + [TestMethod] + public void SpanKind_OverrideToServer() + { + // Act + var activity = ListenForActivity(() => + { + using var scope = InvokeAgentScope.Start( + Details, + Util.GetTenantDetails(), + spanKind: System.Diagnostics.ActivityKind.Server); + }); + + // Assert + activity.Kind.Should().Be(System.Diagnostics.ActivityKind.Server); + } } \ No newline at end of file