dotnet add package ConsoleMonster.Hosting
dotnet add package ConsoleMonster.Extensions.Logging
The dashboard depends only on Microsoft.Extensions.Hosting.Abstractions and ConsoleMonster.
A generic host with a live dashboard
Three hosted services: a queue worker, a cache warmer that completes and a report builder that fails. Log lines go into the dashboard's log region.
09:56:38 ✗ ERR Host BackgroundService failed ✗ InvalidOperationException The report template folder is missing. at ReportBuilder.ExecuteAsync(CancellationToken stoppingToken) ConsoleMonster.Examples.Extensions/Program.cs:117 + 1 framework frame Orders · Staging ctrl+c stops · 00:00:09 ● Queue running 9.3s 49 waiting ✓ CacheWarmer completed 6.8s ✗ ReportBuilder failed 5.3s InvalidOperationException: The report template folder is missing. ── log · times since 09:56:34 ─────────────────────────────────────────────────────────────────────────────── 00:05 QueueWorker Picked up order 1047 00:06 QueueWorker Picked up order 1048 00:06 ▴ WARN QueueWorker Payment slow for order 1048 00:07 QueueWorker Picked up order 1049 00:08 QueueWorker Picked up order 1050 00:09 QueueWorker Picked up order 1051
private static async Task HostingAsync(int seconds) {
HostApplicationBuilder builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { ApplicationName = "Orders", EnvironmentName = "Staging" });
builder.Logging.ClearProviders();
builder.Logging.AddConsoleMonster();
builder.Services.Configure<HostOptions>(options => options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore);
builder.Services.AddHostedService<QueueWorker>();
builder.Services.AddHostedService<CacheWarmer>();
builder.Services.AddHostedService<ReportBuilder>();
builder.Services.AddConsoleDashboard(dashboard => dashboard.LogHeight = 6);
using IHost host = builder.Build();
using CancellationTokenSource stop = new CancellationTokenSource(TimeSpan.FromSeconds(seconds));
await host.RunAsync(stop.Token).ConfigureAwait(false);
}Full program
using System;
using System.Threading;
using System.Threading.Tasks;
using ConsoleMonster.Extensions.Logging;
using ConsoleMonster.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ConsoleMonster.Examples.Extensions {
/// <summary>Examples for the logging provider and the hosting dashboard: <c>logging</c> or <c>hosting [seconds]</c>.</summary>
public static class Program {
/// <summary>Runs an example.</summary>
/// <param name="args"><c>logging</c> or <c>hosting [seconds]</c>.</param>
/// <returns>The exit code.</returns>
public static async Task<int> Main(string[] args) {
string name = args.Length > 0 ? args[0].ToLowerInvariant() : "logging";
switch (name) {
case "logging":
Logging();
return 0;
case "hosting":
int seconds = args.Length > 1 && int.TryParse(args[1], out int value) ? value : 12;
await HostingAsync(seconds).ConfigureAwait(false);
return 0;
default:
Console.Error.WriteLine("Use: logging | hosting [seconds]");
return 1;
}
}
// Every level, a scope, a structured message and an exception.
private static void Logging() {
using ILoggerFactory factory = LoggerFactory.Create(logging => logging.SetMinimumLevel(LogLevel.Trace).AddConsoleMonster());
ILogger orders = factory.CreateLogger("Shop.Services.Orders");
ILogger cache = factory.CreateLogger("Shop.Infrastructure.Cache");
cache.LogTrace("Probing {Count} cache shards", 4);
cache.LogDebug("Shard {Shard} answered in {Ms} ms", "eu-1", 12);
using (orders.BeginScope("order {Id}", 1042)) {
orders.LogInformation("Payment accepted: {Amount} EUR", 129.90m);
orders.LogWarning("Stock is low for {Sku}: {Left} left", "A-17", 3);
}
try {
throw new TimeoutException("The payment gateway did not answer within 30 s.");
} catch (TimeoutException ex) {
orders.LogError(ex, "Refund for order {Id} failed", 1038);
}
orders.LogCritical("Queue {Queue} is not draining", "orders-eu");
orders.LogInformation("Done; {Processed} orders processed", 57);
}
// A generic host with three workers and the console dashboard; logs go into its log region.
private static async Task HostingAsync(int seconds) {
HostApplicationBuilder builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { ApplicationName = "Orders", EnvironmentName = "Staging" });
builder.Logging.ClearProviders();
builder.Logging.AddConsoleMonster();
builder.Services.Configure<HostOptions>(options => options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore);
builder.Services.AddHostedService<QueueWorker>();
builder.Services.AddHostedService<CacheWarmer>();
builder.Services.AddHostedService<ReportBuilder>();
builder.Services.AddConsoleDashboard(dashboard => dashboard.LogHeight = 6);
using IHost host = builder.Build();
using CancellationTokenSource stop = new CancellationTokenSource(TimeSpan.FromSeconds(seconds));
await host.RunAsync(stop.Token).ConfigureAwait(false);
}
private sealed class QueueWorker : BackgroundService {
private readonly ILogger<QueueWorker> _log;
private readonly IConsoleDashboard _dashboard;
public QueueWorker(ILogger<QueueWorker> log, IConsoleDashboard dashboard) {
_log = log;
_dashboard = dashboard;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
int order = 1040;
while (!stoppingToken.IsCancellationRequested) {
order++;
_dashboard.SetDetail("Queue", (50 - order % 50) + " waiting");
_log.LogInformation("Picked up order {Order}", order);
if (order % 4 == 0) {
_log.LogWarning("Payment slow for order {Order}", order);
}
try {
await Task.Delay(900, stoppingToken).ConfigureAwait(false);
} catch (OperationCanceledException) {
break;
}
}
}
}
private sealed class CacheWarmer : BackgroundService {
private readonly ILogger<CacheWarmer> _log;
public CacheWarmer(ILogger<CacheWarmer> log) {
_log = log;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
await Task.Delay(2500, stoppingToken).ConfigureAwait(false);
_log.LogInformation("Cache warmed: {Entries} entries", 12840);
}
}
private sealed class ReportBuilder : BackgroundService {
private readonly ILogger<ReportBuilder> _log;
public ReportBuilder(ILogger<ReportBuilder> log) {
_log = log;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
await Task.Delay(4000, stoppingToken).ConfigureAwait(false);
_log.LogError("The report template folder is missing");
throw new InvalidOperationException("The report template folder is missing.");
}
}
}
}Use it in your app
dotnet add package ConsoleMonster.Hosting
dotnet add package ConsoleMonster.Extensions.LoggingReference: Hosting dashboard, Package README
What it shows
- A header with the title (default: the application name), the environment and the elapsed time.
- One line per hosted service registered before
AddConsoleDashboard, with its state (waiting, starting, running, stopping, stopped, completed or failed), how long it has been in it and the failure message when it fails. ABackgroundServiceshows completed whenExecuteAsyncreturns and failed when it throws. - Your own entries through
IConsoleDashboard, which you can inject anywhere:SetStatus("Import", ServiceState.Running, "file 3 of 10"),SetDetail,LogandSnapshot. - A log region that
ConsoleMonster.Extensions.Loggingwrites into while the dashboard shows. Remove the default console logger (builder.Logging.ClearProviders()) first, so it does not print over the view.
When the host stops, the dashboard stops last and leaves its final state in the scrollback. Ctrl+C stays with the host.
When it stays out of the way
With Mode = Auto (the default) the dashboard draws only on an interactive terminal: not when output is redirected, in CI or a container log, in a Windows service or a daemon without a terminal. There the host and its logging run exactly as they would without it. Always draws whenever the terminal can, and Never only tracks the statuses for Snapshot().
| Option | Default | Meaning |
|---|---|---|
Title | application name | The header title. |
Mode | Auto | Auto, Always or Never. |
ShowServices | true | Lists the hosted services and your entries. |
ShowLog | true | Shows the log region. |
LogHeight | 8 | Rows of the log region. |
KeepFinalState | true | Leaves the last frame in the scrollback. |
ServiceName | type name | Names a hosted service from its type. |