Skip to content
ConsoleMonster

Hosting dashboard

ConsoleMonster.Hosting adds a live dashboard to a .NET generic host: each hosted service's state, failures and the log.

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.

dotnet run (ConsoleMonster.Hosting) 110×1724-bitFull
C#ConsoleMonster.Examples.Extensions/Program.cs
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

.NET 8 and later
dotnet add package ConsoleMonster.Hosting
dotnet add package ConsoleMonster.Extensions.Logging

Reference: 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. A BackgroundService shows completed when ExecuteAsync returns and failed when it throws.
  • Your own entries through IConsoleDashboard, which you can inject anywhere: SetStatus("Import", ServiceState.Running, "file 3 of 10"), SetDetail, Log and Snapshot.
  • A log region that ConsoleMonster.Extensions.Logging writes 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().

OptionDefaultMeaning
Titleapplication nameThe header title.
ModeAutoAuto, Always or Never.
ShowServicestrueLists the hosted services and your entries.
ShowLogtrueShows the log region.
LogHeight8Rows of the log region.
KeepFinalStatetrueLeaves the last frame in the scrollback.
ServiceNametype nameNames a hosted service from its type.

Edit this page on GitHub