Logging

Fast ships a tiny structured logger. Each record is one line of JSON, which stays readable for a human running grep and parseable for a log shipper, without a schema or an external library. It works with zero configuration and gets out of your way in production.

Writing log records

Reach the logger with logger() and call a level method on it:

logger()->info('user signed in', ['id' => $user->id]);
logger()->warning('slow query', ['ms' => $elapsed]);
logger()->error('payment failed', ['order' => $orderId]);

There are four levels, from least to most severe: debug, info, warning, error. The second argument is an optional context array that rides along with the record. Each record also carries a timestamp, the level, the message, and the worker's process id:

{"ts":"2026-07-17T16:22:04Z","level":"info","msg":"user signed in","pid":123,"context":{"id":42}}

The helper is named logger(), not log(), because PHP already defines a built-in log() (the math function) that cannot be redefined.

Levels and thresholds

A record below the configured level is dropped before it is ever formatted, so leaving debug() calls in a hot path costs almost nothing once you are running at info. The default level is info.

You can set the threshold without touching code through the LOG_LEVEL environment variable, which is handy for turning up verbosity on a running server:

LOG_LEVEL=debug

Configuration

Call use_logging() once at startup to change the level or the destination. It takes a LogConfig, and every field has a safe default:

use Fast\Log\LogConfig;

// Log at debug level to the default STDERR stream.
use_logging(new LogConfig(level: 'debug'));

// Append records to a file instead.
use_logging(new LogConfig(sink: 'file', path: __DIR__ . '/storage/app.log'));

The default sink is stderr: one JSON line per record on the standard-error stream, which your terminal or a process supervisor captures. The file sink appends to path instead.

Notes for production

Two things are worth knowing when you run under load.

Fast serves requests from a pool of independent worker processes that share one STDERR stream. A record is written in a single call to keep interleaving to a minimum, but if you redirect that STDERR to a plain file, very large records from different workers can still interleave. The file sink appends with an exclusive lock, so it is the durable choice when many workers log heavily.

Writes are synchronous, so a stalled consumer (a full pipe, a slow disk) blocks the worker for that one write. Keep the level at info or higher in production and avoid logging on the very hottest paths.