LoggerPro 2.1 — Async Logging Framework for Delphi
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 (
LoggerProFromJSONFileorLoggerProFromJSONString), 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) |
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:
WriteToHTTPwas renamed toWriteToWebhook(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.Infois Info,Log.Erroris Error, and so on. NeitherminimumLevelnordefaultMinimumLevelchanges that; they only decide who sees what. RootminimumLevelis a pre-queue filter (free:TLogItemisn’t even constructed for messages below it). Per-appenderminimumLevelis an emit gate (evaluated on the appender’s worker thread, deciding whether THIS appender writes the message).defaultMinimumLevelat 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:
- Load the JSON-describable part with
LoggerProBuilderFromJSONFile. - Chain the extras on the returned builder.
- Call
.Buildyourself.
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\tas 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
| logfmtparser:{app="myapp"} | logfmt | type="ERROR" | order_id="42" -
Vector (single
.exe, config-driven) - parse with thelogfmtcodec, 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
LogParamcontext 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.Shutdownbefore 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
Option 1: Download Release (recommended)
- Extract the ZIP to a folder (e.g.
C:\Libraries\LoggerPro). - In Delphi: Tools > Options > Language > Delphi > Library.
- Add to Library Path for your target platform:
C:\Libraries\LoggerPro(main units)C:\Libraries\LoggerPro\contrib(optional: Redis and Email appenders)
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.
Related Projects
| Project | Description |
|---|---|
| DMVCFramework | REST API framework with built-in LoggerPro integration |
| DelphiRedisClient | Redis client for TLoggerProRedisAppender |
Links
- GitHub: github.com/danieleteti/loggerpro
- LoggerPro 2.0 docs: /loggerpro_2_0/
- Legacy 1.x guide: /loggerpro_1_3/
- Support: Facebook Group
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