File: src\Shared\LoggingHelpers.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 Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Text;
 
namespace Aspire.Hosting;
 
internal static class LoggingHelpers
{
    public static void WriteDashboardSummary(ILogger logger, string? dashboardUrl, string? otlpGrpcUrl, string? otlpHttpUrl, string? token, bool isContainer = false)
    {
        // Callers should pass a single resolved URL, not a semicolon-delimited list.
        AssertSingleUrl(dashboardUrl, nameof(dashboardUrl));
        AssertSingleUrl(otlpGrpcUrl, nameof(otlpGrpcUrl));
        AssertSingleUrl(otlpHttpUrl, nameof(otlpHttpUrl));
 
        static string? GetAuthority(string? url)
        {
            return Uri.TryCreate(url, UriKind.Absolute, out var uri)
                ? uri.GetLeftPart(UriPartial.Authority)
                : null;
        }
 
        var dashboardAuthority = GetAuthority(dashboardUrl);
        var otlpGrpcAuthority = GetAuthority(otlpGrpcUrl);
        var otlpHttpAuthority = GetAuthority(otlpHttpUrl);
 
        // Nothing to log if we have no URLs at all.
        if (dashboardAuthority is null && otlpGrpcAuthority is null && otlpHttpAuthority is null)
        {
            return;
        }
 
        var loginUrl = !string.IsNullOrEmpty(token) && dashboardAuthority is not null
            ? $"{dashboardAuthority}/login?t={token}"
            : null;
 
        if (loginUrl is not null)
        {
            // dotnet watch looks for this exact log message to launch the dashboard. Do not change it.
            logger.LogInformation("Login to the dashboard at {LoginUrl}", loginUrl);
        }
 
        var templateBuilder = new StringBuilder();
        var parameters = new List<object?>();
 
        templateBuilder
            .Append("Aspire Dashboard").Append('\n')
            .Append('\n');
 
        // The default .NET console logger indents the first line 6 characters because of the level.
        // This prefix aligns the URLs under the dashboard title and makes them easier to read.
        // In other logging layouts the prefix is just extra spaces, but it doesn't cause any real issues.
        var prefix = "      ";
 
        if (dashboardAuthority is not null)
        {
            templateBuilder.Append(prefix).Append("- Dashboard:  {DashboardUrl}").Append('\n');
            parameters.Add(dashboardAuthority);
        }
 
        if (loginUrl is not null)
        {
            templateBuilder.Append(prefix).Append("- Login URL:  {LoginUrl}").Append('\n');
            parameters.Add(loginUrl);
        }
 
        if (otlpGrpcAuthority is not null)
        {
            templateBuilder.Append(prefix).Append("- OTLP/gRPC:  {OtlpGrpcUrl}").Append('\n');
            parameters.Add(otlpGrpcAuthority);
        }
 
        if (otlpHttpAuthority is not null)
        {
            templateBuilder.Append(prefix).Append("- OTLP/HTTP:  {OtlpHttpUrl}").Append('\n');
            parameters.Add(otlpHttpAuthority);
        }
 
        logger.LogInformation(templateBuilder.ToString(), parameters.ToArray());
 
        if (isContainer)
        {
            logger.LogInformation("Dashboard is running in a container. Access the dashboard from the host using port forwarding.");
        }
    }
 
    [Conditional("DEBUG")]
    private static void AssertSingleUrl(string? url, string paramName)
    {
        Debug.Assert(url is null || !url.Contains(';'), $"{paramName} should not contain ';': {url}");
    }
}