A Microsoft.Extensions.Logging provider that renders log entries through
Spectre.Console with
first-class awareness of CI runners.
- Rich ANSI colour, exception rendering, scope handling.
- Type-aware placeholder highlighting (
int→cyan,string→yellow, ...) with name-hint overrides (UserId,Email,StatusCode, ...). - Secret masking by regex on placeholder names (
password,token,secret,apikey,bearer, ...). - Interactive vs non-interactive detection with sensible ANSI behaviour.
- Channel-based background writer, single consumer, ordered output.
- Works with
[LoggerMessage]source-generated logging.
Auto-detected from environment variables. Runners with native renderers emit collapsible groups, level annotations, and (where supported) secret masks:
| Runner | Group syntax | Level annotations | Secret mask |
|---|---|---|---|
| GitHub Actions | ::group:: / ::endgroup:: |
::error:: / ::warning:: (::debug:: opt-in) |
::add-mask:: |
| Azure Pipelines | ##[group] / ##[endgroup] |
##[error] / ##[warning] (##[debug] opt-in) |
— |
| GitLab CI | section_start / section_end |
— | — |
| TeamCity | ##teamcity[blockOpened] |
##teamcity[message status=...] |
— |
| Buildkite | --- <label> |
— | — |
| Travis | travis_fold:start/end |
— | — |
Jenkins, CircleCI, and AppVeyor are detected and use a passthrough renderer: plain ANSI output with no grouping or annotations.
dotnet add package MEL.Spectreusing Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MEL.Spectre;
var services = new ServiceCollection()
.AddLogging(builder => builder.AddSpectreConsole());
var sp = services.BuildServiceProvider();
var logger = sp.GetRequiredService<ILogger<Program>>();
logger.LogInformation("User {UserId} logged in", 42);AddSpectreConsole removes the registered ConsoleLoggerProvider so you do
not get duplicate output.
Scalar options can also be loaded from the provider's standard logging configuration section:
{
"Logging": {
"SpectreConsole": {
"Template": "[{Timestamp:HH:mm:ss} {Level:u5}] {Message}",
"CiMode": "Auto",
"WriteMode": "Background",
"IncludeScopes": true
}
}
}An AddSpectreConsole(options => ...) callback is applied after configuration
binding and therefore overrides configured values.
using Microsoft.Extensions.Logging;
using Spectre.Console;
using MEL.Spectre;
using MEL.Spectre.Theme;
builder.AddSpectreConsole(o =>
{
o.Theme = SpectreTheme.Dark
.ForLevel(LogLevel.Information, new Style(Color.Green))
.WithPlaceholders(p =>
{
p.ForName("UserId", Color.Aqua);
p.ForType<bool>(Color.Magenta1);
});
o.Theme.MessageStyle = new Style(Color.White);
});Built-in themes: Default, Dark, Light, Monochrome.
Both
SpectreThemeand itsPlaceholderStyleResolverare configure-once: they freeze when the provider is constructed. Mutating styles or adding rules afterwards throwsInvalidOperationExceptionfrom the setter / fluent call. Invalid regex patterns, malformed templates, and out-of-range timeouts all fail validation at host startup viaIValidateOptions<SpectreConsoleLoggerOptions>(chained with.ValidateOnStart()).
Use LogMarkup to enable Spectre markup for one trusted event while ordinary
messages keep treating brackets as literal text:
logger.LogMarkup("[green]Deployment succeeded[/]");
logger.LogMarkup(LogLevel.Warning, "[yellow]Deployment delayed[/]");LogMarkup(string) logs at Information; overloads accept a level or the full
level/event/exception tuple. Escape any untrusted values with Markup.Escape
before composing them into trusted markup. The global
AllowMarkupInMessageTemplate option remains available for compatibility.
builder.AddSpectreConsole(o =>
{
o.CiMode = CiMode.GitHubActions; // or Auto, Off, AzurePipelines, etc.
});CI log entries render without wrapping by default, even when a consumer-supplied
console has a narrow profile. Set WrapInCi = true to follow that console width.
Debug and trace entries are ordinary visible log lines by default. Native debug
annotations are often hidden by CI runners; for example, GitHub Actions hides
::debug:: unless ACTIONS_STEP_DEBUG=true. Consumers who enable that runner
setting can opt in explicitly:
builder.AddSpectreConsole(o =>
{
o.CiLevelAnnotations[LogLevel.Debug] = CiAnnotation.Debug;
o.CiLevelAnnotations[LogLevel.Trace] = CiAnnotation.Debug;
});Native annotation payloads are rendered as plain, message-only text by default,
so the runner supplies severity without retaining level labels or separators
from the full output template. Set SuppressInlineLevelOnCiAnnotation = false
to keep the complete template. GitHub Actions annotation payloads also escape
percent signs and embedded newlines so each entry remains one complete workflow
command.
Placeholders whose name matches any of the configured regex patterns are
rendered as ***. On GitHub Actions, MEL.Spectre also emits ::add-mask::
once per distinct value so the unmasked value is redacted from subsequent
build steps. Mutable MaskedValuePatterns defaults detect well-known GitHub,
GitLab, AWS, Slack, JWT, and private-key formats. By default they scan both
placeholder string values and final visible message text, covering secrets
embedded through string interpolation or a pre-formatted message. They also
scan rendered exception text; exception matches are masked before output and
registered with supported CI runners. Set
MaskValuePatternsInMessageText = false to retain placeholder-only behavior
when the per-message regex cost is undesirable.
builder.AddSpectreConsole(o =>
{
o.MaskedNamePatterns.Add("session.*id");
o.MaskedValuePatterns.Add(@"^Bearer\s+\S+");
});Both pattern lists are snapshotted at provider construction; mutations after the provider starts are ignored. Clear either mutable list during configuration to disable its defaults.
MaskValuePatternsInMessageTextis also snapshotted when the provider starts.
Relayed stdout from tools that style their own output (dotnet, npm, ...)
often contains raw ANSI escape sequences. An embedded reset (ESC[0m) would
terminate the logger's own styling mid-line, leaving the rest of the entry
unstyled. By default MEL.Spectre converts embedded SGR color/style sequences
into Spectre markup: the child process's colors are preserved, an embedded
reset closes only the child's style, and the theme's outer style resumes
afterwards. All other control sequences (cursor movement, screen clearing,
OSC titles and hyperlinks, ...) are removed, which also keeps native CI
annotation payloads (e.g. ::error::) free of control codes.
builder.AddSpectreConsole(o => o.EmbeddedAnsi = EmbeddedAnsiMode.Strip); // discard child styling entirely
builder.AddSpectreConsole(o => o.EmbeddedAnsi = EmbeddedAnsiMode.Passthrough); // raw sequences, previous behaviourSanitization only engages for message content that actually contains an
escape character; plain messages (including multiline \r\n text) pass
through unchanged.
The background writer uses a bounded Channel<LogEntry>. When full:
BackpressureMode.Wait(default) — log call spins, then waits up toEnqueueWaitTimeout(default 1 s, must be > 0 and ≤ShutdownDrainTimeout) before dropping with a counter increment.BackpressureMode.DropNewest— drop the incoming entry.BackpressureMode.DropOldest— drop the oldest queued entry.
Drops (backpressure or post-disposal) each emit a one-shot warning to
stderr, falling back to Debug.WriteLine if stderr is unavailable.
Background logging preserves log-entry order but returns from ILogger.Log
before rendering. Flush queued entries before an ordering-sensitive direct
write, and take the shared gate so direct output cannot tear a rendered line:
var control = services.GetRequiredService<ISpectreConsoleLoggerControl>();
var console = services.GetRequiredService<IAnsiConsole>();
await control.FlushAsync(cancellationToken);
using (await control.TryAcquireRenderGateAsync(TimeSpan.FromSeconds(1), cancellationToken)
?? throw new TimeoutException("Console render gate unavailable."))
{
lock (control.SynchronizationLock)
{
console.WriteLine("::endgroup::");
}
}Use TryAcquireRenderGate for synchronous callers. Both methods return an
IDisposable lease; the asynchronous method returns null on timeout. Take
SynchronizationLock only for the direct write itself, as shown, so new and
legacy integrations remain mutually exclusive.
CI hosts that prefer strict same-thread ordering over logging throughput can skip the background channel entirely:
builder.AddSpectreConsole(o => o.WriteMode = WriteMode.Synchronous);In synchronous mode, an entry is rendered before ILogger.Log returns. Direct
writes should still use SynchronizationLock when other threads may write to
the same console.
Use Suspend() to suppress only MEL.Spectre output in the current asynchronous
context. Other registered logging providers remain active:
using (control.Suspend())
{
await RunWithDirectConsoleRenderingAsync();
}WouldRender(category, level) evaluates current LoggerFilterOptions for the
SpectreConsole provider alias without writing a log entry.
MIT