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
72 changes: 72 additions & 0 deletions src/Observability/Runtime/DTOs/Builders/OutputDataBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts;
using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
using System;
using System.Collections.Generic;

namespace Microsoft.Agents.A365.Observability.Runtime.DTOs.Builders
{
/// <summary>
/// Builds an OutputData instance.
/// </summary>
public class OutputDataBuilder : BaseDataBuilder<OutputData>
{
private const string OutputMessagesOperationName = "output_messages";

/// <summary>
/// Builds complete data for an output_messages operation.
/// </summary>
/// <param name="agentDetails">The details of the agent.</param>
/// <param name="tenantDetails">The details of the tenant.</param>
/// <param name="response">The response containing output messages.</param>
/// <param name="startTime">Optional custom start time for the operation.</param>
/// <param name="endTime">Optional custom end time for the operation.</param>
/// <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>
/// <returns>An OutputData object containing all telemetry data.</returns>
public static OutputData Build(
AgentDetails agentDetails,
TenantDetails tenantDetails,
Response response,
DateTimeOffset? startTime = null,
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null,
IDictionary<string, object?>? extraAttributes = null)
{
var attributes = BuildAttributes(agentDetails, tenantDetails, response, extraAttributes);

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

private static Dictionary<string, object?> BuildAttributes(
AgentDetails agentDetails,
TenantDetails tenantDetails,
Response response,
IDictionary<string, object?>? extraAttributes = null)
{
var attributes = new Dictionary<string, object?>();

// Operation name
AddIfNotNull(attributes, OpenTelemetryConstants.GenAiOperationNameKey, OutputMessagesOperationName);

// Agent & tenant
AddAgentDetails(attributes, agentDetails);
AddTenantDetails(attributes, tenantDetails);

// Output messages from response
if (response.Messages.Count > 0)
{
AddIfNotNull(attributes, OpenTelemetryConstants.GenAiOutputMessagesKey, string.Join(",", response.Messages));
}

// Add any extra attributes
AddExtraAttributes(attributes, extraAttributes);

return attributes;
}
}
}
37 changes: 37 additions & 0 deletions src/Observability/Runtime/DTOs/OutputData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
using System;
using System.Collections.Generic;

namespace Microsoft.Agents.A365.Observability.Runtime.DTOs
{
/// <summary>
/// Encapsulates all telemetry data for an output_messages operation.
/// </summary>
public class OutputData : BaseData
{
/// <summary>
/// Initializes a new instance of the <see cref="OutputData"/> class.
/// </summary>
/// <param name="attributes">The telemetry attributes (tags).</param>
/// <param name="startTime">Optional custom start time for the operation.</param>
/// <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>
public OutputData(
IDictionary<string, object?>? attributes = null,
DateTimeOffset? startTime = null,
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null)
: base(attributes, startTime, endTime, spanId, parentSpanId)
{ }

/// <summary>
/// Gets the name of the operation.
/// </summary>
public override string Name => OpenTelemetryConstants.OperationNames.OutputMessages.ToString();
}
}
30 changes: 30 additions & 0 deletions src/Observability/Runtime/Etw/A365EtwLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class A365EtwLogger<T> : IA365EtwLogger<T>
private static readonly EventId ExecuteInferenceEventId = new EventId(1002, ExecuteInferenceEventName);
private const string ExecuteToolEventName = "ExecuteTool";
private static readonly EventId ExecuteToolEventId = new EventId(1003, ExecuteToolEventName);
private const string OutputMessagesEventName = "OutputMessages";
private static readonly EventId OutputMessagesEventId = new EventId(1004, OutputMessagesEventName);

/// <summary>
/// Initializes a new instance of the <see cref="A365EtwLogger{T}"/> class.
Expand Down Expand Up @@ -139,6 +141,34 @@ public void LogToolCall(
);
}

/// <inheritdoc/>
public void LogOutput(
AgentDetails agentDetails,
TenantDetails tenantDetails,
Response response,
DateTimeOffset? startTime = null,
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null)
{
var data = OutputDataBuilder.Build(
agentDetails,
tenantDetails,
response,
startTime,
endTime,
spanId,
parentSpanId);

logger.Log(
LogLevel.Information,
OutputMessagesEventId,
data.ToDictionary(),
null,
LogFormatter
);
}

private static string LogFormatter(Dictionary<string, object?> data, Exception? ex)
{
return $"Name: {data["Name"]}, SpanId: {data["SpanId"]}, ParentSpanId: {data["ParentSpanId"]}";
Expand Down
19 changes: 19 additions & 0 deletions src/Observability/Runtime/Etw/IA365EtwLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,24 @@ public void LogToolCall(
string? spanId = null,
string? parentSpanId = null,
SourceMetadata? sourceMetadata = null);

/// <summary>
/// Logs an output_messages event.
/// </summary>
/// <param name="agentDetails">The details of the agent.</param>
/// <param name="tenantDetails">The details of the tenant.</param>
/// <param name="response">The response containing output messages.</param>
/// <param name="startTime">Optional start time of the output operation.</param>
/// <param name="endTime">Optional end time of the output operation.</param>
/// <param name="spanId">Optional span ID for tracing.</param>
/// <param name="parentSpanId">Optional parent span ID for tracing.</param>
public void LogOutput(
AgentDetails agentDetails,
TenantDetails tenantDetails,
Response response,
DateTimeOffset? startTime = null,
DateTimeOffset? endTime = null,
string? spanId = null,
string? parentSpanId = null);
}
}
73 changes: 73 additions & 0 deletions src/Observability/Runtime/Tracing/Contracts/Response.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts
{
/// <summary>
/// Represents a response from an AI agent with output messages.
/// </summary>
public sealed class Response : IEquatable<Response>
{
/// <summary>
/// Initializes a new instance of the <see cref="Response"/> class.
/// </summary>
/// <param name="messages">The output messages from the agent.</param>
public Response(IReadOnlyList<string>? messages = null)
{
Messages = messages ?? Array.Empty<string>();
}

/// <summary>
/// Gets the output messages from the agent response.
/// </summary>
public IReadOnlyList<string> Messages { get; }

/// <inheritdoc/>
public bool Equals(Response? other)
{
if (other is null)
{
return false;
}

if (Messages.Count != other.Messages.Count)
{
return false;
}

for (int i = 0; i < Messages.Count; i++)
{
if (!string.Equals(Messages[i], other.Messages[i], StringComparison.Ordinal))
{
return false;
}
}

return true;
}

/// <inheritdoc/>
public override bool Equals(object? obj)
{
return Equals(obj as Response);
}

/// <inheritdoc/>
public override int GetHashCode()
{
unchecked
{
int hash = 17;
foreach (var message in Messages)
{
hash = (hash * 31) + (message != null ? StringComparer.Ordinal.GetHashCode(message) : 0);
}

return hash;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ public enum OperationNames
ExecuteInference,

[EnumMember(Value = "ExecuteTool")]
ExecuteTool
ExecuteTool,

[EnumMember(Value = "OutputMessages")]
OutputMessages
}

// AI invocation context dimensions
Expand Down
72 changes: 72 additions & 0 deletions src/Observability/Runtime/Tracing/Scopes/OutputScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts;

namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes
{
/// <summary>
/// Provides OpenTelemetry tracing scope for AI agent output operations.
/// </summary>
public sealed class OutputScope : OpenTelemetryScope
{
/// <summary>
/// The operation name for output tracing.
/// </summary>
public const string OperationName = "output_messages";

private readonly List<string> outputMessages = new List<string>();

/// <summary>
/// Creates and starts a new scope for output tracing.
/// </summary>
/// <param name="agentDetails">Information about the agent producing the output.</param>
/// <param name="tenantDetails">Tenant context used for telemetry enrichment and correlation.</param>
/// <param name="response">Response containing output messages.</param>
/// <param name="parentId">Optional parent Activity ID used to link this span to an upstream operation.</param>
/// <returns>A new OutputScope instance.</returns>
public static OutputScope Start(AgentDetails agentDetails, TenantDetails tenantDetails, Response response, string? parentId = null)
=> new OutputScope(agentDetails, tenantDetails, response, parentId);

private OutputScope(AgentDetails agentDetails, TenantDetails tenantDetails, Response response, string? parentId)
: base(
kind: ActivityKind.Client,
agentDetails: agentDetails,
tenantDetails: tenantDetails,
operationName: OperationName,
activityName: $"{OperationName} {agentDetails?.AgentId}",
parentId: parentId)
{
if (response.Messages.Count > 0)
{
foreach (var message in response.Messages)
{
outputMessages.Add(message);
}

SetTagMaybe(OpenTelemetryConstants.GenAiOutputMessagesKey, string.Join(",", outputMessages));
}
}

/// <summary>
/// Records additional output messages and appends them to the existing output messages attribute.
/// </summary>
/// <param name="messages">The messages to append to the output.</param>
public void RecordOutputMessages(IEnumerable<string> messages)
{
if (messages == null)
{
return;
}

foreach (var message in messages)
{
outputMessages.Add(message);
}

SetTagMaybe(OpenTelemetryConstants.GenAiOutputMessagesKey, string.Join(",", outputMessages));
}
}
}
Loading