File: TerminalHostTelemetry.cs
Web Access
Project: src\src\Aspire.TerminalHost\Aspire.TerminalHost.csproj (Aspire.TerminalHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.Metrics;
 
namespace Aspire.TerminalHost;
 
/// <summary>
/// Shared <see cref="System.Diagnostics.ActivitySource"/> and <see cref="System.Diagnostics.Metrics.Meter"/>
/// for the Aspire terminal host. Telemetry is exported via OTLP to the Aspire dashboard so failures
/// like "DCP never dialed in" or "control socket bound but no clients" are diagnosable without
/// resorting to attaching a debugger.
/// </summary>
/// <remarks>
/// <para>
/// The OTLP exporter wiring in <see cref="TerminalHostApp.RunAsync(string[], System.Threading.CancellationToken)"/>
/// only attaches when <c>OTEL_EXPORTER_OTLP_ENDPOINT</c> is set in the environment. The Aspire
/// AppHost injects that via <c>OtlpConfigurationExtensions.AddOtlpEnvironment</c> on each
/// <c>TerminalHostResource</c>, so production runs always have it. Standalone debug runs of the
/// host (<c>dotnet run --project src/Aspire.TerminalHost</c>) drop telemetry silently.
/// </para>
/// <para>
/// Source / meter names follow the assembly name convention
/// (<see href="https://learn.microsoft.com/dotnet/core/diagnostics/observability-with-otel#naming-conventions"/>)
/// so dashboard categorisation matches every other Aspire component.
/// </para>
/// </remarks>
internal static class TerminalHostTelemetry
{
    public const string SourceName = "Aspire.TerminalHost";
 
    /// <summary>
    /// Activity source for terminal host lifecycle spans (process boot, replica build, DCP-dial wait,
    /// consumer client accept, control-listener accept, shutdown).
    /// </summary>
    public static readonly ActivitySource ActivitySource = new(SourceName);
 
    /// <summary>
    /// Meter for terminal host counters and gauges. Disposed when the host shuts down so the OTLP
    /// metric exporter can flush a final reading.
    /// </summary>
    public static readonly Meter Meter = new(SourceName);
 
    /// <summary>
    /// Incremented when the upstream producer connection drops and the replica recycles its
    /// <c>DcpUpstreamAdapter</c>. Diagnoses DCP restart / crash loops — a steadily growing value
    /// on a single host process means DCP keeps reconnecting, which usually means DCP itself
    /// is being restarted by its supervisor.
    /// </summary>
    public static readonly Counter<long> UpstreamRecycles = Meter.CreateCounter<long>(
        name: "aspire.terminalhost.upstream.recycles",
        unit: "{recycle}",
        description: "Number of times the upstream (DCP) connection dropped and was re-listened for.");
 
    /// <summary>
    /// Incremented every time a downstream viewer (dashboard tab, CLI <c>aspire terminal attach</c>)
    /// successfully accepts on the consumer UDS. Diagnoses "I attached but see nothing" by letting
    /// you confirm the accept actually happened on the host side.
    /// </summary>
    public static readonly Counter<long> ConsumerConnections = Meter.CreateCounter<long>(
        name: "aspire.terminalhost.consumer.connections",
        unit: "{connection}",
        description: "Number of downstream consumer (dashboard/CLI viewer) connections accepted.");
 
    /// <summary>
    /// Incremented on viewer disconnect. Pairs with <see cref="ConsumerConnections"/> so a
    /// monotonically-growing delta between the two on the dashboard exposes leaked / orphaned
    /// peer sessions.
    /// </summary>
    public static readonly Counter<long> ConsumerDisconnections = Meter.CreateCounter<long>(
        name: "aspire.terminalhost.consumer.disconnections",
        unit: "{connection}",
        description: "Number of downstream consumer (dashboard/CLI viewer) disconnections observed.");
 
    /// <summary>
    /// Current attached-peer count. Up/down counter so the dashboard can chart "is the terminal
    /// idle right now?" without subtracting two monotonic counters and dealing with restarts.
    /// </summary>
    public static readonly UpDownCounter<long> ConsumerPeersActive = Meter.CreateUpDownCounter<long>(
        name: "aspire.terminalhost.consumer.peers.active",
        unit: "{peer}",
        description: "Current number of downstream consumer (dashboard/CLI viewer) peers attached.");
 
    /// <summary>
    /// Resize events. Tagged with <c>direction</c> = <c>downstream</c> (consumer-side primary
    /// peer changed dims) or <c>upstream</c> (host wrote a <c>FrameResize</c> to DCP), and for
    /// <c>upstream</c> also a <c>result</c> = <c>ok</c> | <c>failed</c> tag.
    /// </summary>
    public static readonly Counter<long> ResizeRequests = Meter.CreateCounter<long>(
        name: "aspire.terminalhost.resize.requests",
        unit: "{resize}",
        description: "Number of resize events observed/forwarded.");
 
    /// <summary>
    /// Bytes transferred. Tagged with <c>socket</c> = <c>producer</c> | <c>consumer</c> |
    /// <c>control</c> and <c>direction</c> = <c>in</c> (host received) | <c>out</c> (host sent).
    /// Counts wire bytes — HMP1 frame headers are included because they're real socket bytes
    /// (5B per frame), so a "bytes out" growing faster than "payload bytes" is itself a signal.
    /// </summary>
    public static readonly Counter<long> Bytes = Meter.CreateCounter<long>(
        name: "aspire.terminalhost.bytes",
        unit: "By",
        description: "Bytes transferred over the per-replica UDS sockets.");
}