// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace Microsoft.Agents.AI.Workflows.Observability;
///
/// Internal context for workflow telemetry, holding the enabled state and configuration options.
///
internal sealed class WorkflowTelemetryContext
{
private const string DefaultSourceName = "Microsoft.Agents.AI.Workflows";
private static readonly ActivitySource s_defaultActivitySource = new(DefaultSourceName);
///
/// Gets a shared instance representing disabled telemetry.
///
public static WorkflowTelemetryContext Disabled { get; } = new();
///
/// Gets a value indicating whether telemetry is enabled.
///
public bool IsEnabled { get; }
///
/// Gets the telemetry options.
///
public WorkflowTelemetryOptions Options { get; }
///
/// Gets the activity source used for creating telemetry spans.
///
public ActivitySource ActivitySource { get; }
private WorkflowTelemetryContext()
{
this.IsEnabled = false;
this.Options = new WorkflowTelemetryOptions();
this.ActivitySource = s_defaultActivitySource;
}
///
/// Initializes a new instance of the class with telemetry enabled.
///
/// The telemetry options.
///
/// An optional activity source to use. If provided, this activity source will be used directly
/// and the caller retains ownership (responsible for disposal). If , the
/// shared default activity source will be used.
///
public WorkflowTelemetryContext(WorkflowTelemetryOptions options, ActivitySource? activitySource = null)
{
this.IsEnabled = true;
this.Options = options;
this.ActivitySource = activitySource ?? s_defaultActivitySource;
}
///
/// Starts an activity if telemetry is enabled, otherwise returns null.
///
/// The activity name.
/// The activity kind.
/// An activity if telemetry is enabled and the activity is sampled, otherwise null.
public Activity? StartActivity(string name, ActivityKind kind = ActivityKind.Internal)
{
if (!this.IsEnabled)
{
return null;
}
return this.ActivitySource.StartActivity(name, kind);
}
///
/// Starts a workflow build activity if enabled.
///
/// An activity if workflow build telemetry is enabled, otherwise null.
public Activity? StartWorkflowBuildActivity()
{
if (!this.IsEnabled || this.Options.DisableWorkflowBuild)
{
return null;
}
return this.ActivitySource.StartActivity(ActivityNames.WorkflowBuild);
}
///
/// Starts a workflow session activity if enabled. This is the outer/parent span
/// that represents the entire lifetime of a workflow execution (from start
/// until stop, cancellation, or error) within the current trace.
/// Individual run stages are typically nested within it.
///
/// An activity if workflow run telemetry is enabled, otherwise null.
public Activity? StartWorkflowSessionActivity()
{
if (!this.IsEnabled || this.Options.DisableWorkflowRun)
{
return null;
}
return this.ActivitySource.StartActivity(ActivityNames.WorkflowSession);
}
///
/// Starts a workflow run activity if enabled. This represents a single
/// input-to-halt cycle within a workflow session.
///
/// An activity if workflow run telemetry is enabled, otherwise null.
public Activity? StartWorkflowRunActivity()
{
if (!this.IsEnabled || this.Options.DisableWorkflowRun)
{
return null;
}
return this.ActivitySource.StartActivity(ActivityNames.WorkflowInvoke);
}
///
/// Starts an executor process activity if enabled, with all standard tags set.
///
/// The executor identifier.
/// The executor type name.
/// The message type name.
/// The input message. Logged only when is true.
/// An activity if executor process telemetry is enabled, otherwise null.
public Activity? StartExecutorProcessActivity(string executorId, string? executorType, string messageType, object? message)
{
if (!this.IsEnabled || this.Options.DisableExecutorProcess)
{
return null;
}
Activity? activity = this.ActivitySource.StartActivity(ActivityNames.ExecutorProcess + " " + executorId);
if (activity is null)
{
return null;
}
activity.SetTag(Tags.ExecutorId, executorId)
.SetTag(Tags.ExecutorType, executorType)
.SetTag(Tags.MessageType, messageType);
if (this.Options.EnableSensitiveData)
{
activity.SetTag(Tags.ExecutorInput, SerializeForTelemetry(message));
}
return activity;
}
///
/// Sets the executor output tag on an activity when sensitive data logging is enabled.
///
/// The activity to set the output on.
/// The output value to log.
public void SetExecutorOutput(Activity? activity, object? output)
{
if (activity is not null && this.Options.EnableSensitiveData)
{
activity.SetTag(Tags.ExecutorOutput, SerializeForTelemetry(output));
}
}
///
/// Starts an edge group process activity if enabled.
///
/// An activity if edge group process telemetry is enabled, otherwise null.
public Activity? StartEdgeGroupProcessActivity()
{
if (!this.IsEnabled || this.Options.DisableEdgeGroupProcess)
{
return null;
}
return this.ActivitySource.StartActivity(ActivityNames.EdgeGroupProcess);
}
///
/// Starts a message send activity if enabled, with all standard tags set.
///
/// The source executor identifier.
/// The target executor identifier, if any.
/// The message being sent. Logged only when is true.
/// An activity if message send telemetry is enabled, otherwise null.
public Activity? StartMessageSendActivity(string sourceId, string? targetId, object? message)
{
if (!this.IsEnabled || this.Options.DisableMessageSend)
{
return null;
}
Activity? activity = this.ActivitySource.StartActivity(ActivityNames.MessageSend, ActivityKind.Producer);
if (activity is null)
{
return null;
}
activity.SetTag(Tags.MessageSourceId, sourceId);
if (targetId is not null)
{
activity.SetTag(Tags.MessageTargetId, targetId);
}
if (this.Options.EnableSensitiveData)
{
activity.SetTag(Tags.MessageContent, SerializeForTelemetry(message));
}
return activity;
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Telemetry serialization is optional and only used when explicitly enabled.")]
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Telemetry serialization is optional and only used when explicitly enabled.")]
private static string? SerializeForTelemetry(object? value)
{
if (value is null)
{
return null;
}
try
{
return JsonSerializer.Serialize(value, value.GetType());
}
catch (JsonException)
{
return $"[Unserializable: {value.GetType().FullName}]";
}
}
}