Skip to main content

Logging

ILogger​

When logging in a module, use the ILogger exposed by context.Logger. Do not inject a category logger such as ILogger<T> into a module.

The context logger applies Modular Pipelines' secret masking and groups output with the current module while still using the standard Microsoft.Extensions.Logging API.

These are detailed below.

Secret Obfuscation​

If secrets have been defined (See Secrets for details on this), then if any of them are attempted to be written through the context logger, either directly or through HTTP and command logs, they will be obfuscated in the output. For example, a bearer token of 'MySuperSecretToken' will appear as '**********'.

Grouped Logs​

When writing through context.Logger, logs are grouped by the current module. Since modules may run in parallel, grouping keeps each module's structured, rich, and plain console output together and readable.

On build systems that support collapsible sections (GitHub Actions, Azure Pipelines, GitLab, TeamCity) and in the local console, each group is headed with the module name, its outcome and its duration, for example BuildModule ✓ (12s 40ms). When the results table is printed, the separate "completed successfully" and "skipped" status lines are then not repeated on the console; other logging providers, such as file or telemetry sinks, still receive them at Information. Build systems without group headers, and runs with Console.PrintResults set to false, still show them on the console.

File system operations such as reading, writing, copying and searching files and folders are logged at Debug level.

Summary Entries​

Use context.Summary to record facts worth reading after the run, such as a generated version or a deployment URL:

context.Summary.KeyValue("Version", "Generated Version Number", version);
context.Summary.Success("Deploy", "Deployed to production");

Entries appear in a Summary section of the final results output, grouped by category, with registered secrets masked. They are also written to every other configured logging provider (for example file or telemetry sinks) at the end of the run. If results are not printed (for example Console.PrintResults is false), the entries are written to the console logger instead.

Interfering with Console Progress​

If you have an interactive terminal, then a progress dialog will be displayed, and constantly updated with the progress of all your modules. Module output is buffered while this display is active so it does not overwrite the progress dialog.

Analyzers​

Modular Pipelines analyzers recommend context.Console over direct Console calls and prevent injecting category loggers into modules. Direct stdout/stderr remains a supported capture path when console stream semantics are specifically required.

How to access ILogger​

Module​

If you're in a module, it's part of your context object. Call context.Logger.

Elsewhere​

If you're in another class, inject IModuleLoggerAccessor and use its Logger property.

Choosing an output API​

Modular Pipelines keeps all three module output forms in one ordered module group:

  • Use System.Console.Write* and System.Console.Error.Write* for captured plain stdout and stderr with normal console fragment and newline semantics.
  • Use context.Console for captured plain text or Spectre.Console rendering.
  • Use context.Logger for structured, levelled events delivered to the configured Microsoft.Extensions.Logging providers and filters.

Raw console writes are not converted into Information or Error log events. They are not sent to non-console logging providers and logging filters do not suppress them. Secrets are masked before console rendering and optional run-report capture, including secrets split across several Write calls.

context.Console.WriteLine("Literal [brackets] stay literal");
context.Console.WriteMarkupLine("[green]Build succeeded[/]");
context.Console.Write(new Table().AddColumn("Result").AddRow("Succeeded"));

Attribution follows the .NET ExecutionContext, so it survives normal await continuations and Task.Run. It also applies during module construction, nested submodules, and distributed execution. Concurrent modules retain separate partial-line state for stdout and stderr, so fragments and lines cannot migrate between module groups.

Writes made with ExecutionContext.SuppressFlow() cannot be attributed and use the pipeline/unattributed output group. The same is true for fire-and-forget work that writes after its owning module's output scope has ended. If that work continues after the pipeline restores the process console, ordinary process-console behavior applies. Await module-owned work whenever its output must belong to that module.

Capture and grouping remain active when Console.ShowProgress is false. Rendering of the resulting groups still follows the selected local or CI build-system formatter. Avoid using the global AnsiConsole directly; use context.Console for rich output so the correct module buffer, ordering, and masking rules apply.

Command Logging​

When you execute CLI commands (e.g., dotnet build, docker run), ModularPipelines logs the command execution details. You can control what gets logged using CommandLoggingOptions.

Command output is logged one line at a time, each prefixed with ↳, whether it is streamed while the command runs or captured when it finishes. Blank output lines are omitted. Streamed standard error lines are logged the same way, because many tools write progress to standard error. When output is captured instead of streamed (for example with an output manipulator), standard error from a failed command is logged as a single warning prefixed with ✗.

Verbosity Levels​

LevelDescription
SilentNo output unless a Show* override explicitly enables it
InputOnlyOnly command input (no output/errors)
NormalInput, output, and errors on failure (default)
DetailedAbove plus exit code and duration
DiagnosticEverything including working directory and timestamps

Per-Command Configuration​

await context.Tools.DotNet.BuildAsync(
new DotNetBuildOptions { Configuration = "Release" },
new CommandExecutionOptions
{
Logging = new CommandLoggingOptions
{
Verbosity = CommandLogVerbosity.Detailed
}
});

Global Defaults​

Set default logging for all commands at the pipeline level:

var builder = Pipeline.CreateBuilder(args);

// All commands will use Silent logging unless overridden
builder.ConfigureCommands(commands => commands with
{
Logging = CommandLoggingOptions.Silent,
});

// Or use Diagnostic for debugging
builder.ConfigureCommands(commands => commands with
{
Logging = CommandLoggingOptions.Diagnostic,
});

await builder.RunAsync();

Configure Microsoft.Extensions.Logging providers and filters through the builder's logging surface:

builder.Logging
.ClearProviders()
.AddConsole()
.SetMinimumLevel(LogLevel.Information);

Using Presets​

// Silent - no command logging
new CommandExecutionOptions { Logging = CommandLoggingOptions.Silent }

// Diagnostic - maximum verbosity
new CommandExecutionOptions { Logging = CommandLoggingOptions.Diagnostic }

// Default - normal verbosity
new CommandExecutionOptions { Logging = CommandLoggingOptions.Default }

Fine-Grained Control​

Each command Show* setting is nullable: null follows the verbosity level, true enables it, and false disables it, including with Silent and Diagnostic. Presets set only verbosity, so changing a preset's verbosity does not leave hidden overrides behind. ShowCommandArguments = false masks command input; the command start and status markers can still be logged.

Working directories appear only at Diagnostic by default. ShowWorkingDirectory overrides that choice. ShowTimestamps prefixes each command log event (start, streamed/captured output, and completion) with its UTC logging time in ISO 8601 format. It does not recover timestamps from already-captured process output. Both settings also apply to dry-run command starts.

With ShowStandardError = null, captured stderr is logged only on command failure; streamed stderr is logged at Normal or above because tools also use it for progress. An explicit true logs stderr even for successful captured commands; false disables both paths.

Override individual settings regardless of verbosity level:

new CommandLoggingOptions
{
Verbosity = CommandLogVerbosity.Normal,
ShowCommandArguments = true,
ShowStandardOutput = true,
ShowStandardError = true,
ShowExitCode = true,
ShowExecutionTime = true,
ShowWorkingDirectory = false,
ShowTimestamps = false
}

Captured Output Limits​

CommandExecutionOptions.MaxCapturedOutputLength defaults to 1,048,576 characters per stream. Longer output retains its beginning and end, with ... [truncated N characters] ... in the middle. This affects CommandResult capture, not the complete streamed log output.

Check StandardOutputTruncatedCharacters and StandardErrorTruncatedCharacters before parsing captured JSON or other structured output. Each count is zero when that stream is complete. The counts are also available on CommandException.Result. A warning names the limit and reports both counts when capture truncates output and effective command verbosity is Normal, Detailed, or Diagnostic. Silent and InputOnly suppress this warning; the result counts remain available.

Raise the limit for commands that produce large results, or use 0 for unlimited capture (which retains all output in memory):

var result = await context.Shell.RunAsync("kubectl", ["get", "pods", "-o", "json"],
new CommandExecutionOptions { MaxCapturedOutputLength = 8 * 1024 * 1024 });

if (result.StandardOutputTruncatedCharacters > 0)
{
throw new InvalidOperationException("Command output is incomplete; increase the capture limit.");
}

Output Manipulators​

Transform logged content before it's written (useful for truncating large outputs or redacting sensitive data):

new CommandExecutionOptions
{
Logging = new CommandLoggingOptions { Verbosity = CommandLogVerbosity.Normal },
InputLoggingManipulator = input => input.Length > 500
? input.Substring(0, 500) + "... (truncated)"
: input,
OutputLoggingManipulator = output => output.Replace("api-key-value", "***")
}

Configuration Precedence​

Logging settings are resolved in this order (highest to lowest priority):

  1. Per-Call: CommandExecutionOptions.Logging on individual command calls
  2. Global Default: Commands.Logging configured at pipeline level
  3. System Default: CommandLoggingOptions.Default (Normal verbosity)

HTTP Logging​

HTTP logging uses the same Logging name at both scopes. Per-request options override the pipeline default:

await context.Network.Http.SendAsync(new HttpOptions(request)
{
Logging = HttpLoggingOptions.Minimal,
});

builder.ConfigureHttp(http => http with
{
Logging = HttpLoggingOptions.Silent,
});

Use HttpLoggingOptions properties for fine-grained request, response, status-code, duration, header, and body logging. Both command and HTTP logging use the Show* prefix and Silent, Default, and Diagnostic presets. HTTP additionally provides Minimal (URL, status, and duration) and Headers (no bodies). HTTP Diagnostic logs bodies up to 64 KiB; Default uses a 4 KiB limit. HTTP flags remain bool, since HTTP has no verbosity level to inherit. HttpLoggingType no longer exists.

var logging = HttpLoggingOptions.Diagnostic with
{
ShowRequestBody = false,
ShowResponseBody = false,
ShowDuration = true,
};

For earlier V4 builds, rename HTTP LogRequest, LogResponse, LogRequestHeaders, LogResponseHeaders, LogRequestBody, LogResponseBody, LogStatusCode, and LogDuration to ShowRequest, ShowResponse, ShowRequestHeaders, ShowResponseHeaders, ShowRequestBody, ShowResponseBody, ShowStatusCode, and ShowDuration. Rename HttpLoggingOptions.None to Silent and HttpLoggingOptions.Full to Diagnostic.