Skip to content

Logger

Structured logging with 20+ handlers, fluent configuration and built-in fault resilience.

Introduction

Logging sounds trivial: error_log() and done. In practice, an enterprise application needs logs in files, alerts in Slack, metrics in Grafana Loki, all with different thresholds, structured data and without a failing handler blocking the entire application.

jardisadapter/logger is a PSR-3 compatible logging pipeline that delivers exactly that:

  • 20+ handlers — file, console, syslog, Slack, Teams, Loki, database, Redis, Kafka, RabbitMQ, email, Logstash, browser console and more
  • Fluent LoggerBuilder — one builder configures all handlers, the result is an immutable logger
  • Fault resilience — a failing handler never breaks the pipeline. The other handlers continue, errors are reported to an optional error handler
  • Structured log records — enrichers add timestamp, UUID, client IP, memory usage and any custom fields
  • Smart handlers — FingersCrossed (buffer until error), Sampling (volume reduction) and Conditional (routing via callback)
  • Bounded-context scoping — every logger carries a context name that appears in every log record
  • 7 formatters — Line, JSON, Human-readable, Slack, Teams, Loki, Browser Console

Installation

bash
composer require jardisadapter/logger

GitHub: jardisAdapter/logger

Optional PHP extensions:

ExtensionFor
ext-redisLogRedis, LogRedisMq
ext-amqpLogRabbitMq
ext-rdkafkaLogKafkaMq

Basic Usage

Two steps: configure the builder, use the logger.

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']);

Each handler has a minimum log level. addFile(LogLevel::INFO, ...) ignores debug messages: only info and above are written to the file. The console receives everything from debug upwards.

Error Handler for Resilient Pipelines

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

// If Slack fails, the file output still runs
$logger->error('Critical failure');

Log Level Hierarchy

The PSR-3 log levels are numerically ordered. A handler with level warning ignores everything below it:

LevelSeverityTypical use
emergency7System unusable
alert6Immediate action required
critical5Critical errors
error4Runtime errors
warning3Unusual conditions
notice2Normal but notable events
info1Informational
debug0Debug information

Handler Overview

Stream-Based Handlers

HandlerTargetBuilder method
LogFileFile (lazy open)addFile(level, path)
LogConsoleSTDOUTaddConsole(level)
LogErrorLogSTDERRaddErrorLog(level)
LogSyslogSyslogaddSyslog(level)
LogBrowserConsoleChromeLogger HTTP headeraddBrowserConsole(level)
LogNullNowhere (test/Graceful Degradation)addNull(level)

Webhook Handlers

HandlerTargetBuilder method
LogSlackSlack Incoming WebhookaddSlack(level, url)
LogTeamsMS Teams MessageCardaddTeams(level, url)
LogLokiGrafana Loki Push APIaddLoki(level, url, staticLabels)
LogWebhookAny HTTP endpointaddWebhook(level, url)

Persistence Handlers

HandlerTargetBuilder method
LogDatabasePDO INSERT (MySQL/PgSQL/SQLite)addDatabase(level, pdo)
LogRedisRedis SETEX with TTLaddRedis(level, redis)
LogEmailDirect SMTP deliveryaddEmail(level, to, from, ...)
LogStashLogstash TCPaddStash(level, host, port)

Message Queue Handlers

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

Structured Log Records

LogData — Enricher Pipeline

Every handler builds its log record via a LogData object. Enrichers are callables that are evaluated only at logging time: zero cost until actually used.

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() adds fields at root level. addExtra() adds fields in the nested data section.

Resulting 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)."
  }
}

Built-in Enrichers

EnricherProvides
LogDateTimeCurrent date/time (Y-m-d H:i:s)
LogUuidUUID v4
LogClientIpClient IP (proxy-aware: X-Forwarded-For, HTTP_CLIENT_IP)
LogWebRequestArray with IP, URL, method, user agent, GET/POST data
LogMemoryUsageCurrent memory usage
LogMemoryPeakPeak memory usage

PSR-3 Message Interpolation

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

Placeholders in {key} format are resolved from the context array. Arrays are JSON-encoded, callables are executed.

Custom LogData per Handler

php
use JardisAdapter\Logger\Handler\LogFile;

$handler = new LogFile(LogLevel::DEBUG, '/var/log/app.log');
$handler->setLogData($logData);  // Custom enrichers only for this handler

Formatters

Each handler has a default formatter that can be overridden via setFormat() or a builder parameter.

FormatterFormatDefault for
LogLineFormat{ "key": "value", ... }\nFile, console, STDERR
LogJsonFormatPure JSON (json_encode)Logstash, webhooks
LogHumanFormatMulti-line: KEY: value\nDebug output
LogSlackFormatSlack webhook payload with emoji and colorsLogSlack
LogTeamsFormatMS Teams MessageCard with ThemeColorLogTeams
LogLokiFormatLoki Push API streams with nanosecond timestampsLogLoki
LogBrowserConsoleFormatChromeLogger v4.1.0 protocolLogBrowserConsole
php
use JardisAdapter\Logger\Formatter\LogJsonFormat;

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

Smart Handlers

Three specialized handlers for advanced logging strategies.

FingersCrossed — Buffer Until Error

Collects all log messages in a buffer. Only when a message reaches the activation level (default: error) is the entire buffer flushed at once. This provides the full context before an error, without flooding the disk during normal operation.

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');    // buffered
$logger->info('Validating card');              // buffered
$logger->error('Card declined');               // → flush: all 3 messages at once
$logger->info('Retrying...');                  // written directly (after activation)
ParameterDefaultDescription
activationLevelerrorLevel that triggers flush
bufferSize100Max buffered messages (FIFO)
stopBufferingAfterActivationtruePass through directly after flush

Sampling — Volume Reduction

Reduces log volume through various sampling strategies: ideal for high-frequency endpoints where not every request needs to be logged.

php
$logger = (new LoggerBuilder('ApiService'))
    ->addSampling(
        wrappedHandler: $fileHandler,
        strategy: 'smart',
        config: [
            'alwaysLogLevels' => ['error', 'critical', 'alert', 'emergency'],
            'samplePercentage' => 10,
        ]
    )
    ->getLogger();
StrategyDescription
rateMax N messages per second
percentageLet through only X% of messages
smartAlways log errors, rest by percentage
fingerprintDeduplication: same message only once per time window

Conditional — Routing via Callback

Routes messages to different handlers based on callables.

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();

The first condition returning true wins. If none matches and no fallback is set, the message is silently discarded.

Named Handlers

Every handler can receive a name and be retrieved at runtime:

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();

// Retrieve handler at runtime
$appHandler   = $logger->getHandler('app_log');
$allHandlers  = $logger->getHandlers();
$fileHandlers = $logger->getHandlersByClass(LogFile::class);

Architecture

Under the hood, the logger follows the Closure-Orchestrator-Pattern, the architectural core principle of all Jardis packages.

LoggerBuilder                        ← Builder (fluent API)
└── Logger                           ← Orchestrator (PSR-3, immutable)
    ├── LogFile                      ← Handler (extends LogCommand)
    │   ├── LogData                  ← Record builder + enrichers
    │   │   ├── LogDateTime          ← Enricher (callable)
    │   │   └── LogUuid              ← Enricher (callable)
    │   └── LogLineFormat            ← Formatter
    ├── LogSlack                     ← Handler
    │   ├── LogSlackFormat           ← Formatter
    │   └── HttpTransport            ← HTTP transport (retry)
    └── LogFingersCrossed            ← Smart handler (decorator)
        └── LogFile                  ← Wrapped handler

Directory Structure

src/
├── Logger.php                    ← PSR-3 orchestrator
├── LoggerBuilder.php             ← Fluent 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            ← Abstract base
    ├── 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 Reference

LoggerBuilder

MethodSignatureDescription
__construct__construct(string $context)Bounded-context name for all records
addHandleraddHandler(LogCommandInterface $handler): selfRegister any handler
setErrorHandlersetErrorHandler(callable $handler): selfError callback for handler exceptions
getLoggergetLogger(): LoggerFinalize — logger is immutable

Convenience methods (all return self):

MethodParameters
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

MethodSignatureDescription
loglog($level, \Stringable|string $message, array $context = []): voidPSR-3 log method
debug ... emergency(Stringable|string $message, array $context = []): voidPSR-3 convenience
getHandlergetHandler(string $name): ?LogCommandInterfaceGet handler by name
getHandlersgetHandlers(): arrayAll handlers (keyed by ID)
getHandlersByClassgetHandlersByClass(string $class): arrayFilter handlers by class

Complete Example

A production-ready setup with file logging, Slack alerts, Loki metrics and 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;

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

// Debug handler with FingersCrossed (buffer until error)
$debugFile = new LogFile(LogLevel::DEBUG, '/var/log/debug.log');
$debugFile->setLogData($logData);
$debugFile->setFormat(new LogJsonFormat());

$logger = (new LoggerBuilder('OrderService'))
    // Structured file logging from INFO
    ->addFile(LogLevel::INFO, '/var/log/orders.log', name: 'main_log',
        format: new LogJsonFormat())
    // Debug context only on errors (FingersCrossed)
    ->addFingersCrossed(
        wrappedHandler: $debugFile,
        activationLevel: LogLevel::ERROR,
        bufferSize: 200,
        name: 'debug_buffer'
    )
    // Slack for critical errors
    ->addSlack(LogLevel::CRITICAL, 'https://hooks.slack.com/services/T.../B.../xxx',
        name: 'slack_alerts')
    // Grafana Loki for all levels
    ->addLoki(LogLevel::DEBUG, 'http://loki:3100/loki/api/v1/push',
        staticLabels: ['app' => 'orders', 'env' => 'production'],
        name: 'loki')
    // Error handler for handler failures
    ->setErrorHandler(function (\Exception $e, string $handlerId) {
        error_log("Logger handler {$handlerId} failed: " . $e->getMessage());
    })
    ->getLogger();

// Normal operation — info logs go only to file and Loki
$logger->info('Order {order_id} created', ['order_id' => 4711]);

// Error — FingersCrossed flushes debug buffer, Slack alerts
$logger->error('Payment timeout for order {order_id}', [
    'order_id' => 4711,
    'gateway' => 'stripe',
    'timeout_ms' => 30000,
]);