Skip to main content

Logging — Usage Guide

🌍 Language: English | Español


Table of Contents

  1. Architecture
  2. ILogger API
  3. Log Levels
  4. Structured Logging
  5. Scopes
  6. ILoggerFactory
  7. Providers
  8. Dependency Injection Integration
  9. Creating a Custom Provider
  10. TLogEntry Reference

Architecture

Your code
   │
   ▼
ILogger           ← per-category façade
   │
   ▼
ILoggerFactory    ← TMultiProviderLoggerFactory
   │  (fan-out)
   ├─► ILoggerProvider A  → ILogger A
   ├─► ILoggerProvider B  → ILogger B
   └─► ILoggerProvider C  → ILogger C

A single ILogger delegates every write to all registered providers. Each provider can write to files, consoles, sinks, or remote endpoints independently.


ILogger API

ILogger = interface(IInvokable)
  // Core write
  procedure Log(const Level: TLogLevel; const Msg: string; const Args: TArray<TValue> = nil); overload;
  procedure Log(const Level: TLogLevel; const Ex: Exception; const Msg: string; const Args: TArray<TValue> = nil); overload;

  // Convenience wrappers
  procedure LogTrace      (const Msg: string; const Args: TArray<TValue> = nil); overload;
  procedure LogDebug      (const Msg: string; const Args: TArray<TValue> = nil); overload;
  procedure LogInformation(const Msg: string; const Args: TArray<TValue> = nil); overload;
  procedure LogWarning    (const Msg: string; const Args: TArray<TValue> = nil); overload;
  procedure LogError      (const Msg: string; const Args: TArray<TValue> = nil); overload;
  procedure LogError      (const Ex: Exception; const Msg: string = ''; const Args: TArray<TValue> = nil); overload;
  procedure LogCritical   (const Msg: string; const Args: TArray<TValue> = nil); overload;
  procedure LogCritical   (const Ex: Exception; const Msg: string; const Args: TArray<TValue> = nil); overload;

  // Scopes
  function BeginScope(const Msg: string; const Args: TArray<TValue>;
                      const ScopedProc: TProc = nil): ILogScopeVoid; overload;
end;

Log Levels

TLogLevel = (Trace, Debug, Information, Warning, Error, Critical, None);
Value Constant Typical use
0 Trace Very fine-grained, off in production
1 Debug Diagnostic during development
2 Information Normal operational milestones
3 Warning Unexpected but recoverable
4 Error Current operation failed
5 Critical Requires immediate attention
6 None Disable logging entirely

Structured Logging

Named placeholders in the message template are matched positionally to Args:

Logger.LogInformation('User {UserId} logged in from {IP}', [UserId, RemoteIP]);
Logger.LogWarning('Retry {Attempt} of {Max} for {Op}', [3, 5, 'SendEmail']);

Internally, TLogState holds the template string and the argument array. Providers receive the TLogState record and may format them independently (flat string, JSON, etc.).

TLogState record

TLogState = record
  property Template: string;                             // '{UserId} logged in from {IP}'
  property Arguments: TArray<TValue>;                    // in the order they were passed
  property KeyValuePairs: TArray<TPair<string, TValue>>; // ('UserId', ...), ('IP', ...)
  property Formatter: TLogFormatter;                     // what renders the final line
  function TryGetValue(const Key: string; out Value: TValue): Boolean;
end;

Scopes

A scope attaches ambient context to all log entries emitted inside a block:

Logger.BeginScope('Processing order {OrderId}', [OrderId],
  procedure
  begin
    Logger.LogInformation('Checking stock', []);
    Logger.LogInformation('Payment authorised', []);
    // Both entries carry the scope, apart from their message
  end);

Scopes are stack-based — nested scope data accumulates. On exit the scope is popped automatically.

The scope reaches the entry as TLogEntry.Scope, apart from its message: TLogEntry.Message is the formatted message and nothing else. Scope holds the open scopes from the innermost to the outermost, each prefixed with |, and is empty outside any scope. A provider decides whether to write it and where. With NNLog the token is ${scope}: ${message}${scope} puts the scopes right after the message, and a layout without ${scope} does not write them.


ILoggerFactory

ILoggerFactory = interface(IInvokable)
  function CreateLogger(const Category: string): ILogger; overload;
  function CreateLogger(const AClass: TClass): ILogger; overload;
  function CreateLogger(const AType: PTypeInfo): ILogger; overload;
end;

Generic helper

On the concrete factory, not on the contract: a Delphi interface cannot declare a parameterized method. Through ILoggerFactory the same result is CreateLogger(TMyService).

var Factory := TMultiProviderLoggerFactory.Create;
var Logger  := Factory.CreateLogger<TMyService>;

Direct instantiation (no DI)

var Factory := TMultiProviderLoggerFactory.Create;
Factory.AddProvider(MyProvider);

Providers

A provider converts TLogEntry records into output:

ILoggerProvider = interface
  function CreateLogger(const Category: string): ILogger;
end;

Built-in providers

Provider Unit Description
TNNLogProvider Daf.NNLog Rule-based targets (file, console, …)
TNullLoggerProvider Daf.Extensions.Logging Drops every entry — how to ask for silence on purpose

There is no Daf.Logging.Console and no Daf.Logging.Debug. NNLog brings Console, Debug, File_ and UDP, with filtering rules and layouts. And a startup failure reaches somebody without any of that: the bootstrap channel writes to the error output and to the system debug output with nothing configured.

Adding a provider

Factory.AddProvider(TNNLogProvider.Create(Targets, Rules));

Dependency Injection Integration

uses Daf.Extensions.Logging, Daf.Logging.Builder, Daf.NNLog;

// While composing startup, on the builder's Services:
AddLogging(Builder.Services,
  procedure(Log: ILoggingBuilder)
  begin
    Log.AddProvider(TNNLogProvider.Create(Targets, Rules));
  end);

AddLogging registers: - ILoggerFactory as singleton - Internally calls Builder.Build to wire providers before the host starts

Composing with no destination is an error

A pipeline with no provider accepts every entry and drops it, which is indistinguishable from one that works — until someone debugs an incident reading records that were never written. So AddLogging refuses it:

AddLogging(Services,
  procedure(Builder: ILoggingBuilder)
  begin
    // the AddProvider that is not there
  end);
// ELoggingCompositionError

The check runs once, where the pipeline is composed — inside the host builder’s Build, before a single hosted service exists and before the first entry, and never on the write path.

Silence on purpose is still fine. It just has to be said:

AddLogging(Services,
  procedure(Builder: ILoggingBuilder)
  begin
    Builder.AddNullProvider;
  end);
What you write What you get
No AddLogging at all No ILoggerFactory in the container. Startup reports still come out through the bootstrap channel
AddLogging with AddNullProvider A factory that drops every entry — declared silence
AddLogging with no provider ELoggingCompositionError while the host is being built

The middle row is the one you want when your own code resolves ILoggerFactory or takes an injected ILogger: without AddLogging there is no factory to inject. If your application simply does not log, not calling AddLogging is the cheaper route and there is nothing to declare.

Resolve in services:

constructor TMyService.Create(Factory: ILoggerFactory);
begin
  FLogger := Factory.CreateLogger<TMyService>;
end;

Creating a Custom Provider

  1. Implement ILoggerProvider:
type
  TMyProvider = class(TInterfacedObject, ILoggerProvider)
    function CreateLogger(const Category: string): ILogger;
  end;

function TMyProvider.CreateLogger(const Category: string): ILogger;
begin
  Result := TMyLogger.Create(Category);
end;
  1. Implement ILogger:
type
  TMyLogger = class(TInterfacedObject, ILogger)
    procedure Log(const Level: TLogLevel; const Msg: string;
                  const Args: TArray<TValue> = nil); overload;
    // … other overloads delegate to this one
  end;
  1. Register:
Factory.AddProvider(TMyProvider.Create);
// or via Builder inside AddLogging(...)

TLogEntry Reference

TLogEntry = record
  Level     : TLogLevel;
  Category  : string;
  EventId   : TEventId;
  Exception : Exception;      // nil when no exception
  Scope     : string;         // rendered scope chain
  State     : TLogState;      // template + values
  Formatter : TLogFormatter;  // what turns State into the line
end;

Every field is populated before Write(Entry) is called on a TTarget. Message is not among them: it is a read-only property, and the first read runs Formatter over State and keeps the result — so a target that renders its own layout out of State never pays for it. Message does not include Scope: a provider that wants the scopes writes them from Scope.