// 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}]"; } } }