ZLS.QuickLog 2.4.0

dotnet add package ZLS.QuickLog --version 2.4.0
                    
NuGet\Install-Package ZLS.QuickLog -Version 2.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ZLS.QuickLog" Version="2.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ZLS.QuickLog" Version="2.4.0" />
                    
Directory.Packages.props
<PackageReference Include="ZLS.QuickLog" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ZLS.QuickLog --version 2.4.0
                    
#r "nuget: ZLS.QuickLog, 2.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ZLS.QuickLog@2.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ZLS.QuickLog&version=2.4.0
                    
Install as a Cake Addin
#tool nuget:?package=ZLS.QuickLog&version=2.4.0
                    
Install as a Cake Tool

QuickLog

NuGet NuGet Downloads

QuickLog is a high-performance, engine-grade logging system written in C#. It is designed for deterministic behavior, low allocation, and post-mortem analysis, making it especially suitable for game engines, demo engines, tools, and services.

QuickLog deliberately avoids heavy abstractions, reflection, DI containers, and message-template complexity. What you get instead is clarity, control, and speed.


Install

dotnet add package ZLS.QuickLog --version 2.4.0

QuickLog targets net8.0 and net10.0, is verified on Windows and Linux, ships with XML documentation, and has no external package dependencies.


Quick Start

using QuickLog;
using QuickLog.Loggers;

var logger = new QuickLogger(
    logFilePath: "logs/app.log",
    consoleLogging: true,
    fileLogging: true);

logger.Log(LogType.Info, "Hello QuickLog");
logger.Log(LogType.Warn, "Something might be wrong");
logger.Log(LogType.Error, new Exception("Boom"));

logger.Dispose();

For application-wide setup:

LogManager.ConfigureDefault(
    new LoggerOptions()
        .WithAsyncOnly()
        .WithJsonLog("logs/app.jsonl")
        .WithBinaryLog("logs/app.qlog")
        .WithRotation(maxFileBytes: 16 * 1024 * 1024, maxFiles: 5)
        .WithRedaction()
        .WithSpamControl(duplicateThreshold: 8));

var log = LogManager.GetDefaultLogger();
log.Log(LogType.Info, "QuickLog is online");

LogManager.Shutdown();

Core Principles

  • Deterministic behavior
  • Async-first design
  • Bounded memory usage
  • No hidden allocations
  • Crash-safe logging
  • Offline analysis tooling
  • Explicit lifecycle control
  • Zero external dependencies

Features

Logging Core

  • IQuickLog clean interface
  • Strongly typed LogType
  • Caller info via compiler services
  • Exception demystification (stack trace clean-up, zero deps)
  • CRC32 integrity checks
  • Async-flowing scopes (LogScope)
  • Correlation ids plus Activity trace/span capture (LogContext)
  • Built-in sensitive value redaction
  • Thread roles (ThreadContext)

Sinks

  • Console
  • File (text)
  • Trace
  • Event-only
  • Memory (circular buffer)
  • Binary (CRC protected)

Async Pipeline

  • Dedicated background dispatcher
  • Bounded queue
  • Configurable drop policies
  • Severity-aware dropping
  • Thread-role-aware dropping
  • Dispatcher health counters
  • Duplicate message coalescing
  • Async-only mode (no sync IO)
  • Deterministic flush & shutdown
  • Startup banners and shutdown summaries
  • Runtime minimum levels and per-sink thresholds
  • Low-noise helpers: log-once, rate-limited logs, frame hitches, asset markers

Exception Ownership (v2.0)

  • Hook AppDomain.UnhandledException and TaskScheduler.UnobservedTaskException
  • Log every captured exception automatically
  • Modal popup on Windows (native MessageBoxW — zero deps), safe stderr fallback elsewhere
  • Structured JSON crash dump (crash_*.json)
  • Auto-restart on fatal exceptions (with loop guard)
  • Recovery delegate for non-fatal task exceptions
  • ExceptionCaught event for custom side-effects
  • Per-exception filter delegate

Godot Integration (v2.0)

  • Route GD.Print, GD.PrintErr, GD.PushError, GD.PushWarning, GDScript/shader errors through QuickLog
  • Dynamic Godot.Logger subclass via Reflection.Emit — zero compile-time Godot dependency
  • Manual bridge template for guaranteed Godot 4 C# compatibility
  • Native OS.Alert() popup for exception dialogs inside Godot
  • One-liner setup via LogManager.AttachGodotHooks()

Tooling

  • Binary log reader
  • Binary log exporter
  • Binary log query/filtering by level, time, correlation, or text
  • Zero-dependency QuickLog.Tools CLI
  • Doctor / inspect / replay / benchmark / bundle commands
  • Source-less launch and passive observe helpers
  • Timeline TUI viewer
  • Colorized output
  • Search + highlighting
  • Level / role toggles
  • Grouping by time slices
  • Filter presets (save/load)

Linux Support (v2.4)

  • XDG-aware log roots via LoggerOptions.ForLinux(...)
  • $XDG_STATE_HOME/<app>/logs preferred, ~/.local/state/<app>/logs fallback
  • Ubuntu CI and Linux smoke project coverage
  • Active-log inspection while apps keep JSONL/QLOG files open
  • No Linux-specific runtime dependencies

Basic Usage

var logger = new QuickLogger(
    logFilePath: "logs/app.log",
    consoleLogging: true,
    fileLogging: true);

logger.Log(LogType.Info,  "Hello QuickLog");
logger.Log(LogType.Warn,  "Something might be wrong");
logger.Log(LogType.Error, new Exception("Boom"));

LogManager — Centralized Setup

// Configure once at startup
LogManager.ConfigureDefault("app.log");

// Get a named logger anywhere in the codebase
var logger = LogManager.GetLogger("Database");
var logger = LogManager.GetLogger(typeof(MyClass));

// Access the default logger
var log = LogManager.GetDefaultLogger();

v2.3 Lean Engine Setup

using QuickLog.Core;
using QuickLog.Loggers;

LogManager.ConfigureDefault(
    LoggerOptions.ForEngine("logs")
        .WithMinimumLevel(LogType.Trace)
        .WithSinkMinimumLevel("console", LogType.Warn));

var logger = LogManager.GetDefaultLogger();
var quickLogger = (QuickLogger)logger;

using (LogContext.BeginCorrelation(Guid.NewGuid().ToString("N")))
using (var session = LogSession.Begin(logger, "startup", quickLogger.SessionId))
{
    logger.Log(LogType.Info, "Game boot sequence started");
    quickLogger.LogOnce("renderer.init", LogType.Info, "Renderer initialized");
    quickLogger.LogEvery("net.retry", TimeSpan.FromSeconds(30), LogType.Warn, "Retrying lobby server");
    quickLogger.LogFrameTime(42, TimeSpan.FromMilliseconds(18), TimeSpan.FromMilliseconds(16));
    session.Bookmark("first-frame");
}

LogManager.Shutdown();

ForEngine enables the dependency-free diagnostics path: async-only dispatch, JSON Lines, CRC-protected binary logs, size-based rotation, crash-safe redaction, duplicate coalescing, a startup banner, a shutdown summary, and an auto-generated session id.

Other lean profiles:

var service = LoggerOptions.ForService("logs");
var tool = LoggerOptions.ForTool("asset-packer");
var godot = LoggerOptions.ForGodot("user://logs");
var linux = LoggerOptions.ForLinux("my-game");

ForLinux("my-game") writes JSON Lines and QLOG output below $XDG_STATE_HOME/my-game/logs when XDG_STATE_HOME is set, otherwise below ~/.local/state/my-game/logs. Pass logDirectory when a service manager, container, or launcher owns the output path:

LogManager.ConfigureDefault(
    LoggerOptions.ForLinux("my-service", logDirectory: "/var/log/my-service")
        .WithMinimumLevel(LogType.Info));

Linux smoke check from the repo:

dotnet run --project samples/QuickLog.LinuxSmoke -- /tmp/quicklog-linux-smoke
dotnet run --project QuickLog.Tools -- doctor /tmp/quicklog-linux-smoke --recursive

Validate options before shipping a preset from config:

var result = LoggerOptions.ForEngine("logs").Validate();
foreach (var issue in result.Issues)
    Console.WriteLine($"{issue.Severity} {issue.Code}: {issue.Message}");

Crash State And Fingerprints

LogStateSnapshot.Set("map", "e1m1");
LogStateSnapshot.Set("phase", "loading");

LogManager.AttachExceptionHooks(new ExceptionHookOptions
{
    CrashDump = new CrashDumpOptions
    {
        Enabled = true,
        Redaction = LogRedactionOptions.CrashSafe()
    }
});

Crash dumps include a stable fingerprint, a repeat count for duplicate crashes, recent log tail, dispatcher stats, and the current state snapshot. State values are redacted before they are written.


QLOG Attributes

[QLOG(...)] marks classes, constructors, and methods for explicit dependency-free instrumentation. QuickLog does not weave IL or create proxies; you opt in by running the marked method through QLogRunner or by adding one scope line inside the method.

public sealed class AssetCompiler
{
    [QLOG(LoggingOption.Default)]
    public void BuildAtlas()
    {
        // work
    }
}

var compiler = new AssetCompiler();
QLogRunner.Invoke(logger, compiler.BuildAtlas);

Inside a method, use the scope helper:

[QLOG(QLogOption.Entry | QLogOption.Exit | QLogOption.Timing)]
public void LoadMap(IQuickLog logger)
{
    using var qlog = QLogScope.Enter(logger);
    // work
}

Discovery is also dependency-free:

var markedTargets = QLogDiscovery.Scan(typeof(AssetCompiler).Assembly);

logger.AsyncOnly = true;
logger.EnableAsyncLogging = true;

logger.AsyncDropPolicy = AsyncDropPolicy.DropBelowLevel;
logger.AsyncMinimumLevel = LogType.Error;

This ensures:

  • No blocking IO on the game thread
  • No frame hitches
  • Critical logs are never dropped

Dispatcher Health

if (LogManager.GetDefaultLogger() is QuickLog.Loggers.QuickLogger quickLogger)
{
    var stats = quickLogger.GetStats();
    Console.WriteLine($"written={stats.Written} dropped={stats.DroppedTotal}");
}

The dispatcher tracks queue depth, capacity, written entries, dropped entries, sink failures, and the last sink error without adding a metrics dependency.


Thread Roles

Assign once per thread:

ThreadContext.Set(ThreadRole.Render);
ThreadContext.Set(ThreadRole.Audio);
ThreadContext.Set(ThreadRole.Network);

All logs from that thread are tagged accordingly, and the async drop policy can protect or deprioritize specific roles.


Scopes

using (LogScope.Begin("Frame", frameId))
using (LogContext.BeginCorrelation(matchId))
{
    logger.Log(LogType.Trace, "Rendering frame");
}

Scopes and correlation ids flow through async continuations and are written to JSON Lines, binary logs, crash dump log tails, and text exports.


Redaction & Duplicate Control

LogManager.ConfigureDefault(
    new LoggerOptions()
        .WithAsyncOnly()
        .WithBinaryLog("logs/app.qlog")
        .WithRedaction(options => options.SensitiveKeys.Add("session"))
        .WithSpamControl(duplicateThreshold: 8));

Redaction masks common secrets before async sinks and crash dumps see them. Duplicate control keeps hot repeated messages from flooding disk by emitting a summary entry after the threshold is crossed.

Built-in presets keep common cases short:

var secrets = LogRedactionOptions.Secrets();
var network = LogRedactionOptions.Network();
var userData = LogRedactionOptions.UserData();
var crashSafe = LogRedactionOptions.CrashSafe();

Exception Ownership (v2.0)

QuickLog can take full ownership of every unhandled exception in your process — logging it, writing a crash dump, showing a popup, and optionally restarting.

One-liner setup

LogManager.ConfigureDefault("app.log");
LogManager.AttachExceptionHooks();          // owns all unhandled exceptions from here

Full options

LogManager.AttachExceptionHooks(new ExceptionHookOptions
{
    ShowPopup             = true,
    ShowStackTraceInPopup = true,
    ExceptionLogType      = LogType.Crit,
    PopupTitle            = "My App — Unhandled Exception",

    // Crash dump — written to %TEMP%\QuickLogCrashDumps\crash_*.json
    CrashDump = new CrashDumpOptions
    {
        Enabled      = true,
        MaxDumpFiles = 10
    },

    // Auto-restart on fatal AppDomain exceptions
    Restart = new RestartOptions
    {
        EnableAutoRestart  = true,
        MaxRestartCount    = 3,
        DelayBeforeRestart = TimeSpan.FromMilliseconds(500),

        // Recovery delegate for non-fatal unobserved task exceptions
        RecoveryAction = ex =>
        {
            if (ex is InvalidOperationException && ex.Message.Contains("connection"))
            {
                ResetConnectionPool();
                return true;   // recovered — skip log/dump/popup
            }
            return false;      // not recovered — proceed normally
        }
    },

    // Filter: ignore specific exceptions entirely
    ExceptionFilter = (ex, source) => ex is not OperationCanceledException
});

Crash dump format

Each crash is written as a structured JSON file:

{
  "Timestamp": "2026-05-11T07:05:42Z",
  "Source": "AppDomain",
  "IsTerminating": true,
  "RestartCount": 0,
  "Fingerprint": "D7C9E3180B4A71C2",
  "RepeatCount": 1,
  "Exception": {
    "Type": "System.AccessViolationException",
    "Message": "Critical failure: memory corruption detected.",
    "StackTrace": "..."
  },
  "Process": {
    "Id": 1234,
    "Name": "MyApp",
    "Executable": "C:\\MyApp\\MyApp.exe",
    "MemoryBytes": 47259648
  },
  "Environment": {
    "MachineName": "WORKSTATION-01",
    "OsVersion": "Microsoft Windows NT 10.0.26200.0",
    "RuntimeVersion": "8.0.22"
  },
  "RecentLogs": [
    {
      "Level": "Error",
      "Message": "Lost connection to asset server",
      "Scope": "Frame:18442",
      "CorrelationId": "match-7"
    }
  ],
  "Dispatcher": {
    "Written": 128,
    "DroppedTotal": 0,
    "SinkFailures": 0
  },
  "State": {
    "map": "e1m1",
    "phase": "loading"
  }
}

Subscribe to the raw event

ExceptionHookManager.ExceptionCaught += (_, args) =>
{
    // args.Exception, args.Source, args.IsTerminating
    // Set args.SuppressDefaultHandling = true to skip log + popup
    UploadCrashReport(args.Exception);
};

Check restart count

// At startup — know if the process was restarted after a crash
if (RestartOptions.CurrentRestartCount > 0)
    logger.Log(LogType.Warn, $"Restarted after crash (attempt #{RestartOptions.CurrentRestartCount})");

Godot Integration (v2.0)

QuickLog integrates directly with the Godot 4 C# engine — routing all engine output through QuickLog and hijacking unhandled exceptions with native OS.Alert() dialogs.

One-liner setup (in your AutoLoad or _Ready())

LogManager.ConfigureDefault("user://logs/game.log");
LogManager.AttachGodotHooks();

This automatically:

  • Intercepts GD.Print, GD.PrintErr, GD.PushError, GD.PushWarning, GDScript errors
  • Hooks all unhandled .NET exceptions with a native OS.Alert() popup
  • Writes crash dumps to %TEMP%\QuickLogCrashDumps on every fatal exception

Full options

LogManager.AttachGodotHooks(new GodotLogOptions
{
    InterceptPrint       = true,
    InterceptPrintError  = true,
    InterceptErrors      = true,
    InterceptWarnings    = true,
    InterceptScriptErrors = true,

    PrintLogType        = LogType.Info,
    PrintErrorLogType   = LogType.Error,
    ErrorLogType        = LogType.Error,
    WarningLogType      = LogType.Warn,
    ScriptErrorLogType  = LogType.Crit,

    HijackExceptions    = true,   // wraps ExceptionHookManager with OS.Alert popup
    ExceptionOptions    = new ExceptionHookOptions
    {
        CrashDump = new CrashDumpOptions { Enabled = true }
    }
});

Check if dynamic Logger registration succeeded

LogManager.AttachGodotHooks();

if (!GodotLogInterceptor.IsDynamicSinkRegistered)
    GD.Print("QuickLog: manual bridge needed — see GodotBridge docs");

Manual bridge (guaranteed to work in all Godot 4 C# setups)

If IsDynamicSinkRegistered is false, add these two files to your Godot project:

// QuickLogSink.cs  (inside your Godot project, NOT in QuickLog)
public partial class QuickLogSink : Godot.Logger
{
    public override void _LogMessage(string message, bool error)
        => GodotBridge.HandleMessage(message, error);

    public override void _LogError(string function, string file, int line,
        string code, string rationale, bool errorType, int errorTypeValue,
        Godot.Collections.Array<ScriptBacktrace> scriptBacktraces)
        => GodotBridge.HandleError(function, file, line, code, rationale, errorTypeValue);
}
// In your AutoLoad _Ready():
OS.AddLogger(new QuickLogSink());

Everything else — routing, log levels, crash dumps, popups — is handled automatically.

Subscribe to Godot log events

GodotLogInterceptor.GodotLogReceived += (_, args) =>
{
    // args.Source, args.Message, args.Function, args.File, args.Line
    // Set args.SuppressLogging = true to skip the QuickLog forward
};

Godot file logger

// Writes to user:// when running under Godot, falls back to %LOCALAPPDATA%\GodotUser
var logger = new GodotFileLogger("game.log", subfolder: "logs");
Console.WriteLine(logger.IsUsingGodotPath);  // true when inside Godot
Console.WriteLine(logger.FullPath);

Binary Logs & Analysis

Export to text

BinaryLogExporter.ExportToText("quicklog.bin", "recovered.log");

Text exports include the source location, scope, correlation id, trace id, and span id when those fields are present.

Query

var errors = BinaryLogQuery.WithLevel(
    "quicklog.bin",
    LogType.Error | LogType.Crit);

var bootLogs = BinaryLogQuery.ContainingText("quicklog.bin", "boot");
var matchLogs = BinaryLogQuery.WithCorrelation("quicklog.bin", "match-7");

Timeline Viewer

BinaryLogTimelineViewer.Run("quicklog.bin");

Summary, merge, and repair

var summary = BinaryLogSummary.FromFile("logs/app.qlog");
BinaryLogMerge.Merge(["logs/a.qlog", "logs/b.qlog"], "logs/merged.qlog");
var repair = BinaryLogRepair.Repair("logs/bad.qlog", "logs/fixed.qlog");

Controls:

↑ ↓        Navigate
PgUp/PgDn  Jump
G          Group by time
/          Search (highlighted)
L          Toggle log levels
R          Toggle thread roles
F5         Save filter preset
F9         Load filter preset
Esc        Exit

QuickLog.Tools (v2.3 Lean Diagnostics)

QuickLog.Tools is a zero-external-dependency companion CLI. It references QuickLog, uses only the .NET runtime libraries, and keeps the core logger clean.

Run it from the repo:

# quicklog-test: parses
dotnet run --project QuickLog.Tools -- doctor logs --recursive
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- inspect logs/app.qlog --level Error --correlation match-7
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- replay logs/app.qlog --to jsonl --out logs/app.replay.jsonl
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- benchmark --iterations 10000 --mode binary
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- bundle --out support.zip --logs logs --crashes crashes --include-env --include-exports

Lean v2.3 commands:

# quicklog-test: parses
dotnet run --project QuickLog.Tools -- tail logs/app.jsonl --lines 20
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- grep error logs --recursive
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- diff logs/old.qlog logs/new.qlog
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- stats logs/app.qlog
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- redact logs/app.log --out logs/app.clean.log
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- summarize logs/app.qlog --out artifacts/quicklog-summary.json
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- report --out artifacts/quicklog-report.html --logs logs --crashes logs/crashes
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- repair logs/bad.qlog --out logs/fixed.qlog
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- merge logs/a.qlog logs/b.qlog --out logs/merged.qlog
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- timeline logs/app.qlog
# quicklog-test: parses
dotnet run --project QuickLog.Tools -- doctor-config logger-options.json

report writes one static HTML file with inline CSS and no scripts. repair scans for valid QLOG records and salvages what can be read; it is a recovery tool, not a guarantee that every corrupt byte can be reconstructed.

Source-less Diagnostics

Use launch when QuickLog should start the selected app and capture stdout, stderr, process lifetime, and QuickLog session artifacts:

dotnet run --project QuickLog.Tools -- launch --out sessions/app --diagnostic-env --wait-for-exit -- dotnet --info

Use observe when the process is already running:

dotnet run --project QuickLog.Tools -- observe --pid 1234 --duration 10 --out sessions/observe-1234

observe is passive. It records process metadata, memory/thread samples, and a best-effort diagnostic-port probe. It does not inject code into the process and does not claim deep EventPipe capture without the diagnostics client dependency that QuickLog intentionally avoids.

Profiler Helper

The profiler command is an experimental helper for CLR profiler environment blocks. It does not ship a native profiler DLL.

dotnet run --project QuickLog.Tools -- profiler explain
dotnet run --project QuickLog.Tools -- profiler env --clsid 00000000-0000-0000-0000-000000000000 --path C:\Tools\Profiler.dll

Only use launch, observe, or profiler settings on applications you own or are authorized to inspect.


Shutdown

Always shut down explicitly — this flushes async queues, detaches all hooks, and ensures no logs are lost:

LogManager.Shutdown();

What QuickLog Is NOT

  • QuickLog, QuickLog.Tools, and QuickLog.Sample do not carry runtime NuGet package dependencies.
  • The test suite includes a dependency-policy check so this stays visible during release work.
  • Not a DI-based framework
  • Not a message-template logger
  • Not reflection-heavy at runtime
  • Not opinionated about formatting
  • Not hiding behavior behind magic

QuickLog is infrastructure, not ceremony.


License

MIT


Status

Component Status
Core logging / sinks Production-ready
Async pipeline Production-ready (v2.3 low-noise helpers + health stats)
Binary logs & tooling Production-ready (v2.3 repair/merge/report utilities)
Exception ownership Stable (v2.0)
Crash dump writer Stable (v2.3 fingerprints + state snapshots)
Godot integration Stable bridge and file logging; dynamic engine sink is best-effort with manual bridge fallback
Linux support Verified (v2.4 XDG profile + Ubuntu smoke coverage)
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.4.0 119 5/23/2026
2.3.1 98 5/19/2026
2.3.0 97 5/17/2026
2.2.0 104 5/13/2026
2.1.0 95 5/11/2026

QuickLog v2.4.0 adds verified Linux support with XDG-aware log profiles, Ubuntu CI and smoke coverage, active-log reader hardening, strict CLI option validation, and clean sample reruns. Zero runtime dependencies.