Skip to content
Viames Marino edited this page May 5, 2026 · 4 revisions

Pair framework: Logger class

The Logger class is a singleton implementing the PSR-3 LoggerInterface.

It provides a unified way to:

  • log messages to LogBar (runtime log viewer);
  • persist warnings and errors into the database (LogEvent) through a structured log-event writer;
  • send email and Telegram notifications for severe events;
  • register custom error / exception / shutdown handlers;
  • optionally forward errors to Sentry and OpenTelemetry-compatible observability adapters.

The logger is designed to be safe in production and helpful in development, while keeping the integration small and predictable.


Getting the logger instance

use Pair\Core\Logger;

$logger = Logger::getInstance();

Logger::getInstance() always returns the same shared instance.


Log levels

Pair uses numeric levels compatible with PSR-3 severity ordering:

Constant Value Name
Logger::EMERGENCY 1 emergency
Logger::ALERT 2 alert
Logger::CRITICAL 3 critical
Logger::ERROR 4 error
Logger::WARNING 5 warning
Logger::NOTICE 6 notice
Logger::INFO 7 info
Logger::DEBUG 8 debug

Lower number = more severe.

The Logger::LEVEL_NAMES constant maps numeric levels to their string names.


What happens when you log

Always: LogBar event

Every call ends up in LogBar:

  • debug, info, noticeLogBar::event(..., 'notice')
  • warningLogBar::event(..., 'warning')
  • error, critical, alert, emergencyLogBar::event(..., 'error')

Only for WARNING or worse: processing (DB + notifications)

If the level is WARNING or worse (<= Logger::WARNING), the logger also:

  1. forwards a structured ObservabilityEvent when an event-aware adapter is installed;
  2. snapshots the request and log event into the DB (LogEvent);
  3. optionally sends email notifications (if configured);
  4. optionally sends Telegram notifications (if configured).

The database row includes the rendered message and sanitized context, plus release, environment, trace, request, user-agent, exception, and grouping metadata.


Configuration via .env

Logger reads these values from Env during construction:

  • PAIR_LOGGER_DISABLED
    If set to true, the logger is disabled.

  • PAIR_LOGGER_EMAIL_RECIPIENTS
    Comma-separated emails. Example: ops@example.com,dev@example.com

  • PAIR_LOGGER_EMAIL_THRESHOLD
    Numeric level from 1 to 8. Default is 4 (error).

  • TELEGRAM_BOT_TOKEN
    Bot token used to send Telegram messages.

  • PAIR_LOGGER_TELEGRAM_CHAT_IDS
    Comma-separated chat IDs. Example: 123456789,987654321

  • PAIR_LOGGER_TELEGRAM_THRESHOLD
    Numeric level from 1 to 8. Default is 4 (error).

Additionally:

  • APP_DEBUG controls handler behavior (see errorHandler()).
  • SENTRY_DSN enables forwarding errors to Sentry (when SDK functions exist).

Example .env:

APP_DEBUG=false

PAIR_LOGGER_DISABLED=false

PAIR_LOGGER_EMAIL_RECIPIENTS=ops@example.com,dev@example.com
PAIR_LOGGER_EMAIL_THRESHOLD=4

TELEGRAM_BOT_TOKEN=123456:ABCDEF_your_token_here
PAIR_LOGGER_TELEGRAM_CHAT_IDS=123456789,987654321
PAIR_LOGGER_TELEGRAM_THRESHOLD=3

SENTRY_DSN=https://public@sentry.example/1

Configuring email transport

Email is sent only if:

  • a mail transport has been configured ($logger->useTransport(...));
  • emailRecipients are set;
  • level <= emailThreshold.

Example: SMTP transport

use Pair\Core\Logger;

$logger = Logger::getInstance();

$logger->useTransport('smtp', [
    // The config array is passed to the transport constructor.
    // Keys depend on the transport implementation.
    'host' => 'smtp.example.com',
    'port' => 587,
    'username' => 'user',
    'password' => 'secret',
    'encryption' => 'tls',
    'from' => ['no-reply@example.com' => 'Pair'],
]);

$logger->setEmailRecipients(['ops@example.com']);
$logger->setEmailThreshold(Logger::ERROR);

Example: Amazon SES transport

$logger->useTransport('ses', [
    // Passed as-is to AmazonSes transport
    'region' => 'eu-west-1',
    'accessKeyId' => '...',
    'secretAccessKey' => '...',
    'from' => ['no-reply@example.com' => 'Pair'],
]);

Example: Sendmail transport

$logger->useTransport('sendmail', [
    // Passed as-is to Sendmail transport
    'from' => ['no-reply@example.com' => 'Pair'],
]);

If email transport is not configured, logs still work (LogBar + DB snapshot + Telegram if configured).


Configuring Telegram notifications

Telegram is sent only if:

  • telegramBotToken is set;
  • telegramChatIds contains at least one chat ID;
  • telegramThreshold >= level (meaning: send on severe events only).
use Pair\Core\Logger;

$logger = Logger::getInstance();

$logger->setTelegramBotToken(Env::get('TELEGRAM_BOT_TOKEN'));
$logger->setTelegramChatIds([123456789, 987654321]);
$logger->setTelegramThreshold(Logger::CRITICAL);

Registering global handlers

Pair can route PHP errors, uncaught exceptions, and fatal shutdown errors to the logger.

use Pair\Core\Logger;

Logger::registerHandlers();

This sets:

  • set_error_handler([Logger::class, 'errorHandler'])
  • set_exception_handler([Logger::class, 'exceptionHandler'])
  • register_shutdown_function([Logger::class, 'shutdownHandler'])

Quick usage examples

Basic messages

$logger = Logger::getInstance();

$logger->info('User logged in');
$logger->notice('Profile updated');
$logger->warning('Slow query detected');
$logger->error('Payment gateway failure');

Context placeholders (PSR-3 style)

Logger replaces {placeholders} with scalar / stringable values from $context.

$logger->error(
    'Order {orderId} failed for user {userId}',
    ['orderId' => 1001, 'userId' => 55]
);

This is done safely: non-scalar values are ignored.

Structured context

For warning/error records, the logger also stores sanitized context in LogEvent and promotes selected values to first-class columns.

Useful standard keys:

  • eventName: stable event name such as order.export.failed.
  • correlationId: request or job correlation ID.
  • exception: a Throwable instance, or exception details prepared by the caller.

appVersion and environment are read from the Pair runtime (APP_VERSION and Application::getEnvironment()). Trace ID is read from an incoming W3C traceparent server value when available, otherwise Pair derives a trace-compatible ID from the correlation ID.

Request arrays and context payloads are sanitized by LogEventSanitizer before being stored. Sensitive keys such as authorization, cookie, password, secret, token, and API keys are redacted.

Manual severity (string or numeric)

$logger->log('error', 'Something went wrong');
$logger->log(Logger::ERROR, 'Something went wrong');

Method reference

public static function getInstance(): self

Returns the singleton instance.

$logger = Logger::getInstance();

public static function registerHandlers(): void

Registers the error, exception and shutdown handlers.

Logger::registerHandlers();

public static function errorHandler(int $errno, string $errstr, ?string $errfile = null, ?int $errline = null): bool

Custom PHP error handler.

  • Logs the error internally (as error level).
  • If SENTRY_DSN is set, forwards to Sentry.
  • Forwards to Insight Hub handler.
  • If APP_DEBUG is true, returns false so PHP can also display the error.
  • Otherwise returns true to suppress output in production.

Returning false allows the default PHP handler to run, which is useful during development.


public static function exceptionHandler(Throwable $e): void

Custom uncaught exception handler.

  • Forwards to Sentry (if enabled and SDK exists).
  • Forwards the exception to Insight Hub.
  • Logs the exception message and location internally.

public static function shutdownHandler(): void

Shutdown handler for fatal errors.

  • Reads error_get_last().
  • If the last error is fatal (E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR):
    • logs it;
    • forwards to Sentry (if enabled);
    • forwards to Insight Hub.

public function disable(): void

Disables the logger (no LogBar, no DB snapshot, no notifications).

Logger::getInstance()->disable();

public function log(mixed $level, Stringable|string $message, array $context = []): void

The main PSR-3 log entrypoint.

  • Accepts:
    • numeric level 1..8, or
    • string level name ("error", "warning", etc).
  • Renders {placeholders} using the provided context.
  • Logs to LogBar.
  • For WARNING or worse, triggers processing (DB + notifications).

Convenience PSR-3 methods

Each of these calls log() with the corresponding level:

  • emergency(Stringable|string $message, array $context = [])
  • alert(Stringable|string $message, array $context = [])
  • critical(Stringable|string $message, array $context = [])
  • error(Stringable|string $message, array $context = [])
  • warning(Stringable|string $message, array $context = [])
  • notice(Stringable|string $message, array $context = [])
  • info(Stringable|string $message, array $context = [])
  • debug(Stringable|string $message, array $context = [])

Example:

$logger->critical('Database is not responding', ['errorCode' => 123]);

public function useTransport(string $type, array $config): void

Configures the email transport used for error notifications.

Supported types:

  • smtpSmtpMailer
  • sesAmazonSes
  • sendmailSendmail
$logger->useTransport('smtp', [/* transport config */]);

If the type is unknown, an InvalidArgumentException is thrown.


public function setEmailRecipients(array $recipients): void

Sets the email recipients list. Values are normalized and filtered to valid emails.

$logger->setEmailRecipients(['ops@example.com', 'dev@example.com']);

public function setEmailThreshold(int $level): void

Sets the email threshold level (1..8).

  • If the level is invalid, the logger emits an internal configuration error.
$logger->setEmailThreshold(Logger::ERROR);

public function setTelegramBotToken(string $token): void

Sets the Telegram bot token used by TelegramBotClient.

$logger->setTelegramBotToken('123456:ABCDEF...');

public function setTelegramChatIds(array $chatIds): void

Sets the Telegram chat IDs list. Values are normalized to integers.

$logger->setTelegramChatIds([123456789, 987654321]);

public function setTelegramThreshold(int $level): void

Sets the Telegram threshold level (1..8).

$logger->setTelegramThreshold(Logger::CRITICAL);

Internal methods (implementation details)

These methods are part of the internal implementation and are listed here for completeness.

private function interpolate(string $message, array $context): string

Replaces {key} tokens with scalar or stringable values found in $context.
Non-scalar values are ignored.


private function snapshot(string $description, ?int $level = null, array $context = []): void

Stores a structured snapshot into the database by delegating to DatabaseLogWriter, including:

  • level;
  • event name, app version, and environment;
  • correlation ID, trace ID, and fingerprint;
  • user id, router path, request method, referer, client IP, and user agent;
  • sanitized request payload, context, selected $_SERVER values, and queued application notifications inside context data;
  • exception class, file, line, and trace when a Throwable is available;

Cookies are intentionally not stored. The snapshot is performed only when the DB is connected and the error is not a known DB connection/query failure, so logging does not recursively amplify database outages.


private function process(int $level, string $description, array $context = [], ?int $errorCode = null): void

Runs the “severe log” pipeline for WARNING or worse:

  1. snapshots to DB (when allowed);
  2. sends email notification (if configured);
  3. sends Telegram notification (if configured).

private function ensureMailer(): void

Verifies mailer configuration and throws/logs on misconfiguration.
Called before sending email notifications.


private function setSmtpMailerConfig(array $config): void

private function setSesConfig(array $config): void

private function setSendmailConfig(array $config): void

Transport-specific builders. Each one instantiates the corresponding transport object with $config.


Notes

  • You can pass an errorCode in $context (as ['errorCode' => ...]) to help categorise errors or avoid specific pipelines.
  • DB snapshot is skipped for known DB connection and query failures to prevent recursion during outages.
  • New LogEvent payloads are stored as JSON. The model still reads legacy PHP-serialized payloads.
  • Sentry integration requires sentry/sdk. Insight Hub integration requires bugsnag/bugsnag.

See also: Log, LogBar, LogEvent, ErrorLog, DatabaseLogWriter, LogEventSanitizer, LogEventFingerprint, Observability, Env, Application.

Clone this wiki locally