Become a member!

LoggerPro 2.1 — Async Logging Framework for Delphi

🌐
This article is also available in other languages:
🇮🇹 Italiano  •  🇪🇸 Español  •  🇩🇪 Deutsch
📚
Looking for an older version?
LoggerPro 2.0  •  LoggerPro 1.x (legacy)

TL;DR: LoggerPro 2.1 is the async, thread-safe logging framework for Delphi. It keeps the fluent Builder pattern API from 2.0 and adds external configuration via JSON (LoggerProFromJSONFile or LoggerProFromJSONString), ExeWatch cloud observability, a spec-compliant LogFmt renderer, a FileBySource appender for per-tenant logs, UTF-8 console output for Docker, DLL-safe initialization, ElasticSearch authentication, and more. Drop-in upgrade from 2.0, Apache 2.0 licensed, works from Delphi 10.2 Tokyo through Delphi 13 Florence, on Windows / Linux / macOS / Android / iOS. LoggerPro 2.0 docs → · LoggerPro 1.x docs →

At a Glance

Name LoggerPro
Version 2.1.0 (April 2026)
Language Delphi / Object Pascal
Minimum Delphi 10.2 Tokyo · Tested up to 13 Florence
Platforms Windows, Linux, macOS, Android, iOS
License Apache 2.0 (free for commercial use)
Author Daniele Teti
Repository github.com/danieleteti/loggerpro
Install BOSS · ZIP release
Category Async · Thread-safe · Pluggable · Structured logging
Closest equivalents Serilog (.NET), NLog (.NET), log4j / Logback (Java)

LoggerPro 2.1 Logo

What is LoggerPro?

LoggerPro is the async, thread-safe, pluggable logging framework for Delphi / Object Pascal - the closest Delphi equivalent of Serilog (.NET), NLog (.NET), or log4j / Logback (Java). It offers a fluent Builder API, structured key-value context, pluggable stack-trace formatters, 20+ built-in appenders (file, console, HTTP, ExeWatch cloud observability, Grafana Loki via LogFmt, ElasticSearch, UDP syslog, Windows Event Log, database, VCL, and more), and runs cross-platform on Windows, Linux, macOS, Android, and iOS. LoggerPro is Apache 2.0 licensed, actively maintained since 2010, and used in thousands of production Delphi applications.

Version 2.1 (April 2026) is the current stable release. It builds on the fluent Builder pattern API introduced in 2.0 and adds a first-class ExeWatch integration for end-to-end cloud observability alongside features focused on modern deployment targets - Docker containers, cloud log aggregators (Grafana Loki, Elastic, Datadog), multi-tenant applications, and Delphi DLLs - while remaining fully backward compatible.

What’s New in 2.1

Feature What it gives you
JSON configuration Reshape the logger at deploy time - LoggerProFromJSONFile('loggerpro.json'). Strict field validation, unknown-type diagnostics
HTML live log viewer Self-contained .html file with sticky filter bar, level-based coloring, client-side search, CSV/JSON export, and live tailing in the browser
ExeWatch integration First-class cloud-observability appender for ExeWatch - imperative, fluent, or JSON-config usage
Pluggable appender pattern Optional backends (ExeWatch, WindowsEventLog, ElasticSearch) auto-register from their own unit - just add the uses clause
Runtime MinimumLevel ILogWriter.MinimumLevel is now a first-class property - change the global gate on the fly
Webhook appender Renamed from HTTPAppender; added API-key auth (header or query-string)
LogFmt renderer Spec-compliant key=value output; greppable, Loki-friendly, single-line records
FileBySource appender Per-source (per-tenant, per-client) subfolders with day+size rotation
UTF-8 console output Correct Unicode in Docker containers and Windows consoles (WithUTF8Output)
DLL-safe init Fixes the Windows Loader Lock deadlock when LoggerPro is used from a DLL
ElasticSearch auth Basic Auth, API Key, and Bearer Token support
UDP Syslog local time WithUseLocalTime(True) option on the syslog appender
GetCurrentLogFileName Retrieve the active log file path - useful for upload, email, restart

Upgrading from 2.0? Mostly a drop-in replacement. One breaking change: WriteToHTTP was renamed to WriteToWebhook (a find-and-replace is enough). Everything else is additive.

Quick Start with the Builder API

uses
  LoggerPro,
  LoggerPro.Builder;

var
  Log: ILogWriter;
begin
  Log := LoggerProBuilder
    .WithDefaultTag('MYAPP')
    .WriteToFile
      .WithLogsFolder('logs')
      .WithMaxBackupFiles(5)
      .WithMaxFileSizeInKB(10000)
      .Done
    .WriteToConsole
      .Done
    .Build;

  Log.Info('Application started');
  Log.Debug('Processing item %d', [42], 'WORKER');
  Log.Error('Connection failed', 'DATABASE');

  // Contextual logging
  Log.WithProperty('user_id', 42)
     .WithProperty('session', 'abc123')
     .Info('User logged in', 'auth');

  // Exception logging
  try
    // ... code ...
  except
    on E: Exception do
      Log.LogException(E, 'Operation failed', 'error');
  end;

  // Explicit shutdown before exit
  Log.Shutdown;
end;

JSON Configuration

New in 2.1. Ship your app with a loggerpro.json next to the EXE and reshape the logger at deploy time without rebuilding. Operators can switch backends, flip log levels, or add/remove appenders without touching the source.

Schema at a glance

{
  "configVersion":        1,
  "minimumLevel":         "Debug",
  "defaultMinimumLevel":  "Warning",
  "defaultTag":           "myapp",

  "appenders": [
    { "type": "Console",  "colors": true, "colorScheme": "Midnight" },
    { "type": "File",     "logsFolder": "logs", "maxBackupFiles": 5,
                          "minimumLevel": "Debug" },
    { "type": "HTMLFile", "logsFolder": "logs", "title": "MyApp" }
  ]
}

The global gate accepts everything (minimumLevel: Debug). Every appender without its own minimumLevel picks up the template value Warning - Console and HTMLFile both get it. The File appender opts out and writes the full audit trail at Debug. Setting defaultMinimumLevel below minimumLevel would be a no-op: the global gate would already have dropped those messages before they reach any appender.

Root fields:

Field Type Purpose
configVersion integer Schema version. Missing = latest. Higher than the library’s version = hard error.
minimumLevel string Pre-queue global gate. "Debug" / "Info" / "Warning" / "Error" / "Fatal". Messages below this severity never enter the queue - zero overhead for filtered calls.
defaultMinimumLevel string Template value that every appender uses as its own minimumLevel when the appender entry doesn’t set one explicitly. Not a gate itself - it only pre-fills the per-appender threshold at parse time.
defaultTag string Default tag for calls like Log.Info('msg') with no tag argument.
appenders array Required. One object per appender, each with a "type" and type-specific fields.

Per-appender common fields:

Field Notes
type Required. Case-insensitive, matched against registered type names.
minimumLevel Optional. Minimum severity this appender emits. Messages below are silently dropped by THIS appender (other appenders still see them). Same value domain as root minimumLevel. Defaults to defaultMinimumLevel (or Debug if that’s also absent). Setting it below the root minimumLevel is a no-op: those messages never reach the appender because they didn’t pass the pre-queue gate.

Type-specific fields are listed on each appender’s section in this guide; typos are caught (see Error diagnostics below).

The three concepts, disambiguated. Every log call carries its own severity - Log.Info is Info, Log.Error is Error, and so on. Neither minimumLevel nor defaultMinimumLevel changes that; they only decide who sees what. Root minimumLevel is a pre-queue filter (free: TLogItem isn’t even constructed for messages below it). Per-appender minimumLevel is an emit gate (evaluated on the appender’s worker thread, deciding whether THIS appender writes the message). defaultMinimumLevel at the root is a write-time default for the per-appender gate, not a runtime filter - it lets you say “every appender defaults to Warning unless it says otherwise” without repeating the field on every entry.

What is mandatory, what has defaults

The absolute minimum is a single array entry:

{ "appenders": [ { "type": "Console" } ] }

That’s it. Everything else is either optional or inferred. The appenders key at the root is required (it can be an empty array to build a no-op logger, but it must be present). Each entry must have a "type"; every other field is optional.

Default values when a field is omitted:

Field If absent Notes
Root configVersion Treated as the latest known schema (currently 1). A higher value is a hard error (forward-compat guard).
Root minimumLevel Debug - everything passes the global pre-queue gate. Messages below this severity are filtered before they’re queued (zero overhead).
Root defaultMinimumLevel Debug Used as the default minimumLevel for each appender that doesn’t set its own.
Root defaultTag "main" Used by calls that don’t pass a tag (e.g. Log.Info('msg')).
Appender minimumLevel defaultMinimumLevel (or Debug if that’s also absent). The appender emits only messages at or above this severity.
Appender type-specific fields Each appender ships its own defaults (listed on its section). E.g. File’s logsFolder defaults to the EXE directory; Console defaults to colors ON with the Midnight scheme (use "colorScheme": null or "colors": false to opt out).

Minimal ready-to-use JSONs

Log to a rotated file (the simplest deployment). Picks up the EXE directory for logsFolder and the EXE name as the base filename; 5 backups of 1 MB each.

{
  "appenders": [
    { "type": "File" }
  ]
}

Want to place the files elsewhere and keep more history:

{
  "appenders": [
    {
      "type": "File",
      "logsFolder": "C:/ProgramData/MyApp/logs",
      "fileBaseName": "myapp",
      "maxBackupFiles": 30,
      "maxFileSizeInKB": 5000
    }
  ]
}

Log to the colored console. Colors are ON by default with the professional Midnight scheme - a single-entry array is enough:

{
  "appenders": [
    { "type": "Console" }
  ]
}

Override the scheme by name (case-insensitive), or opt out of colors entirely:

// Pick a different scheme
{ "appenders": [ { "type": "Console", "colorScheme": "Nord" } ] }

// Opt out: explicit null...
{ "appenders": [ { "type": "Console", "colorScheme": null } ] }

// ...or explicit empty string
{ "appenders": [ { "type": "Console", "colorScheme": "" } ] }

// ...or explicit colors:false
{ "appenders": [ { "type": "Console", "colors": false } ] }

All three “opt out” forms produce plain text, no ANSI. Additionally, every colored scheme auto-degrades to plain text when stdout is redirected to a file or piped into another process - you never end up with escape codes in your log files by accident.

The built-in color schemes

Eleven schemes ship with LoggerPro. They all honor the same palette contract (per-level color + colored tag, metadata, context keys and values), they just emphasize different parts. Scheme names are case-insensitive in JSON; the screenshots below are for reference - launch samples/03b_console_with_colors_appender to see every scheme render on your own terminal with the same seed messages.

Scheme Description
Default Gin-inspired foreground-only baseline; per-severity level color.
Monochrome No ANSI - plain text. Auto-applied when stdout is piped/redirected.
GinBadge Default tones plus colored-background “badge” for the level.
GinMinimal Everything dim gray, only the level word keeps color.
GinVibrant Saturated rainbow - every field a distinct loud color.
Midnight Magenta prefix/tag, green keys, cyan values. JSON default - reads well on light and dark terminals.
Nord Frost-theme cool arctic blues/cyans, yellow warnings, red fatal.
Matrix Everything green; levels rendered as background badges.
Amber 80s amber-CRT monitor - yellow/orange family, red only for errors.
Ocean Layered blues and cyans - teal tag, bright cyan values.
Cyberpunk Neon magenta prefix, cyan tag, yellow values; vivid level badges.

Picking one from JSON. Just reference it by name:

// A calm palette for a dev console
{ "appenders": [ { "type": "Console", "colors": true, "colorScheme": "Nord" } ] }
// Level badges - fastest level scanning
{ "appenders": [ { "type": "Console", "colors": true, "colorScheme": "GinBadge" } ] }
// Plain text, no ANSI - useful in tests or when piping to a file
{ "appenders": [ { "type": "Console", "colors": true, "colorScheme": "Monochrome" } ] }

Picking one from code:

uses LoggerPro, LoggerPro.Builder;

Log := LoggerProBuilder
  .WriteToConsole
    .WithColorScheme(LogColorSchemes.Midnight)   // or .Nord / .Matrix / ...
    .Done
  .Build;

Custom scheme. Every scheme is a TLogColorScheme record - you can build your own starting from any preset and overriding the fields that matter:

var lScheme: TLogColorScheme;
begin
  lScheme := LogColorSchemes.Midnight;
  lScheme.TagColor := FORE_YELLOW;
  lScheme.LevelColor[TLogType.Info] := STYLE_BRIGHT + FORE_WHITE;

  Log := LoggerProBuilder
    .WriteToConsole.WithColorScheme(lScheme).Done
    .Build;
end;

Auto-downgrade. When stdout is redirected to a file or piped to another process, the renderer automatically degrades to plain text (equivalent to Monochrome). You never end up with escape codes in your log files by accident.

Other optional fields:

{
  "appenders": [
    {
      "type": "Console",
      "colors": true,
      "colorScheme": "Nord",
      "prefix": "MYAPP",    // shows as [MYAPP] before each line
      "utf8Output": true    // safe output in Docker / Windows consoles
    }
  ]
}

Combine the two for a typical dev setup - colored console plus a rotated file with separate levels:

{
  "minimumLevel": "Debug",
  "appenders": [
    { "type": "Console", "colors": true, "colorScheme": "Midnight" },
    { "type": "File",    "logsFolder": "logs", "minimumLevel": "Info"  }
  ]
}

Entry points

Two global functions on the LoggerPro unit - one returns a ready-to-use ILogWriter, the other returns the unfinalized ILoggerProBuilder so callers can keep chaining:

uses LoggerPro;

// 1. Pure JSON → ILogWriter (simplest, covers most cases)
Log := LoggerProFromJSONFile('loggerpro.json');

// 1b. Pure JSON from an embedded resource or a hand-assembled string
Log := LoggerProFromJSONString(MyEmbeddedConfig);
uses LoggerPro, LoggerPro.Builder;

// 2. JSON base + more appenders from code (the "mixed" pattern)
Log := LoggerProBuilderFromJSONFile('loggerpro.json')
         .WriteToCallback
           .WithCallback(MyProc)
           .Done
         .Build;   // you call .Build yourself

Both functions raise ELoggerProConfigError on any problem with the file - see Error diagnostics below for the exact wording.

Mixing file config with runtime-only appenders

Some appenders cannot be described in JSON because they need a runtime object reference that doesn’t fit into a configuration file: a callback, a TStrings, a VCL component, a live TFDConnection. The pattern is always the same:

  1. Load the JSON-describable part with LoggerProBuilderFromJSONFile.
  2. Chain the extras on the returned builder.
  3. Call .Build yourself.

Example 1 - JSON base + a UI callback for a status bar.

uses
  LoggerPro, LoggerPro.Builder;

Log := LoggerProBuilderFromJSONFile('loggerpro.json')
  .WriteToCallback
    .WithCallback(
      procedure(const aLogItem: TLogItem; const aFormattedMessage: string)
      begin
        // The callback appender marshals to the main thread for us.
        StatusBar1.SimpleText := aFormattedMessage;
      end)
    .WithSynchronizeToMainThread(True)
    .Done
  .Build;

Example 2 - JSON base + a VCL TListView mirror. The user configures files/console from JSON; the GUI live-tail stays in code.

uses
  LoggerPro, LoggerPro.Builder;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FLog := LoggerProBuilderFromJSONFile('loggerpro.json')
    .WriteToVCLListView(ListViewLogs)
      .WithMaxLogLines(1000)
      .Done
    .Build;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  FLog.Shutdown;   // before the form - and the ListView - are freed
  FLog := nil;
end;

Example 3 - JSON base + an FMX TMemo. TMemo.Lines is a TStrings, so the cross-platform WriteToStrings appender covers FMX, VCL, and anything else that surfaces a TStrings.

FLog := LoggerProBuilderFromJSONFile('loggerpro.json')
  .WriteToStrings(MemoLog.Lines)
    .WithMaxLogLines(500)
    .WithClearOnStartup(True)
    .Done
  .Build;

Example 4 - JSON base + a database appender. The JSON controls console/file/HTML; the DB connection stays in code because TFDConnection is not serializable.

uses
  LoggerPro, LoggerPro.Builder, LoggerPro.DBAppender.FireDAC;

FLog := LoggerProBuilderFromJSONFile('loggerpro.json')
  .WriteToFireDAC
    .WithConnectionDefName('MAIN_DB')
    .WithStoredProcName('sp_insert_log')
    .WithMinimumLevel(TLogType.Warning)     // DB only gets Warning+
    .Done
  .Build;

Scenarios and edge cases

Per-appender minimumLevel is independent of the root minimumLevel. The root gate decides whether a message is even queued; each appender’s own gate decides whether it writes the message out. A message below the root minimumLevel never reaches any appender; a message that passes the root gate but is below one appender’s minimumLevel is dropped at that appender, while the other appenders still receive it.

{
  "minimumLevel": "Debug",
  "appenders": [
    { "type": "Console",                    "minimumLevel": "Debug"   },
    { "type": "File", "logsFolder": "logs", "minimumLevel": "Info"    },
    { "type": "Webhook", "url": "https://ops/alerts",
                                              "minimumLevel": "Error"   }
  ]
}

Result: dev sees everything on the console, the log file keeps Info+, the webhook only fires on errors.

Empty appenders array is allowed - you get a “black hole” logger useful for integration tests:

{ "configVersion": 1, "appenders": [] }

Effectively-disabled logging is just a global gate high enough that nothing ever passes:

{ "configVersion": 1, "minimumLevel": "Fatal", "appenders": [] }

Or, in code, flip the runtime property on an already-built writer: Log.MinimumLevel := TLogType.Fatal;

Pluggable appenders (ExeWatch, WindowsEventLog, ElasticSearch, and any third-party appender following the same convention) need their unit in uses. Forgetting it produces a precise error (see below).

Optional JSON source. Embed a default JSON in resources and override with an on-disk file if present:

uses System.IOUtils;

if TFile.Exists('loggerpro.json') then
  Log := LoggerProFromJSONFile('loggerpro.json')
else
  Log := LoggerProFromJSONString(LoadDefaultConfigFromResources);

Error diagnostics

The parser is strict by design - misconfigurations fail loudly instead of silently half-working. All errors surface as ELoggerProConfigError (declared in LoggerPro.pas, so a single uses LoggerPro; is enough to catch it).

Unknown field (typo catch):

appenders[0] (type=Console): unknown field "colour".
Valid fields: minimumLevel, colors, colorScheme, prefix, utf8Output.

Unknown appender type (missing uses):

appenders[1]: unknown type "ExeWatch".
Currently registered types: console, elasticsearch, file, filebysource,
htmlfile, jsonlfile, memory, outputdebugstring, simpleconsole,
timerotatingfile, udpsyslog, webhook.
If you want "ExeWatch", make sure the unit that registers it is in your
`uses` clause (typically LoggerPro.ExeWatchAppender) - optional
appenders self-register from their initialization section, so simply
including the unit is enough. For custom factories use
TLoggerProConfig.RegisterAppenderType.

Schema version too new:

configVersion 2 is newer than supported (max 1). Update LoggerPro.

Invalid enum value:

appenders[0] (type=Console): invalid colorScheme "NEON".
Valid values: Default, Monochrome, GinBadge, GinMinimal, GinVibrant,
              Midnight, Nord, Matrix, Amber, Ocean, Cyberpunk.

Missing file or malformed JSON also raise ELoggerProConfigError, with the file path and the parser’s diagnostic respectively.

Graceful fallback

For first-run or misconfigured-deployment scenarios, catch the error and build a defensive default in code so the app keeps logging instead of crashing:

uses LoggerPro, LoggerPro.Builder;

function BuildLoggerSafe(const aConfigPath: string): ILogWriter;
begin
  try
    Result := LoggerProFromJSONFile(aConfigPath);
  except
    on E: ELoggerProConfigError do
    begin
      Result := LoggerProBuilder
        .WriteToConsole.Done
        .WithMinimumLevel(TLogType.Info)
        .Build;
      Result.Warn(
        Format('Config fallback: %s', [E.Message]),
        'CONFIG');
    end;
  end;
end;

The warning lands in the fallback logger itself, so operators notice the problem the next time they tail the output.

Custom appender types

LoggerPro.Config is a type registry; your own appenders can plug in the same way the built-ins do:

uses LoggerPro, LoggerPro.Config;

procedure MyCustomFactory(const aBuilder: ILoggerProBuilder;
  const aConfig: TJSONObject);
begin
  aBuilder.WriteToAppender(TMyCustomAppender.Create(
    aConfig.GetValue('endpoint').Value));
end;

initialization
  TLoggerProConfig.RegisterAppenderType(
    'MyCustom',
    MyCustomFactory,
    ['minimumLevel', 'endpoint']);   // closed set of allowed fields

From then on { "type": "MyCustom", "endpoint": "..." } just works, and typos in field names trigger the same strict diagnostic as the built-ins.

Samples: samples/240_config_simple, 241_config_advanced, 242_config_fallback.

Pluggable Appenders (Auto-register Pattern)

Appenders that target external systems - ExeWatch, WindowsEventLog, ElasticSearch - stay OUT of the core runtime package. That keeps the core dependency-free, and keeps optional SDKs (like the ExeWatch client) from being required by every LoggerPro user.

How it works: each optional appender unit registers its own JSON factory from its initialization section. Adding the unit to the uses clause of any module in your project is enough to enable its "type" value in loggerpro.json.

// main.dpr
uses
  LoggerPro,
  LoggerPro.ExeWatchAppender,          // enables "type": "ExeWatch"
  LoggerPro.WindowsEventLogAppender,   // enables "type": "WindowsEventLog"
  LoggerPro.ElasticSearchAppender;     // enables "type": "ElasticSearch"
begin
  Log := LoggerProFromJSONFile('loggerpro.json');
end.

If a JSON file references an appender whose unit isn’t in uses, the error message spells out exactly which unit to add.

LogFmt Output (Structured, grep-friendly)

LogFmt is a line-oriented structured logging format popularized by Heroku and Brandur Leach. Each log line is a sequence of key=value pairs separated by single spaces. Values are bare when safe, otherwise double-quoted with \" and \\ escapes. It’s the sweet spot between human-readable text logs and machine-parseable JSON.

Example output

time=2026-04-18T12:30:45.123Z threadid=7932 type=INFO msg="order placed" tag=ORDERS order_id=42 amount=99.95 paid=true

Enable it

Plug the TLogItemRendererLogFmt renderer into any appender via the Builder:

uses
  LoggerPro, LoggerPro.Builder, LoggerPro.Renderers;

Log := LoggerProBuilder
  .WriteToConsole
    .WithRenderer(TLogItemRendererLogFmt.Create)
    .Done
  .WriteToFile
    .WithRenderer(TLogItemRendererLogFmt.Create)
    .Done
  .Build;

Log.Info('order placed', 'ORDERS', [
  LogParam.I('order_id', 42),
  LogParam.F('amount', 99.95),
  LogParam.B('paid', True)
]);

Or set it globally as the default renderer:

LoggerPro.Renderers.gDefaultLogItemRenderer := TLogItemRendererLogFmt;

Why LogFmt

  • Readable in terminals: one-line records, no trailing noise.
  • Trivially greppable: grep 'order_id=42' just works.
  • Streaming-friendly: every line is self-contained - no multi-line JSON objects to reassemble from container stdout.
  • Cheap to produce: no escape-heavy JSON serializer in the hot path.
  • Tooling: parsed natively by humanlog, lnav, Grafana Loki, Vector, Fluent Bit, Promtail, Filebeat.

Spec conformance

LoggerPro’s LogFmt renderer:

  • Quotes values containing space, ", =, \, or control characters.
  • Escapes " as \", \ as \\, \n \r \t as literal escape sequences.
  • Non-printable control bytes become \uNNNN.
  • Sanitizes context keys to [a-zA-Z0-9_.-] (invalid chars become _).
  • Floats always use . as decimal separator (locale-independent).
  • Booleans render as bare true / false.

This means output is safe to stream one-line-at-a-time through grep, awk, jq -R, Loki’s logfmt parser, etc., without corruption.

Querying LogFmt Logs on Windows

1. ripgrep (rg) - universal filter

Install:

# Option A: winget (Windows 10/11)
winget install BurntSushi.ripgrep.MSVC

# Option B: Scoop
scoop install ripgrep

# Option C: download ripgrep-*.zip from GitHub releases, unzip, put rg.exe on PATH

Usage:

# All errors
rg "type=ERROR" logs\myapp*.log

# One specific order, any log level
rg "order_id=42\b" logs\myapp*.log

# Errors for a given user in the last file only
rg "type=ERROR.*user_id=7" (Get-ChildItem logs\myapp*.log | Sort LastWriteTime -Desc | Select -First 1).FullName

# Count errors per tag
rg -o "tag=\S+" logs\myapp*.log | Group-Object | Sort Count -Desc

2. humanlog - logfmt-aware pretty-printer & filter

humanlog is a Go binary that understands logfmt (and JSON) natively: it colorizes levels, aligns columns, parses timestamps, and lets you filter with a mini-expression language. Much nicer than rg when you’re reading logs, not just grepping them.

Install:

# Download humanlog_<version>_windows_amd64.zip from GitHub releases, unzip, put
# humanlog.exe on PATH. Or via Scoop:
scoop bucket add extras
scoop install humanlog

Usage:

# Pretty-print a static log file
Get-Content logs\myapp.20260418.log | humanlog

# Pipe directly from your Delphi app's stdout (console appender + logfmt)
.\MyApp.exe | humanlog

# Filter: only errors (mini-expression language)
Get-Content logs\*.log | humanlog --skip "type!=ERROR"

# Keep only matching records
Get-Content logs\*.log | humanlog --keep "tag=ORDERS and type=ERROR"

# Hide noisy context keys
Get-Content logs\*.log | humanlog --ignore "threadid,time"

Live tail (tail -f style)

PowerShell’s Get-Content -Wait is the building block for live tailing on Windows:

Get-Content logs\myapp.20260418.log -Wait -Tail 20

Note that Get-Content -Wait watches the original file handle and won’t follow LoggerPro’s daily/size rotation. For advanced techniques (re-tail loops, piping process stdout, encoding gotchas), see the blog.

Zero-install fallback: Select-String

If you can’t install anything on a production box, Select-String is built into every Windows install:

# Errors for tag ORDERS
Select-String -Path logs\myapp*.log -Pattern "type=ERROR.*tag=ORDERS"

# Context block around a match (2 lines before/after)
Select-String -Path logs\*.log -Pattern "order_id=42\b" -Context 2,2

Graduating beyond grep-style tools

Once you have many files, multiple hosts, or >GB/day of logs, stop grepping and start shipping:

  • Grafana Loki + Promtail - LogQL with native | logfmt parser:

    {app="myapp"} | logfmt | type="ERROR" | order_id="42"
    
  • Vector (single .exe, config-driven) - parse with the logfmt codec, ship to Elastic, Clickhouse, Datadog, S3, etc.

  • Fluent Bit (single .exe) - similar, lightweight.

LogFmt vs JSONL - when to pick which

LoggerPro also supports JSONL (one JSON object per line) via TLoggerProJSONLFileAppender. Rule of thumb:

  • LogFmt → dev machines, container stdout, greppable prod logs, low/medium volume, flat context.
  • JSONL → nested structures, analytics pipelines, Elastic/Datadog/ OpenObserve ingest, high volume, strict schema.

Both renderers share the same LogParam.S/I/F/B/D context API, so you can switch between them without changing application code.

FileBySource Appender

New in 2.1. Organizes logs into per-source subfolders - perfect for multi-tenant applications where each client, device, or account gets its own log stream. The source key is pulled from the log context.

uses
  LoggerPro.Builder;

Log := LoggerProBuilder
  .WriteToFileBySource
    .WithLogsFolder('logs')
    .WithMaxFileSizeInKB(5000)
    .WithRetainDays(30)
    .WithDefaultSource('default')
    .Done
  .Build;

// Source comes from the context
Log.Info('Order placed', 'ORDERS', [
  LogParam.S('source', 'ClientA'),
  LogParam.I('order_id', 42)
]);
// → logs/ClientA/ClientA.ORDERS.20260418.00.log

Rotation is day + size: a new file is opened on day change, and when the current file exceeds WithMaxFileSizeInKB. Files older than WithRetainDays are deleted automatically on startup and at each day change.

HTML Live Log Viewer

New in 2.1. A self-contained HTML log file you can open in any browser - no server, no external assets, one .html per rotation. Useful for handing a compact troubleshooting artifact to users, QA, or support.

uses LoggerPro, LoggerPro.Builder;

Log := LoggerProBuilder
  .WriteToHTMLFile
    .WithLogsFolder('logs')
    .WithFileBaseName('myapp')
    .WithTitle('MyApp - Operations Log')
    .WithMaxFileSizeInKB(1000)
    .WithMaxBackupFiles(10)
    .Done
  .Build;

The generated page includes:

  • Sticky filter bar - per-level checkboxes and a full-text search box.
  • Row coloring - WARNING / ERROR / FATAL rows carry a soft background tint so anomalies jump out while scrolling.
  • Context rendering - structured LogParam context is shown as key=value pairs inside a secondary row, hidden or shown by the same filter controls.
  • CSV / JSON export - download currently-visible rows.
  • Live tail - while the file is still being written, the page auto-reloads every 3 seconds and always scrolls to the bottom.
  • LIVE / FINALIZED awareness - the page knows whether the log file is still being written or has been closed, and stops the auto-reload as soon as it is finalized.

Sample: samples/230_html_file_appender. Also exposed to the JSON config as "type": "HTMLFile".

UTF-8 Console Output (Docker / Windows)

By default, Delphi’s Writeln converts to the system locale encoding, which mangles non-ASCII characters in Docker containers (POSIX/C locale) and on Windows consoles (CP 437/1252). Enable WithUTF8Output to write UTF-8 bytes directly to stdout:

Log := LoggerProBuilder
  .WriteToConsole
    .WithUTF8Output
    .Done
  .Build;

// Also available for the plain (no-color) console appender
Log := LoggerProBuilder
  .WriteToSimpleConsole
    .WithUTF8Output
    .Done
  .Build;

This is especially useful when running Delphi applications in Docker containers or when logging Unicode text (CJK, Cyrillic, emoji, etc.) on Windows.

DLL-Safe Initialization

LoggerPro 2.1 works correctly inside DLLs loaded via LoadLibrary or P/Invoke. The logger-thread initialization detects the DLL context and avoids the Windows Loader Lock deadlock that affected earlier versions.

library MyPlugin;

uses
  LoggerPro, LoggerPro.Builder;

var
  GLog: ILogWriter;

begin
  // Safe during DLL_PROCESS_ATTACH
  GLog := LoggerProBuilder
    .WriteToFile.Done
    .Build;
  GLog.Info('DLL loaded successfully');
end.

Note: Call GLog.Shutdown before the DLL is unloaded if you need to ensure queued messages are flushed.

Contextual Logging

LoggerPro’s contextual logging features let you enrich every log call with structured key-value pairs without repeating yourself.

WithProperty (ad-hoc context)

Log.WithProperty('order_id', 12345)
   .WithProperty('amount', 99.99)
   .Info('Order processed', 'orders');

WithDefaultTag (sub-logger)

var
  OrderLog: ILogWriter;
begin
  OrderLog := Log.WithDefaultTag('ORDERS');
  OrderLog.Info('Processing started');   // tag = 'ORDERS'
  OrderLog.Warn('Stock low');            // tag = 'ORDERS'
end;

WithDefaultContext (persistent context)

var
  RequestLog: ILogWriter;
begin
  RequestLog := Log.WithDefaultContext([
    LogParam.S('request_id', 'req-123'),
    LogParam.S('client_ip', '192.168.1.100')
  ]);

  RequestLog.Info('Request received');    // includes request_id, client_ip
  RequestLog.Info('Response sent');       // includes request_id, client_ip
end;

Structured context inline

Log.Info('Order completed', 'ORDERS', [
  LogParam.I('order_id', 12345),
  LogParam.S('customer', 'John Doe'),
  LogParam.F('total', 299.99),
  LogParam.B('paid', True)
]);

Exception Logging

try
  // Code that may raise
except
  on E: Exception do
  begin
    Log.LogException(E);
    Log.LogException(E, 'Database query failed');
    Log.LogException(E, 'Query failed', 'db');
  end;
end;

Stack trace formatter

Integrate with JCL, madExcept, EurekaLog, or any stack trace library:

Log := LoggerProBuilder
  .WithStackTraceFormatter(
    function(E: Exception): string
    begin
      Result := JclLastExceptStackListToString;
    end)
  .WriteToFile.Done
  .Build;

Minimum Level Filtering

Filter messages globally before they’re queued - zero overhead for filtered messages:

Log := LoggerProBuilder
  .WithMinimumLevel(TLogType.Warning)  // Debug and Info filtered
  .WriteToFile.Done
  .Build;

Log.Debug('Not logged');   // Filtered - no TLogItem created
Log.Info('Not logged');    // Filtered - no TLogItem created
Log.Warn('Logged');        // OK

New in 2.1: the gate is a first-class property on ILogWriter, so you can change it at runtime without any cast:

Log.MinimumLevel := TLogType.Fatal;  // effectively mute

// Later
Log.MinimumLevel := TLogType.Debug;  // re-enable everything

ElasticSearch with Authentication

New in 2.1. The ElasticSearch appender now supports all three common auth mechanisms:

// Basic Auth
Log := LoggerProBuilder
  .WriteToElasticSearch
    .WithURL('https://elastic.example.com:9200/logs/_doc')
    .WithBasicAuth('username', 'password')
    .Done
  .Build;

// API Key
Log := LoggerProBuilder
  .WriteToElasticSearch
    .WithHost('https://elastic.example.com')
    .WithPort(9200)
    .WithIndex('app-logs')
    .WithAPIKey('your-api-key-here')
    .Done
  .Build;

// Bearer Token (JWT, OAuth2)
Log := LoggerProBuilder
  .WriteToElasticSearch
    .WithURL('https://elastic.example.com:9200/logs/_doc')
    .WithBearerToken('your-jwt-token')
    .Done
  .Build;

ExeWatch Integration (Cloud Observability)

What is ExeWatch. ExeWatch is a cloud observability service for desktop and backend applications: it collects logs, exceptions, breadcrumbs, user identity, device info and custom metrics, and gives you a dashboard to investigate incidents in production without shipping log files around.

Why a dedicated appender. ExeWatch ships its own Delphi SDK (ExeWatchSDKv1.pas). The LoggerPro bridge - unit LoggerPro.ExeWatchAppender - turns every Log.Info / Warn / Error / Fatal / LogException call into the corresponding SDK call, so you keep the LoggerPro API and transparently gain the ExeWatch pipeline. It is not part of the LoggerPro runtime package, so projects that do not use ExeWatch pay nothing.

Enabling the bridge. Add the SDK folder to your project’s search path (the sample uses C:\dev\exewatchsamples\DelphiCommons) and pull in the bridge unit:

uses
  LoggerPro,
  LoggerPro.Builder,
  LoggerPro.ExeWatchAppender;

The SDK is initialized automatically on appender Setup - you do NOT need to call InitializeExeWatch yourself, and the appender won’t interfere with an SDK already initialized by other code.

Four ways to wire it up

1. Fluent builder - everything in code; fine when the API key, customer ID and version are literals or come from simple globals. Chain like any built-in appender:

Log := WithExeWatch(LoggerProBuilder)
    .WithAPIKey('ew_win_xxxxxx')
    .WithCustomerId('Acme Corp')
    .WithAppVersion('2.3.1')
    .WithAnonymizeDeviceId(False)  // True = per-install ID (GDPR-friendly)
    .Done
  .WriteToConsole.WithColors.Done  // mirror to the console too
  .Build;

2. Fluent chain on a JSON base - pair a loggerpro.json with code-side ExeWatch config. The base file carries the console/file/HTML setup; the SDK secrets stay in code where they can be sourced from a vault, environment variable, or license server:

uses
  LoggerPro, LoggerPro.Builder, LoggerPro.ExeWatchAppender;

Log := WithExeWatch(
      LoggerProBuilderFromJSONFile('loggerpro.json'))
    .WithAPIKey(LoadFromVault('ew_api_key'))
    .WithCustomerId(CurrentTenant)
    .WithAppVersion(GetFileVersion)
    .Done
  .Build;

3. Imperative (lowest friction when values come from config objects):

Log := LoggerProBuilder
  .WriteToAppender(NewExeWatchAppender(
    'ew_win_xxxxxx', 'Acme Corp', '2.3.1'))
  .Build;

4. Pure JSON configuration (thanks to the auto-register pattern, no extra registration call needed):

{
  "configVersion": 1,
  "appenders": [
    { "type": "Console" },
    {
      "type": "ExeWatch",
      "apiKey": "ew_win_xxxxxx",
      "customerId": "Acme Corp",
      "appVersion": "2.3.1",
      "anonymizeDeviceId": false
    }
  ]
}
uses LoggerPro, LoggerPro.ExeWatchAppender;
Log := LoggerProFromJSONFile('loggerpro.json');

Level mapping

TLogType ExeWatch SDK severity
Debug EW.Debug
Info EW.Info
Warning EW.Warning
Error EW.Error
Fatal EW.Fatal

The log Tag is forwarded as the SDK’s tag. Structured LogParam context is collapsed into the formatted message body - if you want the richer SDK features (breadcrumbs, timings, user identity, gauges), call EW.AddBreadcrumb / EW.SetUser / EW.StartTiming directly alongside LoggerPro. The two layers coexist freely.

Shutdown

Log.Shutdown drains the LoggerPro queue and then calls EW.Flush, so any event queued client-side is shipped before the process exits. The SDK’s own finalization handles the rest.

Sample: samples/260_exewatch_appender.

UDP Syslog (RFC 5424) with Local Time

New WithUseLocalTime option - RFC 5424 mandates UTC, but many on-prem syslog servers expect local time.

Log := LoggerProBuilder
  .WriteToUDPSyslog
    .WithHost('syslog.example.com')
    .WithPort(514)
    .WithApplication('MyApp')
    .WithUseLocalTime(True)   // default: False (UTC)
    .Done
  .Build;

Getting the Current Log File Name

New in 2.1. Useful for uploading, emailing, or attaching a log file on demand.

uses LoggerPro, LoggerPro.Builder, LoggerPro.FileAppender;

var
  lAppender: TLoggerProSimpleFileAppender;
  Log: ILogWriter;
begin
  lAppender := TLoggerProSimpleFileAppender.Create;
  Log := LoggerProBuilder
    .WriteToAppender(lAppender)
    .Build;

  Log.Info('Message');
  UploadLogFile(lAppender.GetCurrentLogFileName);
end;

For TLoggerProFileAppender (one file per tag):

var
  lAppender: TLoggerProFileAppender;
begin
  lAppender := TLoggerProFileAppender.Create;
  // Get file for a specific tag
  lFileName := lAppender.GetCurrentLogFileName('ORDERS');
  // Or all current log files
  lAllFiles := lAppender.GetAllCurrentLogFileNames;
end;

Shutdown

Call Shutdown in your application’s finalization to guarantee all pending logs are written:

procedure TMyApp.Finalize;
begin
  Log.Shutdown;   // Flush queue, stop thread
  Log := nil;
end;

Key behaviors:

  • Idempotent: safe to call multiple times.
  • Flushes all pending messages.
  • Terminates the logger thread.
  • After shutdown, log calls are silently ignored (Release) or assert (Debug).

Built-in Appenders

LoggerPro 2.1 ships with 20+ appenders. All are async, thread-safe, and configurable via the Builder.

File Appenders

Appender Builder Method Description
FileAppender WriteToFile One file per tag with size-based rotation. Default: 5 backups, 1 MB max. Supports time-based rotation.
SimpleFileAppender WriteToFile All logs in a single file (no tag separation). Same rotation options.
JSONLFileAppender WriteToJSONLFile One JSON object per line. Ideal for log aggregators.
TimeRotatingFileAppender WriteToTimeRotatingFile New file on each time interval (hourly/daily/weekly/monthly).
FileByFolderAppender (direct) Organizes logs in daily subfolders (Logs/20260418/app.00.log).
FileBySourceAppender (new) WriteToFileBySource Per-source subfolders with day+size rotation and day-based retention.
HTMLFileAppender (new) WriteToHTMLFile Self-contained browsable .html with filter bar, level coloring, CSV/JSON export and live tail.

Console Appenders

Appender Builder Method Description
ConsoleAppender WriteToConsole Cross-platform with colors (Windows API / ANSI). Auto-creates console for GUI apps on Windows. WithUTF8Output for Docker/Unicode.
SimpleConsoleAppender WriteToSimpleConsole Plain Writeln, no colors. Works everywhere. WithUTF8Output available.

Remote / Network Appenders

Appender Builder Method Description
ExeWatchAppender (pluggable) WithExeWatch(builder) First-class cloud observability via ExeWatch. Self-registers in JSON.
WebhookAppender (renamed) WriteToWebhook HTTP POST to REST endpoints. Timeout, custom headers, API-key auth (header or query string).
ElasticSearchAppender (pluggable) WriteToElasticSearch Sends logs to ElasticSearch 6.4+. Supports Basic Auth, API Key, Bearer Token. Self-registers in JSON.
UDPSyslogAppender WriteToUDPSyslog RFC 5424 UDP syslog. Local time or UTC.
EMailAppender (direct) SMTP (via Indy TIdSMTP).
NSQAppender (direct) Publishes logs to NSQ distributed message queue.
RedisAppender (contrib) Stores logs in Redis lists.

UI Appenders

Appender Builder Method Description
StringsAppender WriteToStrings Appends to any TStrings (TMemo.Lines, TStringList). Cross-platform.
VCLMemoAppender WriteToVCLMemo VCL TMemo. Windows only.
VCLListBoxAppender WriteToVCLListBox VCL TListBox. Windows only.
VCLListViewAppender WriteToVCLListView Multi-column VCL TListView. Windows only.

System / Debug Appenders

Appender Builder Method Description
OutputDebugStringAppender WriteToOutputDebugString Windows OutputDebugString.
WindowsEventLogAppender (pluggable) WriteToWindowsEventLog Windows Event Log (applications and Services). Self-registers in JSON.
MemoryAppender WriteToMemory Thread-safe ring buffer.
CallbackAppender WriteToCallback Invokes your callback per log item.

Database Appenders

Appender Builder Method Description
FireDACAppender WriteToFireDAC Database logging via FireDAC stored procedure.
ADOAppender (direct) Database logging via ADO stored procedure.

Filter / Proxy

Appender Builder Method Description
FilterProxy WriteToFilteredAppender Wrap any appender with a custom filter function.

Appenders marked (direct) are used via BuildLogWriter([appender]) or .WriteToAppender(appender).

Windows Event Log Integration

Regular applications

Log := LoggerProBuilder
  .WriteToWindowsEventLog
    .WithSourceName('MyApplication')
    .WithMinimumLevel(TLogType.Warning)
    .Done
  .Build;

Windows Services

procedure TMyService.ServiceCreate(Sender: TObject);
begin
  FLog := LoggerProBuilder
    .WriteToWindowsEventLogForService(Self)
      .Done
    .WriteToFile
      .WithLogsFolder('C:\ProgramData\MyService\Logs')
      .Done
    .Build;
end;

Log Levels

Log.Debug('Detailed debug information');   // TLogType.Debug
Log.Info('General information');           // TLogType.Info
Log.Warn('Warning conditions');            // TLogType.Warning
Log.Error('Error conditions');             // TLogType.Error
Log.Fatal('Critical failures');            // TLogType.Fatal

Delphi Version Compatibility

LoggerPro Version Minimum Delphi Notes
2.1.x (current) Delphi 10.2 Tokyo Full compatibility from 10.2+
2.0.x Delphi 10.3 Rio Previous guide
1.x (legacy) Delphi 10 Seattle Legacy guide

Tested on: Delphi 13 Florence, 12 Athens, 11 Alexandria, 10.4 Sydney, 10.3 Rio.

Platforms: Windows (32/64-bit), Linux, macOS, Android, iOS.

Installation

Download LoggerPro 2.1.0

  1. Extract the ZIP to a folder (e.g. C:\Libraries\LoggerPro).
  2. In Delphi: Tools > Options > Language > Delphi > Library.
  3. Add to Library Path for your target platform:
    • C:\Libraries\LoggerPro (main units)
    • C:\Libraries\LoggerPro\contrib (optional: Redis and Email appenders)
  4. uses LoggerPro, LoggerPro.Builder; in your code.

All releases: github.com/danieleteti/loggerpro/releases

Option 2: BOSS

BOSS is an open-source package manager for Delphi and Lazarus.

boss init        # skip if you already have a boss.json
boss install github.com/danieleteti/loggerpro

BOSS downloads LoggerPro into modules/ and updates the project search path. Commit boss.json and boss-lock.json; add modules/ to .gitignore.

Option 3: Clone Repository (contributors only)

git clone https://github.com/danieleteti/loggerpro.git

Add the root folder and contrib to your Library Path. Use this only if you want to test unreleased features or contribute; the master branch may be unstable.


Project Description
DMVCFramework REST API framework with built-in LoggerPro integration
DelphiRedisClient Redis client for TLoggerProRedisAppender

FAQ

What is new in LoggerPro 2.1?

JSON configuration (LoggerProFromJSONFile), a self-contained HTML log viewer, first-class ExeWatch cloud observability integration, a spec-compliant LogFmt renderer, a FileBySource appender for per-tenant logs, UTF-8 console output for Docker, DLL-safe initialization, ElasticSearch authentication (Basic/APIKey/Bearer), UDP Syslog local time, and a GetCurrentLogFileName API on file appenders.

Is LoggerPro 2.1 backward compatible with 2.0?

Yes, it’s a drop-in upgrade. No code changes needed. All 2.0 Builder methods and all 1.x BuildLogWriter patterns continue to work.

How do I emit LogFmt output?

Plug the renderer: .WriteToConsole.WithRenderer(TLogItemRendererLogFmt.Create).Done. See the LogFmt Output section.

How do I query LogFmt logs on Windows?

Install ripgrep (single .exe, winget install BurntSushi.ripgrep.MSVC) for fast regex filtering, or humanlog for colorized pretty-printing. Combine with Get-Content -Wait for live tailing. See the Querying LogFmt Logs on Windows section.

Can LoggerPro be used in a DLL?

Yes, 2.1 detects DLL context and avoids the Windows Loader Lock deadlock. Call Shutdown before DLL unload.

How do I get correct UTF-8 output in Docker?

Use .WriteToConsole.WithUTF8Output.Done. Writes UTF-8 bytes directly to stdout, bypassing Delphi’s locale conversion.

How do I add context to logs?

Use Log.WithProperty('key', value).Info('msg') for ad-hoc context, or Log.WithDefaultContext([...]) for persistent context. Or pass structured context inline: Log.Info('msg', 'tag', [LogParam.I('id', 42)]).

Should I call Shutdown?

Yes, call Log.Shutdown in your application’s finalization. It’s idempotent and ensures pending logs are written before exit. Especially important inside DLLs, Windows Services, and containerized apps with short lifetimes.

Does LoggerPro support async logging?

Yes - every appender runs on its own background thread. Logging calls return immediately; a logger thread dispatches items to each appender’s queue.

What appenders have native Builder support?

File, JSONL File, Time Rotating File, FileBySource (new), Console, Simple Console, OutputDebugString, HTTP, ElasticSearch, UDP Syslog, FireDAC, Memory, Callback, Strings, VCL (Memo, ListBox, ListView), Windows Event Log. Redis, Email, NSQ are added via .WriteToAppender().

Can I filter logs by level at runtime?

Yes, either globally with .WithMinimumLevel() at build time, or dynamically via Log.MinimumLevel := TLogType.Fatal;.

How do I log to Windows Event Log from a Windows Service?

FLog := LoggerProBuilder
  .WriteToWindowsEventLogForService(Self).Done
  .Build;

Pass the TService instance - LoggerPro uses TService.LogMessage for correct Service-mode logging.

What Delphi versions are supported?

2.1 supports Delphi 10.2 Tokyo and later (up to Delphi 13 Florence). Linux, macOS, Android, and iOS are fully supported.


LoggerPro 2.1 is the modern async logging framework for Delphi / Object Pascal - Builder pattern API, structured context logging, spec-compliant LogFmt renderer for Grafana Loki, per-tenant FileBySource appender, UTF-8 console output for Docker, DLL-safe initialization, ElasticSearch / UDP Syslog / Windows Event Log appenders. The Delphi equivalent of Serilog. Thread-safe, cross-platform (Windows / Linux / macOS / Android / iOS), Apache 2.0 licensed, actively maintained since 2010.

Comments

comments powered by Disqus