File: Diagnostics\ProfilingTelemetry.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Backchannel;
using Aspire.Hosting.Dcp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Trace;
 
namespace Aspire.Hosting.Diagnostics;
 
internal sealed class ProfilingTelemetry(IConfiguration configuration)
{
    public const string ActivitySourceName = "Aspire.Hosting.Profiling";
 
    internal static class Activities
    {
        // Activity names describe AppHost/DCP orchestration work. Keep names stable
        // because profiling exports are queried across CLI and AppHost versions.
        public const string DcpRunApplication = "aspire.hosting.dcp.run_application";
        public const string DcpPrepareTlsCertificate = "aspire.hosting.dcp.prepare_tls_certificate";
        public const string AppHostProcessStartup = "aspire.hosting.apphost.process_startup";
        public const string AppHostStart = "aspire.hosting.apphost.start";
        public const string AppHostBeforeStart = "aspire.hosting.apphost.before_start";
        public const string AppHostEventingSubscribers = "aspire.hosting.apphost.eventing_subscribers";
        public const string AppHostEventingSubscriber = "aspire.hosting.apphost.eventing_subscriber";
        public const string AppHostPublishEvent = "aspire.hosting.apphost.publish_event";
        public const string AppHostEventCallback = "aspire.hosting.apphost.event_callback";
        public const string AppHostLifecycleHooks = "aspire.hosting.apphost.lifecycle_hooks";
        public const string AppHostLifecycleHook = "aspire.hosting.apphost.lifecycle_hook";
        public const string AppHostBeforeStartPipeline = "aspire.hosting.apphost.before_start_pipeline";
        public const string AppHostHostStartup = "aspire.hosting.apphost.host_startup";
        public const string DcpPrepareServices = "aspire.hosting.dcp.prepare_services";
        public const string DcpPrepareResources = "aspire.hosting.dcp.prepare_resources";
        public const string DcpAllocateServiceAddresses = "aspire.hosting.dcp.allocate_service_addresses";
        public const string ResourceCreate = "aspire.hosting.resource.create";
        public const string DcpKubernetesApi = "aspire.hosting.dcp.kubernetes_api";
        public const string DcpEnsureKubernetesClient = "aspire.hosting.dcp.ensure_kubernetes_client";
        public const string DcpResourceObserved = "aspire.hosting.dcp.resource_observed";
        public const string ResourceStartup = "aspire.hosting.resource.startup";
        public const string ResourceBeforeStartWait = "aspire.hosting.resource.before_start_wait";
        public const string ResourceWaitForDependency = "aspire.hosting.resource.wait_for_dependency";
        public const string ResourceWaitForDependencies = "aspire.hosting.resource.wait_for_dependencies";
        public const string ResourceStop = "aspire.hosting.resource.stop";
        public const string ResourceStart = "aspire.hosting.resource.start";
        public const string BackchannelStartup = "aspire.hosting.backchannel.startup";
        public const string JsonRpcServerCall = "aspire.hosting.jsonrpc.server";
        public const string DashboardGetConnectionInfo = "aspire.hosting.dashboard.get_connection_info";
        public const string DashboardWaitHealthy = "aspire.hosting.dashboard.wait_healthy";
        public const string DashboardResolveUrls = "aspire.hosting.dashboard.resolve_urls";
    }
 
    internal static class Tags
    {
        // Tags capture dimensions and diagnostics for spans/events, such as resource
        // identity, DCP object identity, wait conditions, exit codes, and timing data.
        public const string ProfilingSessionId = "aspire.profiling.session_id";
        public const string LegacyStartupOperationId = "aspire.startup.operation_id";
        public const string AppHostName = "aspire.apphost.name";
        public const string AppHostOperation = "aspire.apphost.operation";
        public const string AppHostEntryPoint = "aspire.apphost.entry_point";
        public const string AppHostEventType = "aspire.apphost.event.type";
        public const string AppHostEventDispatchBehavior = "aspire.apphost.event.dispatch_behavior";
        public const string AppHostEventSubscriberCount = "aspire.apphost.event.subscriber_count";
        public const string AppHostLifecycleHookCount = "aspire.apphost.lifecycle_hook_count";
        public const string AppHostComponentType = "aspire.apphost.component.type";
        public const string AppHostPipelineStep = "aspire.apphost.pipeline.step";
        public const string ResourceName = "aspire.resource.name";
        public const string ResourceId = "aspire.resource.id";
        public const string ResourceType = "aspire.resource.type";
        public const string ResourceKind = "aspire.resource.kind";
        public const string ResourceCount = "aspire.resource.count";
        public const string ResourceReplicaCount = "aspire.resource.replica_count";
        public const string ResourceStopped = "aspire.resource.stopped";
        public const string ResourceState = "aspire.resource.state";
        public const string ResourceHealthStatus = "aspire.resource.health_status";
        public const string ResourceExitCode = "aspire.resource.exit_code";
        public const string ResourceReady = "aspire.resource.ready";
        public const string ResourceSnapshotVersion = "aspire.resource.snapshot.version";
        public const string ResourceStartTime = "aspire.resource.start_time";
        public const string ResourceStopTime = "aspire.resource.stop_time";
        public const string ResourceWaitExpectedExitCode = "aspire.resource.wait.expected_exit_code";
        public const string ResourceWaitDependencyCount = "aspire.resource.wait.dependency_count";
        public const string ResourceWaitType = "aspire.resource.wait.type";
        public const string ResourceWaitDependencyName = "aspire.resource.wait.dependency.name";
        public const string ResourceWaitDependencyType = "aspire.resource.wait.dependency.type";
        public const string ResourceWaitBehavior = "aspire.resource.wait.behavior";
        public const string ResourceWaitTargetName = "aspire.resource.wait.target.name";
        public const string ResourceWaitCondition = "aspire.resource.wait.condition";
        public const string DcpResourceName = "aspire.dcp.resource.name";
        public const string DcpResourceKind = "aspire.dcp.resource.kind";
        public const string DcpResourceCount = "aspire.dcp.resource.count";
        public const string DcpContainerCount = "aspire.dcp.container.count";
        public const string DcpExecutableCount = "aspire.dcp.executable.count";
        public const string DcpServiceCount = "aspire.dcp.service.count";
        public const string DcpServiceAllocatedCount = "aspire.dcp.service.allocated_count";
        public const string DcpServiceName = "aspire.dcp.service.name";
        public const string DcpApiOperation = "aspire.dcp.api.operation";
        public const string DcpApiRetryCount = "aspire.dcp.api.retry_count";
        public const string DcpApiRetryAttempt = "aspire.dcp.api.retry_attempt";
        public const string DcpApiRetryDelayMilliseconds = "aspire.dcp.api.retry_delay_ms";
        public const string DcpKubeconfigExists = "aspire.dcp.kubeconfig.exists";
        public const string DcpKubeconfigLockWaitMilliseconds = "aspire.dcp.kubeconfig.lock_wait_ms";
        public const string DcpKubeconfigFileWaitMilliseconds = "aspire.dcp.kubeconfig.file_wait_ms";
        public const string DcpKubeconfigBuildDurationMilliseconds = "aspire.dcp.kubeconfig.build_duration_ms";
        public const string DcpKubeconfigReadDurationMilliseconds = "aspire.dcp.kubeconfig.read_duration_ms";
        public const string DcpKubernetesClientWaitMilliseconds = "aspire.dcp.kubernetes_client.wait_ms";
        public const string DcpKubernetesClientAlreadyInitialized = "aspire.dcp.kubernetes_client_already_initialized";
        public const string DcpKubernetesClientInitialized = "aspire.dcp.kubernetes_client.initialized";
        public const string DcpTlsDeveloperCertificateEnabled = "aspire.dcp.tls.developer_certificate.enabled";
        public const string DcpTlsCertificateMode = "aspire.dcp.tls.certificate.mode";
        public const string DcpTlsCertificatePrepared = "aspire.dcp.tls.certificate.prepared";
        public const string DcpTlsCertificateResult = "aspire.dcp.tls.certificate.result";
        public const string BackchannelSocketPath = "aspire.hosting.backchannel.socket.path";
        public const string PreviousResourceState = "aspire.resource.previous_state";
        public const string PreviousResourceHealthStatus = "aspire.resource.previous_health_status";
        public const string JsonRpcMethod = "rpc.method";
        public const string JsonRpcStreaming = "aspire.hosting.jsonrpc.streaming";
        public const string JsonRpcStreamItemCount = "aspire.hosting.jsonrpc.stream.item_count";
        public const string DashboardHealthy = "aspire.hosting.dashboard.healthy";
        public const string DashboardUrlSource = "aspire.hosting.dashboard.url.source";
        public const string DashboardHasApiBaseUrl = "aspire.hosting.dashboard.api_base_url.exists";
        public const string ExceptionType = "exception.type";
        public const string ExceptionMessage = "exception.message";
    }
 
    internal static class Events
    {
        // Events mark important moments within longer spans, for example retries,
        // readiness observations, resource wait completions, and exception details.
        public const string DcpServiceAddressAllocated = "aspire.dcp.service_address_allocated";
        public const string DcpServiceAddressAllocationFailed = "aspire.dcp.service_address_allocation_failed";
        public const string AppHostCreateBuilderEntered = "aspire.hosting.apphost.create_builder_entered";
        public const string AppHostBuilderConstructing = "aspire.hosting.apphost.builder_constructing";
        public const string AppHostBuilderConstructed = "aspire.hosting.apphost.builder_constructed";
        public const string AppHostBuildStarted = "aspire.hosting.apphost.build_started";
        public const string AppHostBuildCompleted = "aspire.hosting.apphost.build_completed";
        public const string AppHostStartAsyncEntered = "aspire.hosting.apphost.start_async_entered";
        public const string AppHostRunAsyncEntered = "aspire.hosting.apphost.run_async_entered";
        public const string AppHostHostStarting = "aspire.hosting.apphost.host_starting";
        public const string AppHostHostStarted = "aspire.hosting.apphost.host_started";
        public const string KubernetesApiTimeout = "aspire.hosting.dcp.kubernetes_api.timeout";
        public const string KubernetesApiRetry = "aspire.hosting.dcp.kubernetes_api.retry";
        public const string KubeconfigLockAcquired = "aspire.hosting.dcp.kubeconfig_lock_acquired";
        public const string KubeconfigFileDetected = "aspire.hosting.dcp.kubeconfig_file_detected";
        public const string KubeconfigReadComplete = "aspire.hosting.dcp.kubeconfig_read_complete";
        public const string KubernetesClientCreated = "aspire.hosting.dcp.kubernetes_client_created";
        public const string KubernetesClientReady = "aspire.hosting.dcp.kubernetes_client_ready";
        public const string ResourceStartupObserved = "aspire.resource.startup.observed";
        public const string ResourceStartupStateChanged = "aspire.resource.startup.state_changed";
        public const string ResourceStartupHealthChanged = "aspire.resource.startup.health_changed";
        public const string ResourceStartupReady = "aspire.resource.startup.ready";
        public const string ResourceWaitObserved = "aspire.resource.wait.observed";
        public const string ResourceWaitCompleted = "aspire.resource.wait.completed";
        public const string ResourceWaitCancelled = "aspire.resource.wait.cancelled";
        public const string BackchannelSocketDeleted = "aspire.hosting.backchannel.socket_deleted";
        public const string BackchannelListening = "aspire.hosting.backchannel.listening";
        public const string BackchannelReadyPublished = "aspire.hosting.backchannel.ready_published";
        public const string BackchannelClientAccepted = "aspire.hosting.backchannel.client_accepted";
        public const string BackchannelRpcListening = "aspire.hosting.backchannel.rpc_listening";
        public const string BackchannelConnectedPublished = "aspire.hosting.backchannel.connected_published";
        public const string Exception = "exception";
        public const string JsonRpcStreamFirstItem = "aspire.hosting.jsonrpc.stream.first_item";
        public const string JsonRpcStreamCompleted = "aspire.hosting.jsonrpc.stream.completed";
        public const string DashboardWaitHealthyCompleted = "aspire.hosting.dashboard.wait_healthy.completed";
        public const string DashboardWaitHealthyFailed = "aspire.hosting.dashboard.wait_healthy.failed";
    }
 
    internal static class Values
    {
        public const string DashboardUrlSourceNone = "none";
        public const string DashboardUrlSourceResource = "resource";
        public const string DashboardUrlSourceConfiguration = "configuration";
        public const string DcpTlsCertificateModeFiles = "files";
        public const string DcpTlsCertificateModeThumbprint = "thumbprint";
        public const string DcpTlsCertificateResultMissingThumbprint = "missing_thumbprint";
        public const string DcpTlsCertificateResultNoCertificate = "no_certificate";
        public const string DcpTlsCertificateResultNoPrivateKeyCertificate = "no_private_key_certificate";
        public const string DcpTlsCertificateResultPrepared = "prepared";
    }
 
    internal static class Annotations
    {
        // DCP annotations carry profiling trace context through rendered resources so
        // later watch/reconcile notifications can reconnect to the resource creation span.
        public const string ProfilingSessionId = "aspire-profiling-session-id";
        public const string TraceParent = "aspire-profiling-traceparent";
        public const string TraceState = "aspire-profiling-tracestate";
        public const string LegacyStartupOperationId = "aspire-startup-operation-id";
        public const string LegacyStartupTraceParent = "aspire-startup-traceparent";
        public const string LegacyStartupTraceState = "aspire-startup-tracestate";
    }
 
    private static readonly ActivitySource s_activitySource = new(ActivitySourceName);
 
    internal static ActivitySource ActivitySource => s_activitySource;
 
    private static readonly ConcurrentQueue<AppHostStartupEvent> s_startupEvents = new();
 
    private readonly IConfiguration _configuration = configuration;
 
    // These static helpers are used before ProfilingTelemetry can be resolved from DI. Some startup
    // milestones occur before HostApplicationBuilder has created IConfiguration or before OpenTelemetry
    // has built the TracerProvider, so buffer timestamps here and attach them later to the process-start
    // span without forcing the full telemetry stack to initialize too soon.
    public static void RecordAppHostStartupEvent(string eventName, IConfiguration? configuration = null)
    {
        // CreateBuilder-entered events happen before IConfiguration exists. Later startup phases
        // pass configuration explicitly so command-line/config providers are honored as soon as
        // they are available.
        if (!IsStartupEventRecordingEnabled(configuration))
        {
            return;
        }
 
        s_startupEvents.Enqueue(new AppHostStartupEvent(eventName, DateTimeOffset.UtcNow));
    }
 
    public static ActivityScope StartAppHostProcessStartup(IConfiguration? configuration)
    {
        if (!IsEnabled(configuration))
        {
            return default;
        }
 
        var activity = StartActivity(configuration, Activities.AppHostProcessStartup, ActivityKind.Internal, GetProcessStartTime());
        activity.AddAppHostStartupEvents(DrainAppHostStartupEvents());
        return activity;
    }
 
    public static void RecordAppHostProcessStartup(IConfiguration? configuration)
    {
        using var activity = StartAppHostProcessStartup(configuration);
    }
 
    public static void EnsureInitialized(IServiceProvider services)
    {
        var configuration = services.GetRequiredService<IConfiguration>();
        if (IsEnabled(configuration))
        {
            // OpenTelemetry.Extensions.Hosting normally builds the TracerProvider when the host starts.
            // Before-start hooks run before that, so force provider construction when self-profiling is
            // enabled or the spans that explain pre-hosted-service startup work would be dropped.
            _ = services.GetService<TracerProvider>();
        }
    }
 
    public static ActivityScope CurrentActivity(IConfiguration? configuration) =>
        IsEnabled(configuration) ? new(Activity.Current, configuration, ownsActivity: false) : default;
 
    public static IEnumerable<KeyValuePair<string, object>> CreateAppHostResourceAttributes(string appHostPath, string operation)
    {
        return
        [
            new(Tags.AppHostName, Path.GetFileName(appHostPath)),
            new(Tags.AppHostOperation, operation)
        ];
    }
 
    public static ActivityScope StartDcpRunApplication(IConfiguration? configuration, int resourceCount)
    {
        var activity = StartActivity(configuration, Activities.DcpRunApplication);
        activity.SetResourceCount(resourceCount);
        return activity;
    }
 
    public static ActivityScope StartDcpPrepareTlsCertificate(IConfiguration? configuration)
    {
        var activity = StartActivity(configuration, Activities.DcpPrepareTlsCertificate);
        activity.SetDcpTlsDeveloperCertificateEnabled(true);
        return activity;
    }
 
    public static ActivityScope StartAppHostStart(IConfiguration? configuration, string entryPoint)
    {
        var activity = StartActivity(configuration, Activities.AppHostStart);
        activity.SetAppHostEntryPoint(entryPoint);
        return activity;
    }
 
    public static ActivityScope StartAppHostBeforeStart(IConfiguration? configuration)
    {
        return StartActivity(configuration, Activities.AppHostBeforeStart);
    }
 
    public static ActivityScope StartAppHostEventingSubscribers(IConfiguration? configuration, int subscriberCount)
    {
        var activity = StartActivity(configuration, Activities.AppHostEventingSubscribers);
        activity.SetAppHostEventSubscriberCount(subscriberCount);
        return activity;
    }
 
    public static ActivityScope StartAppHostEventingSubscriber(IConfiguration? configuration, Type subscriberType)
    {
        var activity = StartActivity(configuration, Activities.AppHostEventingSubscriber);
        activity.SetAppHostComponentType(subscriberType);
        return activity;
    }
 
    public static ActivityScope StartAppHostPublishEvent(IConfiguration? configuration, Type eventType)
    {
        var activity = StartActivity(configuration, Activities.AppHostPublishEvent);
        activity.SetAppHostEvent(eventType);
        return activity;
    }
 
    public static ActivityScope StartAppHostEventCallback(Type eventType, string dispatchBehavior)
    {
        var parentActivity = Activity.Current;
        if (parentActivity?.Source.Name != ActivitySourceName ||
            parentActivity.OperationName != Activities.AppHostPublishEvent)
        {
            return default;
        }
 
        var activity = s_activitySource.StartActivity(Activities.AppHostEventCallback);
        AddProfilingSessionId(activity, parentActivity.GetBaggageItem(Tags.ProfilingSessionId));
        var scope = new ActivityScope(activity);
        scope.SetAppHostEvent(eventType, dispatchBehavior);
        return scope;
    }
 
    public static ActivityScope StartAppHostLifecycleHooks(IConfiguration? configuration, int lifecycleHookCount)
    {
        var activity = StartActivity(configuration, Activities.AppHostLifecycleHooks);
        activity.SetAppHostLifecycleHookCount(lifecycleHookCount);
        return activity;
    }
 
    public static ActivityScope StartAppHostLifecycleHook(IConfiguration? configuration, Type lifecycleHookType)
    {
        var activity = StartActivity(configuration, Activities.AppHostLifecycleHook);
        activity.SetAppHostComponentType(lifecycleHookType);
        return activity;
    }
 
    public static ActivityScope StartAppHostBeforeStartPipeline(IConfiguration? configuration, string pipelineStep)
    {
        var activity = StartActivity(configuration, Activities.AppHostBeforeStartPipeline);
        activity.SetAppHostPipelineStep(pipelineStep);
        return activity;
    }
 
    public ActivityScope StartAppHostHostStartup()
    {
        return StartActivity(Activities.AppHostHostStartup);
    }
 
    public static ActivityScope StartDcpPrepareServices(IConfiguration? configuration)
    {
        return StartActivity(configuration, Activities.DcpPrepareServices);
    }
 
    public static ActivityScope StartDcpPrepareResources(IConfiguration? configuration)
    {
        return StartActivity(configuration, Activities.DcpPrepareResources);
    }
 
    public static ActivityScope StartDcpAllocateServiceAddresses(IConfiguration? configuration, int serviceCount)
    {
        var activity = StartActivity(configuration, Activities.DcpAllocateServiceAddresses);
        activity.SetDcpServiceCount(serviceCount);
        return activity;
    }
 
    public static ActivityScope StartDcpEnsureKubernetesClient(IConfiguration? configuration, bool kubeconfigExists)
    {
        var activity = StartActivity(configuration, Activities.DcpEnsureKubernetesClient);
        activity.SetDcpKubeconfigExists(kubeconfigExists);
        return activity;
    }
 
    public static ActivityScope StartDcpKubernetesApi(IConfiguration? configuration, DcpApiOperationType operationType, string resourceType)
    {
        var activity = StartActivity(configuration, Activities.DcpKubernetesApi);
        activity.SetDcpKubernetesApi(operationType, resourceType);
        return activity;
    }
 
    public ActivityScope StartDcpResourceObserved(
        IResource appModelResource,
        string resourceKind,
        string resourceName,
        string? state,
        DateTime? startupTimestamp,
        DateTime? finishedTimestamp,
        IDictionary<string, string>? annotations)
    {
        // Resource observations arrive from DCP watch notifications after the outbound Kubernetes create call
        // has ended, so use a short child activity from the propagated profiling trace context.
        var activity = StartActivityFromTraceAnnotations(Activities.DcpResourceObserved, annotations);
        activity.SetResource(appModelResource);
        activity.SetDcpResource(resourceKind, resourceName);
        activity.SetResourceObserved(state, startupTimestamp, finishedTimestamp);
        return activity;
    }
 
    public static ActivityScope StartResourceBeforeStartWait(IConfiguration? configuration, IResource resource)
    {
        var activity = StartActivity(configuration, Activities.ResourceBeforeStartWait);
        activity.SetResource(resource);
        return activity;
    }
 
    public static ActivityScope StartResourceCreate(IConfiguration? configuration, IResource resource, string resourceKind, int replicaCount)
    {
        var activity = StartActivity(configuration, Activities.ResourceCreate);
        activity.SetResource(resource);
        activity.SetResourceCreate(resourceKind, replicaCount);
        return activity;
    }
 
    public static ActivityScope StartResourceStartup(
        IConfiguration? configuration,
        IResource resource,
        string resourceId,
        CustomResourceSnapshot snapshot,
        DateTimeOffset startTime)
    {
        var activity = StartActivity(configuration, $"{Activities.ResourceStartup} {resource.Name}", ActivityKind.Internal, startTime);
        activity.SetResourceLifecycle(resource, resourceId, snapshot);
        return activity;
    }
 
    public static ActivityScope StartResourceStart(IConfiguration? configuration, IResource resource, string resourceKind, string resourceName, string resourceType)
    {
        var activity = StartActivity(configuration, Activities.ResourceStart);
        activity.SetResource(resource);
        activity.SetDcpResource(resourceKind, resourceName);
        activity.SetResourceKind(resourceType);
        return activity;
    }
 
    public ActivityScope StartBackchannelStartup(string socketPath)
    {
        var activity = StartActivity(Activities.BackchannelStartup);
        activity.SetBackchannelSocketPath(socketPath);
        return activity;
    }
 
    public ActivityScope StartJsonRpcServerCall(string methodName, bool streaming, BackchannelTraceContext? traceContext = null)
    {
        var activity = StartActivityFromTraceContext(Activities.JsonRpcServerCall, ActivityKind.Server, traceContext);
        activity.SetJsonRpcCall(methodName, streaming);
        return activity;
    }
 
    public ActivityScope StartDashboardGetConnectionInfo()
    {
        return StartActivity(Activities.DashboardGetConnectionInfo);
    }
 
    public ActivityScope StartDashboardWaitHealthy()
    {
        return StartActivity(Activities.DashboardWaitHealthy);
    }
 
    public ActivityScope StartDashboardResolveUrls()
    {
        return StartActivity(Activities.DashboardResolveUrls);
    }
 
    public static ActivityScope StartResourceStop(IConfiguration? configuration, IResource resource, string resourceKind, string resourceName)
    {
        var activity = StartActivity(configuration, Activities.ResourceStop);
        activity.SetResource(resource);
        activity.SetDcpResource(resourceKind, resourceName);
        return activity;
    }
 
    public static ActivityScope StartResourceWaitForDependencies(IConfiguration? configuration, IResource resource, int dependencyCount)
    {
        var activity = StartActivity(configuration, Activities.ResourceWaitForDependencies);
        activity.SetResource(resource);
        activity.SetResourceWaitDependencyCount(dependencyCount);
        return activity;
    }
 
    public static ActivityScope StartResourceWaitForDependency(IConfiguration? configuration, IResource resource, IResource dependency, WaitType waitType, WaitBehavior? waitBehavior)
    {
        var activity = StartActivity(configuration, Activities.ResourceWaitForDependency);
        activity.SetDependencyWait(resource, dependency, waitType, waitBehavior);
        return activity;
    }
 
    private static ActivityScope StartActivity(IConfiguration? configuration, string name, ActivityKind activityKind = ActivityKind.Internal)
    {
        if (!IsEnabled(configuration))
        {
            return default;
        }
 
        var activity = (Activity.Current is null || Activity.Current.Source.Name != ActivitySourceName) &&
            TryGetProfilingParentContext(configuration, out var parentContext)
            ? s_activitySource.StartActivity(name, activityKind, parentContext)
            : s_activitySource.StartActivity(name, activityKind);
 
        AddProfilingSessionId(activity, configuration);
        return new ActivityScope(activity, configuration);
    }
 
    private static ActivityScope StartActivity(IConfiguration? configuration, string name, ActivityKind activityKind, DateTimeOffset startTime)
    {
        if (!IsEnabled(configuration))
        {
            return default;
        }
 
        Activity? activity;
        if (TryGetProfilingParentContext(configuration, out var parentContext))
        {
            // Backdated lifecycle spans summarize async resource state changes and can start before the
            // short-lived publish/update span that observes the milestone. Keep them under the profiling
            // root instead of making them children of an ambient activity that may start later.
            activity = s_activitySource.StartActivity(name, activityKind, parentContext, tags: null, links: null, startTime: startTime);
        }
        else if (Activity.Current is { } currentActivity)
        {
            activity = s_activitySource.StartActivity(name, activityKind, currentActivity.Context, tags: null, links: null, startTime: startTime);
        }
        else
        {
            activity = s_activitySource.StartActivity(name, activityKind, parentContext: default, tags: null, links: null, startTime: startTime);
        }
 
        AddProfilingSessionId(activity, configuration);
        return new ActivityScope(activity, configuration);
    }
 
    private ActivityScope StartActivity(string name, ActivityKind activityKind = ActivityKind.Internal)
    {
        return StartActivity(_configuration, name, activityKind);
    }
 
    private ActivityScope StartActivityFromTraceAnnotations(string name, IDictionary<string, string>? annotations)
    {
        if (!IsEnabled(_configuration))
        {
            return default;
        }
 
        Activity? activity = null;
        if (annotations is not null &&
            TryGetAnnotation(annotations, Annotations.TraceParent, Annotations.LegacyStartupTraceParent, out var traceParent))
        {
            // DCP annotations carry the create_object trace context to later watch/reconcile spans.
            TryGetAnnotation(annotations, Annotations.TraceState, Annotations.LegacyStartupTraceState, out var traceState);
            if (ActivityContext.TryParse(traceParent, traceState, out var parentContext))
            {
                activity = s_activitySource.StartActivity(name, ActivityKind.Internal, parentContext);
            }
        }
 
        if (activity is null)
        {
            return StartActivity(name);
        }
 
        AddProfilingSessionId(activity, _configuration, annotations);
 
        return new ActivityScope(activity, _configuration);
    }
 
    private ActivityScope StartActivityFromTraceContext(string name, ActivityKind activityKind, BackchannelTraceContext? traceContext)
    {
        if (!IsEnabled(_configuration))
        {
            return default;
        }
 
        Activity? activity;
        if (TryGetBackchannelTraceParent(traceContext, out var traceContextParent))
        {
            activity = s_activitySource.StartActivity(name, activityKind, traceContextParent);
        }
        else if (TryGetAmbientRemoteParentContext(out var ambientParent))
        {
            // StreamJsonRpc's ActivityTracingStrategy creates an unexported server activity
            // from the caller's W3C traceparent. Parent profiling spans to the remote caller
            // instead of that hidden activity so exported CLI and Hosting spans are adjacent.
            activity = s_activitySource.StartActivity(name, activityKind, ambientParent);
        }
        else if ((Activity.Current is null || Activity.Current.Source.Name != ActivitySourceName) &&
            TryGetProfilingParentContext(_configuration, out var configuredParent))
        {
            activity = s_activitySource.StartActivity(name, activityKind, configuredParent);
        }
        else
        {
            activity = s_activitySource.StartActivity(name, activityKind);
        }
 
        AddBaggage(activity, traceContext);
        AddProfilingSessionId(activity, _configuration, traceContext);
        return new ActivityScope(activity, _configuration);
    }
 
    private static void AddProfilingSessionId(Activity? activity, IConfiguration? configuration, IDictionary<string, string>? annotations = null)
    {
        if (activity is null)
        {
            return;
        }
 
        var sessionId = annotations is not null && TryGetAnnotation(annotations, Annotations.ProfilingSessionId, Annotations.LegacyStartupOperationId, out var annotationSessionId)
            ? annotationSessionId
            : GetConfigurationValue(configuration, KnownConfigNames.ProfilingSessionId, KnownConfigNames.Legacy.StartupOperationId);
        if (!string.IsNullOrEmpty(sessionId))
        {
            activity.SetBaggage(Tags.ProfilingSessionId, sessionId);
            activity.SetTag(Tags.ProfilingSessionId, sessionId);
            activity.SetTag(Tags.LegacyStartupOperationId, sessionId);
        }
    }
 
    private static void AddProfilingSessionId(Activity? activity, string? sessionId)
    {
        if (activity is null || string.IsNullOrEmpty(sessionId))
        {
            return;
        }
 
        activity.SetBaggage(Tags.ProfilingSessionId, sessionId);
        activity.SetTag(Tags.ProfilingSessionId, sessionId);
        activity.SetTag(Tags.LegacyStartupOperationId, sessionId);
    }
 
    private static void AddBaggage(Activity? activity, BackchannelTraceContext? traceContext)
    {
        if (activity is null || traceContext is null)
        {
            return;
        }
 
        foreach (var (key, value) in traceContext.Baggage)
        {
            activity.SetBaggage(key, value);
        }
    }
 
    private static void AddProfilingSessionId(Activity? activity, IConfiguration? configuration, BackchannelTraceContext? traceContext)
    {
        if (activity is null)
        {
            return;
        }
 
        var sessionId = traceContext?.Baggage.TryGetValue(Tags.ProfilingSessionId, out var baggageSessionId) == true && !string.IsNullOrEmpty(baggageSessionId)
            ? baggageSessionId
            : GetConfigurationValue(configuration, KnownConfigNames.ProfilingSessionId, KnownConfigNames.Legacy.StartupOperationId);
        if (!string.IsNullOrEmpty(sessionId))
        {
            activity.SetBaggage(Tags.ProfilingSessionId, sessionId);
            activity.SetTag(Tags.ProfilingSessionId, sessionId);
            activity.SetTag(Tags.LegacyStartupOperationId, sessionId);
        }
    }
 
    private static bool TryGetBackchannelTraceParent(BackchannelTraceContext? traceContext, out ActivityContext parentContext)
    {
        if (!string.IsNullOrEmpty(traceContext?.TraceParent) &&
            ActivityContext.TryParse(traceContext.TraceParent, traceContext.TraceState, out parentContext))
        {
            return true;
        }
 
        parentContext = default;
        return false;
    }
 
    private static bool TryGetAmbientRemoteParentContext(out ActivityContext parentContext)
    {
        var ambientActivity = Activity.Current;
        if (ambientActivity is not null &&
            ambientActivity.Source.Name != ActivitySourceName &&
            ambientActivity.Parent is null &&
            ambientActivity.ParentSpanId != default)
        {
            parentContext = new ActivityContext(
                ambientActivity.TraceId,
                ambientActivity.ParentSpanId,
                ambientActivity.ActivityTraceFlags,
                ambientActivity.TraceStateString,
                isRemote: true);
            return true;
        }
 
        parentContext = default;
        return false;
    }
 
    private static bool TryGetProfilingParentContext(IConfiguration? configuration, out ActivityContext parentContext)
    {
        var traceParent = GetConfigurationValue(configuration, KnownConfigNames.ProfilingTraceParent, KnownConfigNames.Legacy.StartupTraceParent);
        var traceState = GetConfigurationValue(configuration, KnownConfigNames.ProfilingTraceState, KnownConfigNames.Legacy.StartupTraceState);
        if (string.IsNullOrEmpty(traceParent))
        {
            parentContext = default;
            return false;
        }
 
        return ActivityContext.TryParse(traceParent, traceState, out parentContext);
    }
 
    internal static bool IsEnabled(IConfiguration? configuration)
    {
        return configuration?.GetBool(KnownConfigNames.ProfilingEnabled, KnownConfigNames.Legacy.StartupProfilingEnabled) is true;
    }
 
    private static bool IsStartupEventRecordingEnabled(IConfiguration? configuration)
    {
        if (configuration is not null)
        {
            return IsEnabled(configuration);
        }
 
        return IsTruthy(Environment.GetEnvironmentVariable(KnownConfigNames.ProfilingEnabled)) ||
            IsTruthy(Environment.GetEnvironmentVariable(KnownConfigNames.Legacy.StartupProfilingEnabled));
    }
 
    private static bool TryGetAnnotation(IDictionary<string, string> annotations, string name, string legacyName, out string? value)
    {
        if (annotations.TryGetValue(name, out value) && !string.IsNullOrEmpty(value))
        {
            return true;
        }
 
        return annotations.TryGetValue(legacyName, out value) && !string.IsNullOrEmpty(value);
    }
 
    private static string? GetConfigurationValue(IConfiguration? configuration, string name, string legacyName)
    {
        return configuration?[name] is { Length: > 0 } value
            ? value
            : configuration?[legacyName];
    }
 
    private static bool IsTruthy(string? value)
    {
        return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || value == "1";
    }
 
    private static DateTimeOffset GetProcessStartTime()
    {
        return new DateTimeOffset(Process.GetCurrentProcess().StartTime.ToUniversalTime(), TimeSpan.Zero);
    }
 
    private static AppHostStartupEvent[] DrainAppHostStartupEvents()
    {
        // Move the pre-DI startup timestamps into the process-start activity exactly once. The queue
        // is static because the earliest records happen before DI is available, but draining here
        // prevents the same milestones from leaking into later AppHost instances in this process.
        // ConcurrentQueue.TryDequeue atomically claims each event, so callers that race with the
        // drain do not need a separate lock.
        if (s_startupEvents.IsEmpty)
        {
            return [];
        }
 
        var events = new List<AppHostStartupEvent>();
        while (s_startupEvents.TryDequeue(out var startupEvent))
        {
            events.Add(startupEvent);
        }
 
        return events.ToArray();
    }
 
    internal readonly record struct AppHostStartupEvent(string Name, DateTimeOffset Timestamp);
 
    internal readonly struct ActivityScope(Activity? activity, IConfiguration? configuration = null, bool ownsActivity = true) : IDisposable
    {
        public void AddDcpServiceAddressAllocated(string serviceName)
        {
            activity?.AddEvent(new ActivityEvent(Events.DcpServiceAddressAllocated, tags: new ActivityTagsCollection
            {
                [Tags.DcpServiceName] = serviceName
            }));
        }
 
        public void AddDcpServiceAddressAllocationFailed(string serviceName)
        {
            activity?.AddEvent(new ActivityEvent(Events.DcpServiceAddressAllocationFailed, tags: new ActivityTagsCollection
            {
                [Tags.DcpServiceName] = serviceName
            }));
        }
 
        public void AddAppHostHostStarting() => AddEvent(Events.AppHostHostStarting);
 
        public void AddAppHostHostStarted() => AddEvent(Events.AppHostHostStarted);
 
        internal void AddAppHostStartupEvents(ReadOnlySpan<AppHostStartupEvent> events)
        {
            if (activity is null)
            {
                return;
            }
 
            foreach (var startupEvent in events)
            {
                activity.AddEvent(new ActivityEvent(startupEvent.Name, startupEvent.Timestamp));
            }
        }
 
        public void AddKubeconfigLockAcquired() => AddEvent(Events.KubeconfigLockAcquired);
 
        public void AddKubeconfigReadComplete() => AddEvent(Events.KubeconfigReadComplete);
 
        public void AddKubernetesApiRetry(int attemptNumber, TimeSpan retryDelay, Exception? exception)
        {
            activity?.AddEvent(new ActivityEvent(Events.KubernetesApiRetry, tags: new ActivityTagsCollection
            {
                [Tags.DcpApiRetryAttempt] = attemptNumber,
                [Tags.DcpApiRetryDelayMilliseconds] = retryDelay.TotalMilliseconds,
                [Tags.ExceptionType] = exception?.GetType().FullName,
                [Tags.ExceptionMessage] = exception?.Message
            }));
        }
 
        public void AddKubernetesApiTimeout() => AddEvent(Events.KubernetesApiTimeout);
 
        public void AddKubernetesClientCreated() => AddEvent(Events.KubernetesClientCreated);
 
        public void AddJsonRpcStreamFirstItemEvent() => AddEvent(Events.JsonRpcStreamFirstItem);
 
        public void AddJsonRpcStreamCompletedEvent() => AddEvent(Events.JsonRpcStreamCompleted);
 
        public void AddDashboardWaitHealthyCompleted() => AddEvent(Events.DashboardWaitHealthyCompleted);
 
        public void AddDashboardWaitHealthyFailed() => AddEvent(Events.DashboardWaitHealthyFailed);
 
        public void AddResourceWaitCancelled(string resourceName, string waitCondition)
        {
            activity?.AddEvent(new ActivityEvent(Events.ResourceWaitCancelled, tags: new ActivityTagsCollection
            {
                [Tags.ResourceWaitTargetName] = resourceName,
                [Tags.ResourceWaitCondition] = waitCondition
            }));
        }
 
        public void AddResourceWaitCompleted(ResourceEvent resourceEvent, string waitCondition) =>
            AddResourceWaitEvent(Events.ResourceWaitCompleted, resourceEvent, waitCondition);
 
        public void AddResourceWaitObserved(ResourceEvent resourceEvent, string waitCondition) =>
            AddResourceWaitEvent(Events.ResourceWaitObserved, resourceEvent, waitCondition);
 
        public void AnnotateTraceContext(Action<string, string> annotate)
        {
            if (!IsEnabled(configuration))
            {
                return;
            }
 
            var sessionId = GetConfigurationValue(configuration, KnownConfigNames.ProfilingSessionId, KnownConfigNames.Legacy.StartupOperationId);
            if (!string.IsNullOrEmpty(sessionId))
            {
                annotate(Annotations.ProfilingSessionId, sessionId);
                annotate(Annotations.LegacyStartupOperationId, sessionId);
            }
 
            var traceParent = activity?.Id ?? GetConfigurationValue(configuration, KnownConfigNames.ProfilingTraceParent, KnownConfigNames.Legacy.StartupTraceParent);
            if (!string.IsNullOrEmpty(traceParent))
            {
                annotate(Annotations.TraceParent, traceParent);
                annotate(Annotations.LegacyStartupTraceParent, traceParent);
            }
 
            var traceState = activity?.TraceStateString ?? GetConfigurationValue(configuration, KnownConfigNames.ProfilingTraceState, KnownConfigNames.Legacy.StartupTraceState);
            if (!string.IsNullOrEmpty(traceState))
            {
                annotate(Annotations.TraceState, traceState);
                annotate(Annotations.LegacyStartupTraceState, traceState);
            }
        }
 
        public void SetDcpKubeconfigExists(bool exists) => SetTag(Tags.DcpKubeconfigExists, exists);
 
        public void SetDcpKubeconfigLockWait(long elapsedMilliseconds) => SetTag(Tags.DcpKubeconfigLockWaitMilliseconds, elapsedMilliseconds);
 
        public void SetDcpKubeconfigFileWait(long elapsedMilliseconds) => SetTag(Tags.DcpKubeconfigFileWaitMilliseconds, elapsedMilliseconds);
 
        public void SetDcpKubeconfigBuildDuration(long elapsedMilliseconds) => SetTag(Tags.DcpKubeconfigBuildDurationMilliseconds, elapsedMilliseconds);
 
        public void SetDcpKubeconfigReadDuration(long elapsedMilliseconds) => SetTag(Tags.DcpKubeconfigReadDurationMilliseconds, elapsedMilliseconds);
 
        public void AddKubeconfigFileDetected() => AddEvent(Events.KubeconfigFileDetected);
 
        public void AddKubernetesClientReady(long waitMilliseconds, bool initialized)
        {
            SetTag(Tags.DcpKubernetesClientWaitMilliseconds, waitMilliseconds);
            SetTag(Tags.DcpKubernetesClientInitialized, initialized);
            activity?.AddEvent(new ActivityEvent(Events.KubernetesClientReady, tags: new ActivityTagsCollection
            {
                [Tags.DcpKubernetesClientWaitMilliseconds] = waitMilliseconds,
                [Tags.DcpKubernetesClientInitialized] = initialized
            }));
        }
 
        public void SetDcpKubernetesApi(DcpApiOperationType operationType, string resourceType)
        {
            SetTag(Tags.DcpApiOperation, operationType.ToString());
            SetTag(Tags.DcpResourceKind, resourceType);
        }
 
        public void SetDcpTlsDeveloperCertificateEnabled(bool enabled) => SetTag(Tags.DcpTlsDeveloperCertificateEnabled, enabled);
 
        public void SetDcpTlsCertificateResult(string result, string? mode = null, bool prepared = false)
        {
            SetTag(Tags.DcpTlsCertificateResult, result);
            SetTag(Tags.DcpTlsCertificatePrepared, prepared);
 
            if (!string.IsNullOrEmpty(mode))
            {
                SetTag(Tags.DcpTlsCertificateMode, mode);
            }
        }
 
        public void SetAppHostEntryPoint(string entryPoint) => SetTag(Tags.AppHostEntryPoint, entryPoint);
 
        public void SetAppHostEventSubscriberCount(int subscriberCount) => SetTag(Tags.AppHostEventSubscriberCount, subscriberCount);
 
        public void SetAppHostLifecycleHookCount(int lifecycleHookCount) => SetTag(Tags.AppHostLifecycleHookCount, lifecycleHookCount);
 
        public void SetAppHostPipelineStep(string pipelineStep) => SetTag(Tags.AppHostPipelineStep, pipelineStep);
 
        public void SetAppHostComponentType(Type componentType) => SetTag(Tags.AppHostComponentType, componentType.FullName);
 
        public void SetAppHostEvent(Type eventType, string? dispatchBehavior = null)
        {
            SetTag(Tags.AppHostEventType, eventType.FullName);
            SetTag(Tags.AppHostEventDispatchBehavior, dispatchBehavior);
        }
 
        public void SetDcpKubernetesClientAlreadyInitialized() => SetTag(Tags.DcpKubernetesClientAlreadyInitialized, true);
 
        public void SetBackchannelSocketPath(string socketPath) => SetTag(Tags.BackchannelSocketPath, socketPath);
 
        public void AddBackchannelSocketDeleted() => AddEvent(Events.BackchannelSocketDeleted);
 
        public void AddBackchannelListening() => AddEvent(Events.BackchannelListening);
 
        public void AddBackchannelReadyPublished() => AddEvent(Events.BackchannelReadyPublished);
 
        public void AddBackchannelClientAccepted() => AddEvent(Events.BackchannelClientAccepted);
 
        public void AddBackchannelRpcListening() => AddEvent(Events.BackchannelRpcListening);
 
        public void AddBackchannelConnectedPublished() => AddEvent(Events.BackchannelConnectedPublished);
 
        public void SetJsonRpcCall(string methodName, bool streaming)
        {
            SetTag(Tags.JsonRpcMethod, methodName);
            SetTag(Tags.JsonRpcStreaming, streaming);
        }
 
        public void SetJsonRpcStreamItemCount(int count) => SetTag(Tags.JsonRpcStreamItemCount, count);
 
        public void SetDashboardHealthy(bool healthy) => SetTag(Tags.DashboardHealthy, healthy);
 
        public void SetDashboardUrlSource(string source) => SetTag(Tags.DashboardUrlSource, source);
 
        public void SetDashboardHasApiBaseUrl(bool hasApiBaseUrl) => SetTag(Tags.DashboardHasApiBaseUrl, hasApiBaseUrl);
 
        public void SetDcpPreparedResourceCounts(int containerCount, int executableCount)
        {
            SetTag(Tags.DcpContainerCount, containerCount);
            SetTag(Tags.DcpExecutableCount, executableCount);
        }
 
        public void SetDcpResource(string resourceKind, string resourceName)
        {
            SetTag(Tags.DcpResourceKind, resourceKind);
            SetTag(Tags.DcpResourceName, resourceName);
        }
 
        public void SetDcpResourceSet(string resourceKind, int resourceCount)
        {
            SetTag(Tags.DcpResourceKind, resourceKind);
            SetTag(Tags.DcpResourceCount, resourceCount);
        }
 
        public void SetDcpServiceAllocatedCount(int count) => SetTag(Tags.DcpServiceAllocatedCount, count);
 
        public void SetDcpServiceCount(int count) => SetTag(Tags.DcpServiceCount, count);
 
        public void SetDcpApiRetryCount(int retryCount) => SetTag(Tags.DcpApiRetryCount, retryCount);
 
        public void SetDependencyWait(IResource resource, IResource dependency, WaitType waitType, WaitBehavior? waitBehavior)
        {
            SetResource(resource);
            SetTag(Tags.ResourceWaitType, waitType.ToString());
            SetTag(Tags.ResourceWaitDependencyName, dependency.Name);
            SetTag(Tags.ResourceWaitDependencyType, dependency.GetType().Name);
            SetTag(Tags.ResourceWaitBehavior, waitBehavior?.ToString());
        }
 
        public void SetError(Exception exception)
        {
            if (activity is null)
            {
                return;
            }
 
            activity.SetStatus(ActivityStatusCode.Error, exception.Message);
            activity.AddEvent(new ActivityEvent(Events.Exception, tags: new ActivityTagsCollection
            {
                [Tags.ExceptionType] = exception.GetType().FullName,
                [Tags.ExceptionMessage] = exception.Message
            }));
        }
 
        public void SetResource(IResource resource)
        {
            SetTag(Tags.ResourceName, resource.Name);
            SetTag(Tags.ResourceType, resource.GetType().Name);
        }
 
        public void SetResourceCount(int count) => SetTag(Tags.ResourceCount, count);
 
        public void SetResourceCreate(string resourceKind, int replicaCount)
        {
            SetTag(Tags.ResourceKind, resourceKind);
            SetTag(Tags.ResourceReplicaCount, replicaCount);
        }
 
        public void SetResourceKind(string resourceKind) => SetTag(Tags.ResourceKind, resourceKind);
 
        public void SetResourceLifecycle(IResource resource, string resourceId, CustomResourceSnapshot snapshot)
        {
            SetResource(resource);
            SetTag(Tags.ResourceId, resourceId);
            SetTag(Tags.ResourceSnapshotVersion, snapshot.Version);
            SetTag(Tags.ResourceReady, snapshot.ResourceReadyEvent is not null);
            SetTag(Tags.ResourceState, snapshot.State?.Text);
            SetTag(Tags.ResourceHealthStatus, snapshot.HealthStatus?.ToString());
            SetTag(Tags.ResourceStartTime, snapshot.StartTimeStamp?.ToString("O", CultureInfo.InvariantCulture));
            SetTag(Tags.ResourceStopTime, snapshot.StopTimeStamp?.ToString("O", CultureInfo.InvariantCulture));
 
            if (snapshot.ExitCode is { } exitCode)
            {
                SetTag(Tags.ResourceExitCode, exitCode);
            }
        }
 
        public void AddResourceStartupObserved(CustomResourceSnapshot snapshot, DateTimeOffset timestamp) =>
            AddResourceStartupEvent(Events.ResourceStartupObserved, snapshot, timestamp);
 
        public void AddResourceStartupStateChanged(CustomResourceSnapshot snapshot, DateTimeOffset timestamp, string? previousState) =>
            AddResourceStartupEvent(Events.ResourceStartupStateChanged, snapshot, timestamp, previousState: previousState);
 
        public void AddResourceStartupHealthChanged(CustomResourceSnapshot snapshot, DateTimeOffset timestamp, string? previousHealthStatus) =>
            AddResourceStartupEvent(Events.ResourceStartupHealthChanged, snapshot, timestamp, previousHealthStatus: previousHealthStatus);
 
        public void AddResourceStartupReady(CustomResourceSnapshot snapshot, DateTimeOffset timestamp) =>
            AddResourceStartupEvent(Events.ResourceStartupReady, snapshot, timestamp);
 
        public void SetResourceObserved(string? state, DateTime? startupTimestamp, DateTime? finishedTimestamp)
        {
            SetTag(Tags.ResourceState, state);
            SetTag(Tags.ResourceStartTime, startupTimestamp?.ToString("O", CultureInfo.InvariantCulture));
            SetTag(Tags.ResourceStopTime, finishedTimestamp?.ToString("O", CultureInfo.InvariantCulture));
        }
 
        public void SetResourceStopped(bool stopped) => SetTag(Tags.ResourceStopped, stopped);
 
        public void SetResourceWaitDependencyCount(int count) => SetTag(Tags.ResourceWaitDependencyCount, count);
 
        public void SetResourceWaitExpectedExitCode(int exitCode) => SetTag(Tags.ResourceWaitExpectedExitCode, exitCode);
 
        public void SetResourceWaitTarget(string resourceName, string waitCondition)
        {
            SetTag(Tags.ResourceWaitTargetName, resourceName);
            SetTag(Tags.ResourceWaitCondition, waitCondition);
        }
 
        public void Dispose()
        {
            if (ownsActivity)
            {
                activity?.Dispose();
            }
        }
 
        private void AddEvent(string name) => activity?.AddEvent(new ActivityEvent(name));
 
        private void AddResourceWaitEvent(string eventName, ResourceEvent resourceEvent, string waitCondition)
        {
            if (activity is null)
            {
                return;
            }
 
            var snapshot = resourceEvent.Snapshot;
            var tags = new ActivityTagsCollection
            {
                [Tags.ResourceName] = resourceEvent.Resource.Name,
                [Tags.ResourceId] = resourceEvent.ResourceId,
                [Tags.ResourceType] = snapshot.ResourceType,
                [Tags.ResourceWaitCondition] = waitCondition,
                [Tags.ResourceSnapshotVersion] = snapshot.Version,
                [Tags.ResourceReady] = snapshot.ResourceReadyEvent is not null
            };
 
            if (snapshot.State?.Text is { } state)
            {
                tags[Tags.ResourceState] = state;
            }
 
            if (snapshot.HealthStatus is { } healthStatus)
            {
                tags[Tags.ResourceHealthStatus] = healthStatus.ToString();
            }
 
            if (snapshot.ExitCode is { } exitCode)
            {
                tags[Tags.ResourceExitCode] = exitCode;
            }
 
            activity.AddEvent(new ActivityEvent(eventName, tags: tags));
        }
 
        private void AddResourceStartupEvent(
            string eventName,
            CustomResourceSnapshot snapshot,
            DateTimeOffset timestamp,
            string? previousState = null,
            string? previousHealthStatus = null)
        {
            if (activity is null)
            {
                return;
            }
 
            var tags = new ActivityTagsCollection
            {
                [Tags.ResourceSnapshotVersion] = snapshot.Version,
                [Tags.ResourceReady] = snapshot.ResourceReadyEvent is not null
            };
 
            if (previousState is not null)
            {
                tags[Tags.PreviousResourceState] = previousState;
            }
 
            if (previousHealthStatus is not null)
            {
                tags[Tags.PreviousResourceHealthStatus] = previousHealthStatus;
            }
 
            if (snapshot.State?.Text is { } state)
            {
                tags[Tags.ResourceState] = state;
            }
 
            if (snapshot.HealthStatus is { } healthStatus)
            {
                tags[Tags.ResourceHealthStatus] = healthStatus.ToString();
            }
 
            if (snapshot.ExitCode is { } exitCode)
            {
                tags[Tags.ResourceExitCode] = exitCode;
            }
 
            activity.AddEvent(new ActivityEvent(eventName, timestamp, tags));
        }
 
        private void SetTag(string key, object? value) => activity?.SetTag(key, value);
    }
}