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
46 changes: 43 additions & 3 deletions src/Microsoft.OpenTelemetry/.publicApi/PublicAPI.Unshipped.txt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ public string FormatLogData(IDictionary<string, object?> data)
SpanId = data["SpanId"],
ParentSpanId = data["ParentSpanId"],
TraceId = data.TryGetValue("TraceId", out var traceIdObj) ? traceIdObj : null,
Kind = data.TryGetValue("SpanKind", out var spanKindObj) && spanKindObj != null ? spanKindObj : SpanKindConstants.Client
Kind = data.TryGetValue("SpanKind", out var spanKindObj) && spanKindObj != null ? spanKindObj : SpanKindConstants.Client,
Status = data.TryGetValue("Status", out var statusObj) && statusObj != null
? statusObj
: new Dictionary<string, object> { { "code", 0 }, { "message", "" } }
};

return SerializePayload(payload);
Expand Down
21 changes: 20 additions & 1 deletion src/Microsoft.OpenTelemetry/Agent365/Runtime/DTOs/BaseData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ public BaseData(
/// </summary>
public string? TraceId { get; }

/// <summary>
/// Gets or sets the OpenTelemetry status code for the operation. Defaults to
/// <see cref="SpanStatusCode.Unset"/> (0); set to <see cref="SpanStatusCode.Error"/> (2)
/// when an error is recorded.
/// </summary>
public SpanStatusCode StatusCode { get; set; } = SpanStatusCode.Unset;

/// <summary>
/// Gets or sets the OpenTelemetry status message. Per the OTel spec this is only populated for an error status.
/// </summary>
public string? StatusMessage { get; set; }

/// <summary>
/// Gets the duration of the operation if both start and end times are provided.
/// </summary>
Expand All @@ -102,7 +114,14 @@ public BaseData(
{ "ParentSpanId", ParentSpanId },
{ "TraceId", TraceId },
{ "SpanKind", SpanKind },
{ "Duration", Duration }
{ "Duration", Duration },
{
"Status", new Dictionary<string, object>
{
{ "code", (int)StatusCode },
{ "message", StatusMessage ?? "" }
}
}
};

return dict;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class ApplyGuardrailDataBuilder : BaseDataBuilder<ApplyGuardrailData>
/// <param name="extraAttributes">Optional dictionary of extra attributes.</param>
/// <param name="spanKind">Optional span kind override.</param>
/// <param name="traceId">Optional trace ID for distributed tracing.</param>
/// <param name="error">Optional exception describing a failure; sets an OTel error status and the <c>error.type</c> attribute.</param>
/// <returns>An ApplyGuardrailData object containing all telemetry data.</returns>
public static ApplyGuardrailData Build(
GuardrailDetails guardrailDetails,
Expand All @@ -41,11 +42,12 @@ public static ApplyGuardrailData Build(
CallerDetails? callerDetails = null,
IDictionary<string, object?>? extraAttributes = null,
string? spanKind = null,
string? traceId = null)
string? traceId = null,
Exception? error = null)
{
var attributes = BuildAttributes(guardrailDetails, agentDetails, conversationId, channel, callerDetails, extraAttributes);

return new ApplyGuardrailData(parentSpanId, attributes, startTime, endTime, spanId, spanKind, traceId);
return ApplyStatus(new ApplyGuardrailData(parentSpanId, attributes, startTime, endTime, spanId, spanKind, traceId), error);
}

private static Dictionary<string, object?> BuildAttributes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ namespace Microsoft.Agents.A365.Observability.Runtime.DTOs.Builders
/// </summary>
public abstract class BaseDataBuilder<T> where T : BaseData
{

/// <summary>
/// Applies an OpenTelemetry status to the built data based on an optional error, and records
/// the <c>error.type</c> attribute when an error is present. When <paramref name="error"/> is
/// <c>null</c> the status is left as <see cref="SpanStatusCode.Unset"/>.
/// </summary>
/// <param name="data">The telemetry data to annotate.</param>
/// <param name="error">Optional exception describing a failure for the operation.</param>
/// <returns>The same <paramref name="data"/> instance, for fluent use.</returns>
protected static T ApplyStatus(T data, Exception? error)
{
var status = SpanStatusBuilder.FromError(error, data.Attributes);
data.StatusCode = status.Code;
data.StatusMessage = status.Message;
return data;
}
Comment thread
nikhilNava marked this conversation as resolved.

// Reserved attribute keys managed by specific builder methods; extra attributes must NOT override these.
private static readonly HashSet<string> ReservedAttributeKeys = new HashSet<string>(StringComparer.Ordinal)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class ExecuteInferenceDataBuilder : BaseDataBuilder<ExecuteInferenceData>
/// <param name="callerDetails">Optional details about the caller.</param>
/// <param name="extraAttributes">Optional dictionary of extra attributes.</param>
/// <param name="traceId">Optional trace ID for distributed tracing.</param>
/// <param name="error">Optional exception describing a failure; sets an OTel error status and the <c>error.type</c> attribute.</param>
/// <returns>An ExecuteInferenceData object containing all telemetry data.</returns>
public static ExecuteInferenceData Build(
InferenceCallDetails inferenceCallDetails,
Expand All @@ -45,7 +46,8 @@ public static ExecuteInferenceData Build(
string? thoughtProcess = null,
CallerDetails? callerDetails = null,
IDictionary<string, object?>? extraAttributes = null,
string? traceId = null)
string? traceId = null,
Exception? error = null)
{
var attributes = BuildAttributes(
inferenceCallDetails,
Expand All @@ -58,7 +60,7 @@ public static ExecuteInferenceData Build(
callerDetails,
extraAttributes);

return new ExecuteInferenceData(attributes, startTime, endTime, spanId, parentSpanId, traceId);
return ApplyStatus(new ExecuteInferenceData(attributes, startTime, endTime, spanId, parentSpanId, traceId), error);
}

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 ExecuteToolDataBuilder : BaseDataBuilder<ExecuteToolData>
/// <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>
/// <param name="traceId">Optional trace ID for distributed tracing.</param>
/// <param name="error">Optional exception describing a failure; sets an OTel error status and the <c>error.type</c> attribute.</param>
/// <returns>An ExecuteToolData object containing all telemetry data.</returns>
public static ExecuteToolData Build(
ToolCallDetails toolCallDetails,
Expand All @@ -45,11 +46,12 @@ public static ExecuteToolData Build(
CallerDetails? callerDetails = null,
IDictionary<string, object?>? extraAttributes = null,
string? spanKind = null,
string? traceId = null)
string? traceId = null,
Exception? error = null)
{
var attributes = BuildAttributes(toolCallDetails, agentDetails, conversationId, responseContent, channel, callerDetails, extraAttributes);

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

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="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>
/// <param name="traceId">Optional trace ID for distributed tracing.</param>
/// <param name="error">Optional exception describing a failure; sets an OTel error status and the <c>error.type</c> attribute.</param>
/// <returns>An InvokeAgentData object containing all telemetry data.</returns>
public static InvokeAgentData Build(
InvokeAgentScopeDetails invokeAgentScopeDetails,
Expand All @@ -46,7 +47,8 @@ public static InvokeAgentData Build(
string? parentSpanId = null,
IDictionary<string, object?>? extraAttributes = null,
string? spanKind = null,
string? traceId = null)
string? traceId = null,
Exception? error = null)
{
var attributes = BuildAttributes(
invokeAgentScopeDetails,
Expand All @@ -58,14 +60,14 @@ public static InvokeAgentData Build(
outputMessages,
extraAttributes);

return new InvokeAgentData(
return ApplyStatus(new InvokeAgentData(
attributes,
startTime,
endTime,
spanId,
parentSpanId,
spanKind,
traceId);
traceId), error);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class OutputDataBuilder : BaseDataBuilder<OutputData>
/// <param name="parentSpanId">Optional parent span ID for distributed tracing.</param>
/// <param name="extraAttributes">Optional dictionary of extra attributes.</param>
/// <param name="traceId">Optional trace ID for distributed tracing.</param>
/// <param name="error">Optional exception describing a failure; sets an OTel error status and the <c>error.type</c> attribute.</param>
/// <returns>An OutputData object containing all telemetry data.</returns>
public static OutputData Build(
AgentDetails agentDetails,
Expand All @@ -40,11 +41,12 @@ public static OutputData Build(
string? spanId = null,
string? parentSpanId = null,
IDictionary<string, object?>? extraAttributes = null,
string? traceId = null)
string? traceId = null,
Exception? error = null)
{
var attributes = BuildAttributes(agentDetails, response, conversationId, channel, callerDetails, extraAttributes);

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

private static Dictionary<string, object?> BuildAttributes(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

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

namespace Microsoft.Agents.A365.Observability.Runtime.DTOs.Builders
{
/// <summary>
/// Builds an OpenTelemetry-compliant <see cref="SpanStatus"/> from an exception and, for error
/// statuses, records the <c>error.type</c> attribute on the span attributes.
/// </summary>
/// <remarks>
/// Centralizes the exception-to-status mapping so that the ETW DTO logging path stays consistent
/// with the Activity-based scope path (<see cref="OpenTelemetryScope.RecordError(Exception)"/>).
/// </remarks>
public static class SpanStatusBuilder
{
/// <summary>
/// Creates a <see cref="SpanStatus"/> from an optional exception.
/// </summary>
/// <param name="error">
/// The exception to record. When <c>null</c>, an <see cref="SpanStatusCode.Unset"/> status is
/// returned and any pre-existing <c>error.type</c> attribute is removed so it is never emitted
/// without an accompanying error status.
/// </param>
/// <param name="attributes">
/// Optional span attributes dictionary. When an error is supplied and this is non-<c>null</c>, the
/// <c>error.type</c> attribute is written following OTel semantic conventions.
/// </param>
/// <returns>
/// An <see cref="SpanStatusCode.Error"/> status (with the exception message) when <paramref name="error"/>
/// is non-<c>null</c>; otherwise an <see cref="SpanStatusCode.Unset"/> status.
/// </returns>
public static SpanStatus FromError(Exception? error, IDictionary<string, object?>? attributes = null)
{
if (error == null)
{
// Ensure error.type is never emitted without an accompanying error status, even if a
// caller passed it through extraAttributes (it is not part of the reserved-key filter).
attributes?.Remove(OpenTelemetryConstants.ErrorTypeKey);
return new SpanStatus(SpanStatusCode.Unset);
}

// Mirrors OpenTelemetryScope.RecordError: prefer the HTTP status from a RequestFailedException,
// otherwise fall back to the exception's full type name.
var errorType = error is RequestFailedException requestFailed && requestFailed.Status != 0
? requestFailed.Status.ToString()
: error.GetType().FullName ?? "error";

if (attributes != null)
{
attributes[OpenTelemetryConstants.ErrorTypeKey] = errorType;
}

return new SpanStatus(SpanStatusCode.Error, error.Message);
}
}
}
34 changes: 34 additions & 0 deletions src/Microsoft.OpenTelemetry/Agent365/Runtime/DTOs/SpanStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Agents.A365.Observability.Runtime.DTOs
{
/// <summary>
/// Represents an OpenTelemetry span status (<c>code</c> and optional <c>message</c>),
/// following the OTel specification where <see cref="SpanStatusCode.Unset"/> = 0,
/// <see cref="SpanStatusCode.Ok"/> = 1, and <see cref="SpanStatusCode.Error"/> = 2.
/// </summary>
public readonly struct SpanStatus
{
/// <summary>
/// Initializes a new instance of the <see cref="SpanStatus"/> struct.
/// </summary>
/// <param name="code">The status code. Defaults to <see cref="SpanStatusCode.Unset"/>.</param>
/// <param name="message">Optional status message. Per the OTel spec this is only meaningful for an error status.</param>
public SpanStatus(SpanStatusCode code = SpanStatusCode.Unset, string? message = null)
{
Code = code;
Message = message;
}

/// <summary>
/// Gets the status code (<see cref="SpanStatusCode.Unset"/>, <see cref="SpanStatusCode.Ok"/>, or <see cref="SpanStatusCode.Error"/>).
/// </summary>
public SpanStatusCode Code { get; }

/// <summary>
/// Gets the optional status message. Only populated for an error status.
/// </summary>
public string? Message { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Agents.A365.Observability.Runtime.DTOs
{
/// <summary>
/// Represents the OpenTelemetry span status code as defined by the OTLP specification.
/// The numeric values map directly onto the OTLP <c>Status.StatusCode</c> enum and are
/// emitted as-is on the ETW telemetry payload.
/// </summary>
public enum SpanStatusCode
{
/// <summary>
/// The default status; the span has not been explicitly marked successful or failed.
/// Maps to OTLP <c>STATUS_CODE_UNSET</c>.
/// </summary>
Unset = 0,

/// <summary>
/// The operation completed successfully (explicitly set by the application).
/// Maps to OTLP <c>STATUS_CODE_OK</c>.
/// </summary>
Ok = 1,

/// <summary>
/// The operation failed. Maps to OTLP <c>STATUS_CODE_ERROR</c>.
/// </summary>
Error = 2,
}
}
Loading
Loading