Skip to content
ConsoleMonster

Logging

A Microsoft.Extensions.Logging provider with ConsoleMonster's level tags, scopes and exceptions, and plain lines when output is redirected.

dotnet add package ConsoleMonster.Extensions.Logging

The provider depends only on Microsoft.Extensions.Logging.Abstractions and ConsoleMonster, and works with trimming and native AOT on .NET 8 and later.

Log entries the ConsoleMonster way

Every level, a scope, a structured message and an exception, through the Microsoft.Extensions.Logging provider.

dotnet run (ConsoleMonster.Extensions.Logging) 76×1024-bitFull
C#ConsoleMonster.Examples.Extensions/Program.cs
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);
}
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
dotnet add package ConsoleMonster.Extensions.Logging

Reference: Logging, Package README

What it writes

  • The level tag and colour that Write-ConsoleLog and the dashboard log region use: ✗ ERR for Error and Critical, ▴ WARN for Warning, DBG and VERB muted for Debug and Trace, and no tag for Information.
  • A timestamp (TimestampFormat, default HH:mm:ss; null leaves it out), then the category (the last part by default, Full or None) and the scopes in brackets.
  • An exception's type and message under the entry, with up to MaxExceptionLines lines of details.
  • While a ConsoleMonster dashboard or AddConsoleDashboard shows, entries go into its log region, so they never print over the view.
  • With output redirected, plain lines without escape codes.

Options

OptionDefaultMeaning
MinimumLevelTraceThe lowest level written; the logging builder's filters apply first.
TimestampFormatHH:mm:ssA .NET date format, or null for no timestamp.
UseUtcTimestampfalseUTC instead of local time.
CategoryShortShort, Full or None.
IncludeScopestrueShows the scopes after the category.
IncludeEventIdfalseShows the event id.
MaxExceptionLines12Lines of exception details; 0 for the type and message only.
LogRegion$logThe live log region to write into while one runs; null always prints.

Without dependency injection, use loggerFactory.AddConsoleMonster(). The provider is added once: a second AddConsoleMonster keeps the first call's options. It is thread-safe.

Edit this page on GitHub