File: Pipelines\PipelineLoggerProvider.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
// 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 Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Pipelines;
 
/// <summary>
/// Provides loggers that forward calls to a contextual logger associated with the current pipeline step.
/// </summary>
/// <remarks>
/// This logger provider uses AsyncLocal storage to maintain the current logger context across async operations.
/// This enables pipeline steps to log through a contextual logger that can be set per execution.
/// </remarks>
internal sealed class PipelineLoggerProvider(IOptions<PipelineLoggingOptions> options) : ILoggerProvider
{
    private static readonly AsyncLocal<StepLoggerHolder?> s_currentStep = new();
 
    /// <summary>
    /// Gets or sets the current logger for the executing pipeline step.
    /// </summary>
    public static IReportingStep? CurrentStep
    {
        get => s_currentStep.Value?.Step;
        set
        {
            // Clear the current logger from AsyncLocal context
            s_currentStep.Value = null;
 
            if (value is not null)
            {
                // Use an object indirection to hold the logger in the AsyncLocal,
                // so it can be cleared in all ExecutionContexts when cleared.
                s_currentStep.Value = new StepLoggerHolder { Step = value };
            }
        }
    }
 
    /// <inheritdoc/>
    public ILogger CreateLogger(string categoryName) =>
        new StepLogger(() => CurrentStep, options.Value);
 
    /// <inheritdoc/>
    public void Dispose()
    {
        // No resources to dispose
    }
 
    /// <summary>
    /// Holds the logger instance in AsyncLocal storage.
    /// </summary>
    private sealed class StepLoggerHolder
    {
        public IReportingStep? Step;
    }
 
    /// <summary>
    /// A logger implementation that forwards all calls to the current contextual logger.
    /// </summary>
    /// <remarks>
    /// This logger acts as a proxy and dynamically resolves the current logger on each operation,
    /// allowing the target logger to change between calls.
    /// When logging exceptions, stack traces are only included when the configured minimum log level is Debug or Trace.
    /// </remarks>
    private sealed class StepLogger(Func<IReportingStep?> currentStepAccessor, PipelineLoggingOptions options) : ILogger
    {
        /// <inheritdoc/>
        public IDisposable? BeginScope<TState>(TState state) where TState : notnull =>
            null;
 
        /// <inheritdoc/>
        public bool IsEnabled(LogLevel logLevel) =>
            true;
 
        /// <inheritdoc/>
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        {
            var step = currentStepAccessor();
 
            if (step is null)
            {
                // No current step logger; nothing to log to
                return;
            }
 
            // Also log to the step logger (for publishing output display)
            var message = formatter(state, exception);
 
            if (options.IncludeExceptionDetails && exception != null)
            {
                message = $"{message} {exception}";
            }
 
            step.Log(logLevel, message, enableMarkdown: false);
        }
    }
}