Skip to content

Logger

Strukturiertes Logging mit 20+ Handlern, fluenter Konfiguration und eingebauter Fehlerresilienz.

Einführung

Logging klingt trivial: error_log() und fertig. In der Praxis braucht eine Unternehmensanwendung aber Logs in Dateien, Alerts in Slack, Metriken in Grafana Loki, und das alles mit unterschiedlichen Schwellenwerten, strukturierten Daten und ohne dass ein fehlerhafter Handler die gesamte Anwendung blockiert.

jardisadapter/logger ist eine PSR-3 kompatible Logging-Pipeline, die genau das liefert:

  • 20+ Handler — Datei, Console, Syslog, Slack, Teams, Loki, Database, Redis, Kafka, RabbitMQ, E-Mail, Logstash, Browser Console und mehr
  • Fluenter LoggerBuilder — ein Builder konfiguriert alle Handler, das Ergebnis ist ein immutabler Logger
  • Fehlerresilienz — ein fehlerhafter Handler bricht nie die Pipeline. Die anderen Handler laufen weiter, Fehler werden an einen optionalen Error-Handler gemeldet
  • Strukturierte Log-Records — Enricher fügen Timestamp, UUID, Client-IP, Memory-Usage und beliebige Custom-Felder hinzu
  • Smart Handler — FingersCrossed (Buffer bis Error), Sampling (Volume-Reduktion) und Conditional (Routing per Callback)
  • Bounded-Context-Scoping — jeder Logger trägt einen Context-Namen, der in jedem Log-Record erscheint
  • 7 Formatter — Line, JSON, Human-Readable, Slack, Teams, Loki, Browser Console

Installation

bash
composer require jardisadapter/logger

GitHub: jardisAdapter/logger

Optionale PHP-Extensions:

ExtensionFür
ext-redisLogRedis, LogRedisMq
ext-amqpLogRabbitMq
ext-rdkafkaLogKafkaMq

Grundlegende Nutzung

Zwei Schritte: Builder konfigurieren, Logger verwenden.

php
use JardisAdapter\Logger\LoggerBuilder;
use JardisAdapter\Logger\Data\LogLevel;

$logger = (new LoggerBuilder('OrderService'))
    ->addConsole(LogLevel::DEBUG)
    ->addFile(LogLevel::INFO, '/var/log/orders.log')
    ->getLogger();

// PSR-3 API
$logger->info('Order created', ['order_id' => 4711]);
$logger->error('Payment failed', ['order_id' => 4711, 'reason' => 'timeout']);

Jeder Handler hat einen minimalen Log-Level. addFile(LogLevel::INFO, ...) ignoriert debug-Nachrichten: nur info und höher landen in der Datei. Die Console bekommt alles ab debug.

Error-Handler für resiliente Pipelines

php
$logger = (new LoggerBuilder('PaymentService'))
    ->setErrorHandler(function (\Exception $e, string $handlerId, string $level, string $message, array $context) {
        // Handler-Fehler loggen oder alerting triggern
        error_log("Logger handler {$handlerId} failed: " . $e->getMessage());
    })
    ->addSlack(LogLevel::ERROR, 'https://hooks.slack.com/...')
    ->addFile(LogLevel::DEBUG, '/var/log/app.log')
    ->getLogger();

// Wenn Slack ausfällt, läuft die Datei-Ausgabe trotzdem weiter
$logger->error('Critical failure');

Log-Level-Hierarchie

Die PSR-3 Log-Levels sind numerisch geordnet. Ein Handler mit Level warning ignoriert alles darunter:

LevelSchwereTypischer Einsatz
emergency7System unbenutzbar
alert6Sofortige Aktion nötig
critical5Kritische Fehler
error4Laufzeitfehler
warning3Ungewöhnliche Zustände
notice2Normale, aber bemerkenswerte Ereignisse
info1Informativ
debug0Debug-Informationen

Handler-Übersicht

Stream-basierte Handler

HandlerZielBuilder-Methode
LogFileDatei (Lazy Open)addFile(level, path)
LogConsoleSTDOUTaddConsole(level)
LogErrorLogSTDERRaddErrorLog(level)
LogSyslogSyslogaddSyslog(level)
LogBrowserConsoleChromeLogger HTTP-HeaderaddBrowserConsole(level)
LogNullNirgendwo (Test/Graceful Degradation)addNull(level)

Webhook-Handler

HandlerZielBuilder-Methode
LogSlackSlack Incoming WebhookaddSlack(level, url)
LogTeamsMS Teams MessageCardaddTeams(level, url)
LogLokiGrafana Loki Push APIaddLoki(level, url, staticLabels)
LogWebhookBeliebiger HTTP-EndpointaddWebhook(level, url)

Persistenz-Handler

HandlerZielBuilder-Methode
LogDatabasePDO INSERT (MySQL/PgSQL/SQLite)addDatabase(level, pdo)
LogRedisRedis SETEX mit TTLaddRedis(level, redis)
LogEmailDirekte SMTP-ZustellungaddEmail(level, to, from, ...)
LogStashLogstash TCPaddStash(level, host, port)

Message-Queue-Handler

HandlerProtokollBuilder-Methode
LogRedisMqRedis PUBLISH (Pub/Sub)addRedisMq(redis, channel)
LogRabbitMqAMQP Fanout ExchangeaddRabbitMq(connection, exchange)
LogKafkaMqKafka Topic ProduceraddKafkaMq(producer, topic)

Strukturierte Log-Records

LogData — Enricher Pipeline

Jeder Handler baut seinen Log-Record über ein LogData-Objekt. Enricher sind Callables, die erst zum Zeitpunkt des Loggings ausgewertet werden: Zero-Cost bis zur tatsächlichen Nutzung.

php
use JardisAdapter\Logger\Data\LogData;
use JardisAdapter\Logger\Enricher\LogDateTime;
use JardisAdapter\Logger\Enricher\LogUuid;
use JardisAdapter\Logger\Enricher\LogClientIp;
use JardisAdapter\Logger\Enricher\LogMemoryUsage;

$logData = (new LogData())
    ->addField('timestamp', new LogDateTime())
    ->addField('hostname', fn() => gethostname())
    ->addExtra('request_id', new LogUuid())
    ->addExtra('client_ip', new LogClientIp())
    ->addExtra('memory', new LogMemoryUsage());

addField() fügt Felder auf Root-Ebene hinzu. addExtra() fügt Felder im verschachtelten data-Abschnitt hinzu.

Resultierender Log-Record

json
{
  "context": "OrderService",
  "level": "error",
  "message": "Payment failed for order 4711",
  "timestamp": "2025-11-30 10:00:00",
  "hostname": "server-01",
  "data": {
    "order_id": 4711,
    "request_id": "a3f8b2c1-...",
    "client_ip": "192.168.1.42",
    "memory": "12.34 MB (12935168 Bytes)."
  }
}

Eingebaute Enricher

EnricherLiefert
LogDateTimeAktuelles Datum/Uhrzeit (Y-m-d H:i:s)
LogUuidUUID v4
LogClientIpClient-IP (Proxy-aware: X-Forwarded-For, HTTP_CLIENT_IP)
LogWebRequestArray mit IP, URL, Method, User-Agent, GET/POST-Daten
LogMemoryUsageAktueller Speicherverbrauch
LogMemoryPeakPeak-Speicherverbrauch

PSR-3 Message-Interpolation

php
$logger->info('Order {order_id} shipped to {city}', [
    'order_id' => 4711,
    'city' => 'Berlin',
]);
// Message: "Order 4711 shipped to Berlin"

Platzhalter im {key}-Format werden aus dem Context-Array aufgelöst. Arrays werden JSON-encodiert, Callables ausgeführt.

Custom LogData pro Handler

php
use JardisAdapter\Logger\Handler\LogFile;

$handler = new LogFile(LogLevel::DEBUG, '/var/log/app.log');
$handler->setLogData($logData);  // Custom Enricher nur für diesen Handler

Formatter

Jeder Handler hat einen Standard-Formatter, der per setFormat() oder Builder-Parameter überschrieben werden kann.

FormatterFormatStandard für
LogLineFormat{ "key": "value", ... }\nDatei, Console, STDERR
LogJsonFormatPure JSON (json_encode)Logstash, Webhooks
LogHumanFormatMehrzeilig: KEY: value\nDebug-Ausgabe
LogSlackFormatSlack Webhook Payload mit Emoji und FarbenLogSlack
LogTeamsFormatMS Teams MessageCard mit ThemeColorLogTeams
LogLokiFormatLoki Push API Streams mit Nanosekunden-TimestampsLogLoki
LogBrowserConsoleFormatChromeLogger v4.1.0 ProtokollLogBrowserConsole
php
use JardisAdapter\Logger\Formatter\LogJsonFormat;

$logger = (new LoggerBuilder('ApiService'))
    ->addFile(LogLevel::INFO, '/var/log/structured.log', format: new LogJsonFormat())
    ->getLogger();

Smart Handler

Drei spezialisierte Handler für fortgeschrittene Logging-Strategien.

FingersCrossed — Buffer bis Error

Sammelt alle Log-Nachrichten in einem Puffer. Erst wenn eine Nachricht den Aktivierungs-Level erreicht (Standard: error), wird der gesamte Puffer auf einmal geflushed. So bekommt man den vollständigen Kontext vor einem Fehler, ohne im Normalbetrieb die Festplatte zu fluten.

php
use JardisAdapter\Logger\Handler\LogFile;
use JardisAdapter\Logger\Handler\LogFingersCrossed;

$fileHandler = new LogFile(LogLevel::DEBUG, '/var/log/app.log');

$logger = (new LoggerBuilder('PaymentService'))
    ->addFingersCrossed(
        wrappedHandler: $fileHandler,
        activationLevel: LogLevel::ERROR,
        bufferSize: 200,
        stopBufferingAfterActivation: true
    )
    ->getLogger();

$logger->debug('Starting payment process');    // gepuffert
$logger->info('Validating card');              // gepuffert
$logger->error('Card declined');               // → Flush: alle 3 Nachrichten auf einmal
$logger->info('Retrying...');                  // direkt geschrieben (nach Aktivierung)
ParameterStandardBeschreibung
activationLevelerrorLevel, der den Flush auslöst
bufferSize100Max. gepufferte Nachrichten (FIFO)
stopBufferingAfterActivationtrueNach Flush direkt durchreichen

Sampling — Volume-Reduktion

Reduziert das Log-Volumen durch verschiedene Sampling-Strategien: ideal für hochfrequente Endpoints, bei denen nicht jeder Request geloggt werden muss.

php
$logger = (new LoggerBuilder('ApiService'))
    ->addSampling(
        wrappedHandler: $fileHandler,
        strategy: 'smart',
        config: [
            'alwaysLogLevels' => ['error', 'critical', 'alert', 'emergency'],
            'samplePercentage' => 10,
        ]
    )
    ->getLogger();
StrategieBeschreibung
rateMax. N Nachrichten pro Sekunde
percentageNur X% der Nachrichten durchlassen
smartErrors immer, Rest nach Prozentsatz
fingerprintDeduplizierung: gleiche Nachricht nur einmal pro Zeitfenster

Conditional — Routing per Callback

Leitet Nachrichten basierend auf Callables an unterschiedliche Handler weiter.

php
use JardisAdapter\Logger\Handler\LogConditional;

$conditional = new LogConditional([
    [fn($level, $msg, $ctx) => isset($ctx['payment_id']), $paymentFileHandler],
    [fn($level, $msg, $ctx) => isset($ctx['api_request']), $apiLokiHandler],
], fallbackHandler: $defaultFileHandler);

$logger = (new LoggerBuilder('AppService'))
    ->addHandler($conditional)
    ->getLogger();

Die erste Bedingung, die true zurückgibt, gewinnt. Trifft keine zu und kein Fallback ist gesetzt, wird die Nachricht stillschweigend verworfen.

Benannte Handler

Jeder Handler kann einen Namen erhalten und zur Laufzeit abgefragt werden:

php
$logger = (new LoggerBuilder('OrderService'))
    ->addFile(LogLevel::DEBUG, '/var/log/app.log', name: 'app_log')
    ->addFile(LogLevel::ERROR, '/var/log/errors.log', name: 'error_log')
    ->addSlack(LogLevel::CRITICAL, $webhookUrl, name: 'slack_alerts')
    ->getLogger();

// Handler zur Laufzeit abrufen
$appHandler   = $logger->getHandler('app_log');
$allHandlers  = $logger->getHandlers();
$fileHandlers = $logger->getHandlersByClass(LogFile::class);

Architektur

Unter der Haube folgt der Logger dem Closure-Orchestrator-Pattern, dem architektonischen Grundprinzip aller Jardis-Packages.

LoggerBuilder                        ← Builder (Fluent API)
└── Logger                           ← Orchestrator (PSR-3, immutabel)
    ├── LogFile                      ← Handler (extends LogCommand)
    │   ├── LogData                  ← Record Builder + Enricher
    │   │   ├── LogDateTime          ← Enricher (Callable)
    │   │   └── LogUuid              ← Enricher (Callable)
    │   └── LogLineFormat            ← Formatter
    ├── LogSlack                     ← Handler
    │   ├── LogSlackFormat           ← Formatter
    │   └── HttpTransport            ← HTTP-Transport (Retry)
    └── LogFingersCrossed            ← Smart Handler (Decorator)
        └── LogFile                  ← Gewrappter Handler

Verzeichnisstruktur

src/
├── Logger.php                    ← PSR-3 Orchestrator
├── LoggerBuilder.php             ← Fluenter Builder
├── Contract/
│   ├── LogCommandInterface.php
│   ├── StreamableLogCommandInterface.php
│   ├── LogDataInterface.php
│   └── LogFormatInterface.php
├── Data/
│   ├── LogData.php               ← Record Builder
│   └── LogLevel.php              ← Level → Severity Map
├── Enricher/
│   ├── LogDateTime.php
│   ├── LogUuid.php
│   ├── LogClientIp.php
│   ├── LogWebRequest.php
│   ├── LogMemoryUsage.php
│   └── LogMemoryPeak.php
├── Formatter/
│   ├── LogLineFormat.php
│   ├── LogJsonFormat.php
│   ├── LogHumanFormat.php
│   ├── LogSlackFormat.php
│   ├── LogTeamsFormat.php
│   ├── LogLokiFormat.php
│   └── LogBrowserConsoleFormat.php
└── Handler/
    ├── LogCommand.php            ← Abstrakte Basis
    ├── LogFile.php
    ├── LogConsole.php
    ├── LogErrorLog.php
    ├── LogSyslog.php
    ├── LogBrowserConsole.php
    ├── LogNull.php
    ├── LogSlack.php
    ├── LogTeams.php
    ├── LogLoki.php
    ├── LogWebhook.php
    ├── LogDatabase.php
    ├── LogEmail.php
    ├── LogRedis.php
    ├── LogRedisMq.php
    ├── LogRabbitMq.php
    ├── LogKafkaMq.php
    ├── LogStash.php
    ├── HttpTransport.php
    ├── LogFingersCrossed.php
    ├── LogSampling.php
    └── LogConditional.php

API-Referenz

LoggerBuilder

MethodeSignaturBeschreibung
__construct__construct(string $context)Bounded-Context-Name für alle Records
addHandleraddHandler(LogCommandInterface $handler): selfBeliebigen Handler registrieren
setErrorHandlersetErrorHandler(callable $handler): selfFehler-Callback für Handler-Exceptions
getLoggergetLogger(): LoggerFinalisiert — Logger ist immutabel

Convenience-Methoden (alle return self):

MethodeParameter
addConsole(string $level, ?string $name, ?LogFormatInterface $format)
addFile(string $level, string $path, ?string $name, ?LogFormatInterface $format)
addErrorLog(string $level, ?string $name)
addSyslog(string $level, ?string $name)
addBrowserConsole(string $level, ?string $name)
addNull(string $level, ?string $name)
addDatabase(string $level, \PDO $pdo, ?string $table, ?string $name)
addSlack(string $level, string $url, ?string $name, int $timeout, int $retryAttempts)
addTeams(string $level, string $url, ?string $name, int $timeout, int $retryAttempts)
addLoki(string $level, string $url, array $staticLabels, ?string $name, int $timeout, int $retryAttempts)
addWebhook(string $level, string $url, ?string $name, string $method, array $headers, ...)
addEmail(string $level, string $to, string $from, string $subject, string $smtpHost, ...)
addRedis(string $level, \Redis $redis, ?string $name, int $ttl)
addRedisMq(\Redis $redis, string $channel, ?string $name)
addRabbitMq(\AMQPConnection $connection, string $exchange, ?string $name)
addKafkaMq(\RdKafka\Producer $producer, string $topic, ?string $name)
addStash(string $level, string $host, int $port, ?array $bindTo, ?string $name)
addFingersCrossed(StreamableLogCommandInterface $wrappedHandler, string $activationLevel, int $bufferSize, bool $stopBufferingAfterActivation, ?string $name)
addSampling(StreamableLogCommandInterface $wrappedHandler, string $strategy, array $config, ?string $name)
addConditional(array $conditionalHandlers, ?StreamableLogCommandInterface $fallbackHandler, ?string $name)

Logger

MethodeSignaturBeschreibung
loglog($level, \Stringable|string $message, array $context = []): voidPSR-3 Log-Methode
debug ... emergency(Stringable|string $message, array $context = []): voidPSR-3 Convenience
getHandlergetHandler(string $name): ?LogCommandInterfaceHandler per Name abrufen
getHandlersgetHandlers(): arrayAlle Handler (keyed by ID)
getHandlersByClassgetHandlersByClass(string $class): arrayHandler per Klasse filtern

Vollständiges Beispiel

Ein produktionsreifes Setup mit File-Logging, Slack-Alerts, Loki-Metriken und FingersCrossed-Buffering:

php
use JardisAdapter\Logger\LoggerBuilder;
use JardisAdapter\Logger\Data\LogData;
use JardisAdapter\Logger\Data\LogLevel;
use JardisAdapter\Logger\Enricher\LogDateTime;
use JardisAdapter\Logger\Enricher\LogUuid;
use JardisAdapter\Logger\Enricher\LogClientIp;
use JardisAdapter\Logger\Formatter\LogJsonFormat;
use JardisAdapter\Logger\Handler\LogFile;
use JardisAdapter\Logger\Handler\LogFingersCrossed;

// Enricher konfigurieren
$logData = (new LogData())
    ->addField('timestamp', new LogDateTime())
    ->addField('hostname', fn() => gethostname())
    ->addExtra('request_id', new LogUuid())
    ->addExtra('client_ip', new LogClientIp());

// Debug-Handler mit FingersCrossed (Buffer bis Error)
$debugFile = new LogFile(LogLevel::DEBUG, '/var/log/debug.log');
$debugFile->setLogData($logData);
$debugFile->setFormat(new LogJsonFormat());

$logger = (new LoggerBuilder('OrderService'))
    // Strukturiertes File-Logging ab INFO
    ->addFile(LogLevel::INFO, '/var/log/orders.log', name: 'main_log',
        format: new LogJsonFormat())
    // Debug-Context nur bei Fehlern (FingersCrossed)
    ->addFingersCrossed(
        wrappedHandler: $debugFile,
        activationLevel: LogLevel::ERROR,
        bufferSize: 200,
        name: 'debug_buffer'
    )
    // Slack für kritische Fehler
    ->addSlack(LogLevel::CRITICAL, 'https://hooks.slack.com/services/T.../B.../xxx',
        name: 'slack_alerts')
    // Grafana Loki für alle Levels
    ->addLoki(LogLevel::DEBUG, 'http://loki:3100/loki/api/v1/push',
        staticLabels: ['app' => 'orders', 'env' => 'production'],
        name: 'loki')
    // Error-Handler für Handler-Fehler
    ->setErrorHandler(function (\Exception $e, string $handlerId) {
        error_log("Logger handler {$handlerId} failed: " . $e->getMessage());
    })
    ->getLogger();

// Normalbetrieb — Info-Logs gehen nur in Datei und Loki
$logger->info('Order {order_id} created', ['order_id' => 4711]);

// Fehler — FingersCrossed flushed Debug-Buffer, Slack alertet
$logger->error('Payment timeout for order {order_id}', [
    'order_id' => 4711,
    'gateway' => 'stripe',
    'timeout_ms' => 30000,
]);