Skip to content

Commit 05ef476

Browse files
davidfowlCopilot
andauthored
Add logging capabilities to the publishing pipeline (#12279)
* Add logging capabilities to the publishing pipeline - Introduced `--log-level` and `--environment` options for the publish command to configure logging and environment settings. - Implemented `PipelineLoggerProvider` to manage logging context across pipeline steps. - Enhanced `DistributedApplicationPipeline` to utilize the new logging provider. - Updated tests to verify logging behavior and configuration. * Refactor pipeline step logging to remove unnecessary async/await and improve clarity * Refactor logger tests to use FakeLogger and remove redundant tests * Update documentation in PipelineLoggerProvider to clarify logger context usage * Remove redundant log level and environment variable handling from PublishCommandBase execution * Update src/Aspire.Cli/Commands/PublishCommandBase.cs * Update src/Aspire.Cli/Commands/PublishCommandBase.cs Co-authored-by: Copilot <[email protected]> * Add markdown formatting support to publishing activities and logging - Introduced EnableMarkdown property in PublishingActivityData to control markdown formatting. - Updated logging methods in IReportingStep and related classes to accept an enableMarkdown parameter. - Refactored existing logging calls to utilize the new markdown formatting feature. - Adjusted tests to verify markdown behavior in logging. * Change log level from Information and Error to Debug for process output in ContainerRuntimeBase and ResourceContainerImageBuilder * Add markdown support to logging in AzureDeployerTests * Fix markdown handling in PublishCommandBase and improve error logging in PublishModeProvisioningContextProvider * Change log level from Information to Debug for docker buildx output in RunDockerBuildAsync * Add log level and environment options to PublishCommandBase and DeployCommand --------- Co-authored-by: Copilot <[email protected]>
1 parent 29130bf commit 05ef476

21 files changed

+747
-67
lines changed

src/Aspire.Cli/Commands/DeployCommand.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, st
4747
baseArgs.AddRange(["--clear-cache", "true"]);
4848
}
4949

50+
// Add --log-level and --envionment flags if specified
51+
var logLevel = parseResult.GetValue(_logLevelOption);
52+
53+
if (!string.IsNullOrEmpty(logLevel))
54+
{
55+
baseArgs.AddRange(["--log-level", logLevel!]);
56+
}
57+
58+
var environment = parseResult.GetValue(_environmentOption);
59+
if (!string.IsNullOrEmpty(environment))
60+
{
61+
baseArgs.AddRange(["--environment", environment!]);
62+
}
63+
5064
baseArgs.AddRange(unmatchedTokens);
5165

5266
return [.. baseArgs];

src/Aspire.Cli/Commands/PublishCommand.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, st
5555

5656
baseArgs.AddRange(["--output-path", targetPath]);
5757

58+
// Add --log-level and --envionment flags if specified
59+
var logLevel = parseResult.GetValue(_logLevelOption);
60+
61+
if (!string.IsNullOrEmpty(logLevel))
62+
{
63+
baseArgs.AddRange(["--log-level", logLevel!]);
64+
}
65+
66+
var environment = parseResult.GetValue(_environmentOption);
67+
if (!string.IsNullOrEmpty(environment))
68+
{
69+
baseArgs.AddRange(["--environment", environment!]);
70+
}
71+
5872
baseArgs.AddRange(unmatchedTokens);
5973

6074
return [.. baseArgs];

src/Aspire.Cli/Commands/PublishCommandBase.cs

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ internal abstract class PublishCommandBase : BaseCommand
2929
private readonly IFeatures _features;
3030
private readonly ICliHostEnvironment _hostEnvironment;
3131

32+
protected readonly Option<string?> _logLevelOption = new("--log-level")
33+
{
34+
Description = "Set the minimum log level for pipeline logging (trace, debug, information, warning, error, critical). The default is 'information'."
35+
};
36+
37+
protected readonly Option<string?> _environmentOption = new("--environment", "-e")
38+
{
39+
Description = "The environment to use for the operation. The default is 'Production'."
40+
};
41+
3242
protected abstract string OperationCompletedPrefix { get; }
3343
protected abstract string OperationFailedPrefix { get; }
3444

@@ -70,6 +80,9 @@ protected PublishCommandBase(string name, string description, IDotNetCliRunner r
7080
};
7181
Options.Add(outputPath);
7282

83+
Options.Add(_logLevelOption);
84+
Options.Add(_environmentOption);
85+
7386
// In the publish and deploy commands we forward all unrecognized tokens
7487
// through to the underlying tooling when we launch the app host.
7588
TreatUnmatchedTokensAsErrors = false;
@@ -278,6 +291,17 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
278291
}
279292
}
280293

294+
/// <summary>
295+
/// Conditionally converts markdown to Spectre markup based on the EnableMarkdown flag in the activity data.
296+
/// </summary>
297+
/// <param name="text">The text to convert.</param>
298+
/// <param name="activityData">The publishing activity data containing the EnableMarkdown flag.</param>
299+
/// <returns>The converted text if markdown is enabled, otherwise the original text.</returns>
300+
private static string ConvertTextWithMarkdownFlag(string text, PublishingActivityData activityData)
301+
{
302+
return activityData.EnableMarkdown ? MarkdownToSpectreConverter.ConvertToSpectre(text) : text.EscapeMarkup();
303+
}
304+
281305
public async Task<bool> ProcessPublishingActivitiesDebugAsync(IAsyncEnumerable<PublishingActivity> publishingActivities, IAppHostBackchannel backchannel, CancellationToken cancellationToken)
282306
{
283307
var stepCounter = 1;
@@ -297,7 +321,7 @@ public async Task<bool> ProcessPublishingActivitiesDebugAsync(IAsyncEnumerable<P
297321
if (!steps.TryGetValue(activity.Data.Id, out var stepStatus))
298322
{
299323
// New step - log it
300-
var statusText = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
324+
var statusText = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
301325
InteractionService.DisplaySubtleMessage($"[[DEBUG]] Step {stepCounter++}: {statusText}", escapeMarkup: false);
302326
steps[activity.Data.Id] = activity.Data.CompletionState;
303327
}
@@ -306,7 +330,7 @@ public async Task<bool> ProcessPublishingActivitiesDebugAsync(IAsyncEnumerable<P
306330
// Step completed - log completion
307331
var status = IsCompletionStateError(activity.Data.CompletionState) ? "FAILED" :
308332
IsCompletionStateWarning(activity.Data.CompletionState) ? "WARNING" : "COMPLETED";
309-
var statusText = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
333+
var statusText = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
310334
InteractionService.DisplaySubtleMessage($"[[DEBUG]] Step {activity.Data.Id}: {status} - {statusText}", escapeMarkup: false);
311335
steps[activity.Data.Id] = activity.Data.CompletionState;
312336
}
@@ -319,7 +343,7 @@ public async Task<bool> ProcessPublishingActivitiesDebugAsync(IAsyncEnumerable<P
319343
{
320344
// Log activity - display the log message
321345
var logLevel = activity.Data.LogLevel ?? "Information";
322-
var message = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
346+
var message = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
323347
var timestamp = activity.Data.Timestamp?.ToString("HH:mm:ss", CultureInfo.InvariantCulture) ?? DateTimeOffset.UtcNow.ToString("HH:mm:ss", CultureInfo.InvariantCulture);
324348

325349
// Use 3-letter prefixes for log levels
@@ -337,9 +361,9 @@ public async Task<bool> ProcessPublishingActivitiesDebugAsync(IAsyncEnumerable<P
337361
// Make debug and trace logs more subtle
338362
var formattedMessage = logLevel.ToUpperInvariant() switch
339363
{
340-
"DEBUG" => $"[{timestamp}] [dim][[{logPrefix}]] {message}[/]",
341-
"TRACE" => $"[{timestamp}] [dim][[{logPrefix}]] {message}[/]",
342-
_ => $"[{timestamp}] [[{logPrefix}]] {message}"
364+
"DEBUG" => $"[[{timestamp}]] [dim][[{logPrefix}]] {message}[/]",
365+
"TRACE" => $"[[{timestamp}]] [dim][[{logPrefix}]] {message}[/]",
366+
_ => $"[[{timestamp}]] [[{logPrefix}]] {message}"
343367
};
344368

345369
InteractionService.DisplaySubtleMessage(formattedMessage, escapeMarkup: false);
@@ -352,17 +376,17 @@ public async Task<bool> ProcessPublishingActivitiesDebugAsync(IAsyncEnumerable<P
352376
{
353377
var status = IsCompletionStateError(activity.Data.CompletionState) ? "FAILED" :
354378
IsCompletionStateWarning(activity.Data.CompletionState) ? "WARNING" : "COMPLETED";
355-
var statusText = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
379+
var statusText = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
356380
InteractionService.DisplaySubtleMessage($"[[DEBUG]] Task {activity.Data.Id} ({stepId}): {status} - {statusText}", escapeMarkup: false);
357381
if (!string.IsNullOrEmpty(activity.Data.CompletionMessage))
358382
{
359-
var completionMessage = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.CompletionMessage);
383+
var completionMessage = ConvertTextWithMarkdownFlag(activity.Data.CompletionMessage, activity.Data);
360384
InteractionService.DisplaySubtleMessage($"[[DEBUG]] {completionMessage}", escapeMarkup: false);
361385
}
362386
}
363387
else
364388
{
365-
var statusText = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
389+
var statusText = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
366390
InteractionService.DisplaySubtleMessage($"[[DEBUG]] Task {activity.Data.Id} ({stepId}): {statusText}", escapeMarkup: false);
367391
}
368392
}
@@ -374,7 +398,7 @@ public async Task<bool> ProcessPublishingActivitiesDebugAsync(IAsyncEnumerable<P
374398
if (publishingActivity is not null)
375399
{
376400
var status = hasErrors ? "FAILED" : hasWarnings ? "WARNING" : "COMPLETED";
377-
var statusText = MarkdownToSpectreConverter.ConvertToSpectre(publishingActivity.Data.StatusText);
401+
var statusText = ConvertTextWithMarkdownFlag(publishingActivity.Data.StatusText, publishingActivity.Data);
378402
InteractionService.DisplaySubtleMessage($"[[DEBUG]] {OperationCompletedPrefix}: {status} - {statusText}", escapeMarkup: false);
379403

380404
// Send visual bell notification when operation is complete
@@ -407,7 +431,7 @@ public async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsyncEnumera
407431
{
408432
if (!steps.TryGetValue(activity.Data.Id, out var stepInfo))
409433
{
410-
var title = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
434+
var title = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
411435
stepInfo = new StepInfo
412436
{
413437
Id = activity.Data.Id,
@@ -424,7 +448,7 @@ public async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsyncEnumera
424448
else if (IsCompletionStateComplete(activity.Data.CompletionState))
425449
{
426450
stepInfo.CompletionState = activity.Data.CompletionState;
427-
stepInfo.CompletionText = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
451+
stepInfo.CompletionText = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
428452
stepInfo.EndTime = DateTime.UtcNow;
429453
if (IsCompletionStateError(stepInfo.CompletionState))
430454
{
@@ -453,7 +477,7 @@ public async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsyncEnumera
453477
if (stepId != null && steps.TryGetValue(stepId, out var stepInfo))
454478
{
455479
var logLevel = activity.Data.LogLevel ?? "Information";
456-
var message = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
480+
var message = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
457481

458482
// Add 3-letter prefix to message for consistency
459483
var logPrefix = logLevel.ToUpperInvariant() switch
@@ -508,7 +532,7 @@ public async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsyncEnumera
508532

509533
if (!tasks.TryGetValue(activity.Data.Id, out var task))
510534
{
511-
var statusText = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
535+
var statusText = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
512536
task = new TaskInfo
513537
{
514538
Id = activity.Data.Id,
@@ -521,13 +545,13 @@ public async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsyncEnumera
521545
logger.Progress(stepInfo.Id, statusText);
522546
}
523547

524-
task.StatusText = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
548+
task.StatusText = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
525549
task.CompletionState = activity.Data.CompletionState;
526550

527551
if (IsCompletionStateComplete(activity.Data.CompletionState))
528552
{
529553
task.CompletionMessage = !string.IsNullOrEmpty(activity.Data.CompletionMessage)
530-
? MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.CompletionMessage)
554+
? ConvertTextWithMarkdownFlag(activity.Data.CompletionMessage, activity.Data)
531555
: null;
532556

533557
var duration = DateTime.UtcNow - task.StartTime;
@@ -617,12 +641,12 @@ var cs when IsCompletionStateWarning(cs) => ConsoleActivityLogger.ActivityState.
617641
}
618642
}
619643

620-
private static string BuildPromptText(PublishingPromptInput input, int inputCount, string statusText)
644+
private static string BuildPromptText(PublishingPromptInput input, int inputCount, string statusText, PublishingActivityData activityData)
621645
{
622646
if (inputCount > 1)
623647
{
624648
// Multi-input: just show the label with markdown conversion
625-
var labelText = MarkdownToSpectreConverter.ConvertToSpectre($"{input.Label}: ");
649+
var labelText = ConvertTextWithMarkdownFlag($"{input.Label}: ", activityData);
626650
return labelText;
627651
}
628652

@@ -633,12 +657,12 @@ private static string BuildPromptText(PublishingPromptInput input, int inputCoun
633657
// If StatusText equals Label (case-insensitive), show only the label once
634658
if (header.Equals(label, StringComparison.OrdinalIgnoreCase))
635659
{
636-
return $"[bold]{MarkdownToSpectreConverter.ConvertToSpectre(label)}[/]";
660+
return $"[bold]{ConvertTextWithMarkdownFlag(label, activityData)}[/]";
637661
}
638662

639663
// Show StatusText as header (converted from markdown), then Label on new line
640-
var convertedHeader = MarkdownToSpectreConverter.ConvertToSpectre(header);
641-
var convertedLabel = MarkdownToSpectreConverter.ConvertToSpectre(label);
664+
var convertedHeader = ConvertTextWithMarkdownFlag(header, activityData);
665+
var convertedLabel = ConvertTextWithMarkdownFlag(label, activityData);
642666
return $"[bold]{convertedHeader}[/]\n{convertedLabel}: ";
643667
}
644668

@@ -663,7 +687,7 @@ private async Task HandlePromptActivityAsync(PublishingActivity activity, IAppHo
663687
// Don't display if there are validation errors. Validation errors means the header has already been displayed.
664688
if (!hasValidationErrors && inputs.Count > 1)
665689
{
666-
var headerText = MarkdownToSpectreConverter.ConvertToSpectre(activity.Data.StatusText);
690+
var headerText = ConvertTextWithMarkdownFlag(activity.Data.StatusText, activity.Data);
667691
AnsiConsole.MarkupLine($"[bold]{headerText}[/]");
668692
}
669693

@@ -680,7 +704,7 @@ private async Task HandlePromptActivityAsync(PublishingActivity activity, IAppHo
680704
if (!hasValidationErrors || input.ValidationErrors is { Count: > 0 })
681705
{
682706
// Build the prompt text based on number of inputs
683-
var promptText = BuildPromptText(input, inputs.Count, activity.Data.StatusText);
707+
var promptText = BuildPromptText(input, inputs.Count, activity.Data.StatusText, activity.Data);
684708

685709
result = await HandleSingleInputAsync(input, promptText, cancellationToken);
686710
}

src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ public override async Task<ProvisioningContext> CreateProvisioningContextAsync(J
6565
await RetrieveAzureProvisioningOptions(cancellationToken).ConfigureAwait(false);
6666
_logger.LogDebug("Azure provisioning options have been handled successfully.");
6767
}
68+
catch (OperationCanceledException)
69+
{
70+
throw;
71+
}
6872
catch (Exception ex)
6973
{
7074
_logger.LogError(ex, "Failed to retrieve Azure provisioning options.");

src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ internal sealed class PublishingActivityData
135135
/// Gets the timestamp for log activities, if available.
136136
/// </summary>
137137
public DateTimeOffset? Timestamp { get; init; }
138+
139+
/// <summary>
140+
/// Gets a value indicating whether markdown formatting is enabled for the publishing activity.
141+
/// </summary>
142+
public bool EnableMarkdown { get; init; } = true;
138143
}
139144

140145
/// <summary>

src/Aspire.Hosting/DistributedApplicationBuilder.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,25 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
446446
_innerBuilder.Services.AddSingleton<PipelineActivityReporter>();
447447
_innerBuilder.Services.AddSingleton<IPipelineActivityReporter, PipelineActivityReporter>(sp => sp.GetRequiredService<PipelineActivityReporter>());
448448
_innerBuilder.Services.AddSingleton(Pipeline);
449+
_innerBuilder.Services.AddSingleton<ILoggerProvider, PipelineLoggerProvider>();
450+
451+
// Read this once from configuration and use it to filter logs from the pipeline
452+
LogLevel? minLevel = null;
453+
_innerBuilder.Logging.AddFilter<PipelineLoggerProvider>((level) =>
454+
{
455+
minLevel ??= _innerBuilder.Configuration["Publishing:LogLevel"]?.ToLowerInvariant() switch
456+
{
457+
"trace" => LogLevel.Trace,
458+
"debug" => LogLevel.Debug,
459+
"info" or "information" => LogLevel.Information,
460+
"warn" or "warning" => LogLevel.Warning,
461+
"error" => LogLevel.Error,
462+
"crit" or "critical" => LogLevel.Critical,
463+
_ => LogLevel.Information
464+
};
465+
466+
return level >= minLevel;
467+
});
449468

450469
// Register IDeploymentStateManager based on execution context
451470
if (ExecutionContext.IsPublishMode)
@@ -540,6 +559,7 @@ private void ConfigurePublishingOptions(DistributedApplicationOptions options)
540559
{ "--publisher", "Publishing:Publisher" },
541560
{ "--output-path", "Publishing:OutputPath" },
542561
{ "--deploy", "Publishing:Deploy" },
562+
{ "--log-level", "Publishing:LogLevel" },
543563
{ "--clear-cache", "Publishing:ClearCache" },
544564
{ "--dcp-cli-path", "DcpPublisher:CliPath" },
545565
{ "--dcp-container-runtime", "DcpPublisher:ContainerRuntime" },

src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Runtime.ExceptionServices;
1010
using System.Text;
1111
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Logging.Abstractions;
1213

1314
namespace Aspire.Hosting.Pipelines;
1415

@@ -254,6 +255,9 @@ async Task ExecuteStepWithDependencies(PipelineStep step)
254255
PipelineContext = context,
255256
ReportingStep = publishingStep
256257
};
258+
259+
PipelineLoggerProvider.CurrentLogger = stepContext.Logger;
260+
257261
await ExecuteStepAsync(step, stepContext).ConfigureAwait(false);
258262
}
259263
catch (Exception ex)
@@ -262,6 +266,10 @@ async Task ExecuteStepWithDependencies(PipelineStep step)
262266
await publishingStep.FailAsync(ex.Message, CancellationToken.None).ConfigureAwait(false);
263267
throw;
264268
}
269+
finally
270+
{
271+
PipelineLoggerProvider.CurrentLogger = NullLogger.Instance;
272+
}
265273
}
266274

267275
stepTcs.TrySetResult();

src/Aspire.Hosting/Pipelines/IReportingStep.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public interface IReportingStep : IAsyncDisposable
2525
/// </summary>
2626
/// <param name="logLevel">The log level for the message.</param>
2727
/// <param name="message">The message to log.</param>
28-
void Log(LogLevel logLevel, string message);
28+
/// <param name="enableMarkdown">Whether to enable Markdown formatting for the message.</param>
29+
void Log(LogLevel logLevel, string message, bool enableMarkdown);
2930

3031
/// <summary>
3132
/// Completes the step with the specified completion text and state.

src/Aspire.Hosting/Pipelines/NullPipelineActivityReporter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public Task<IReportingTask> CreateTaskAsync(string statusText, CancellationToken
3535
return Task.FromResult<IReportingTask>(new NullPublishingTask());
3636
}
3737

38-
public void Log(LogLevel logLevel, string message)
38+
public void Log(LogLevel logLevel, string message, bool enableMarkdown)
3939
{
4040
// No-op for null implementation
4141
}

0 commit comments

Comments
 (0)