Why Logging Sucks (And How to Fix It)
As loggingsucks.com brilliantly articulates, traditional logging is fundamentally broken for modern applications. The problems are systemic:
Logs were built for the wrong era. They originated when systems were monolithic and ran on single servers. Today, a single user request might touch 15+ services, multiple databases, caches, and message queues. Traditional logging practices simply don't scale to this distributed reality.
String search is fundamentally flawed. When you search logs for "user-123", you discover it's logged inconsistently across services in dozens of different formats. Logs are optimized for writing, not for querying—making post-incident investigation a painful archaeological expedition through grep sessions.
Massive volume, minimal insight. Modern applications generate tens of thousands of log lines per second. A single successful request might emit 17 different log statements scattered across your codebase. Multiplied across thousands of concurrent users, this becomes unmanageable noise lacking the context you actually need for debugging.
The solution proposed by loggingsucks.com is simple but powerful: wide events (or canonical log lines). Instead of scattering log.info() statements throughout your code, emit one comprehensive, context-rich structured event per request per service. This single event should contain all potentially useful debugging information: user context, business data, feature flags, error details, and more.
Enter Knotlog
Knotlog is a PHP library that implements wide logging as a first-class pattern. Rather than treating wide events as an afterthought, Knotlog makes them the default way to log.
The core concept is straightforward: build up context throughout your request lifecycle, then emit a single structured event at the end. Transform debugging from archaeological grep sessions into analytical queries with structured, queryable data.
How to Use Knotlog
Installation
composer require knotlog/knotlog
Knotlog requires PHP 8.4 or newer.
Basic Usage
The fundamental workflow is simple: create a Log instance, accumulate context as your request processes, then write it out at the end:
use Knotlog\Log;
use Knotlog\Writer\FileWriter;
$log = new Log();
// Build up context throughout your request
$log->set('user_id', $userId);
$log->set('request_method', $_SERVER['REQUEST_METHOD']);
$log->set('request_path', $_SERVER['REQUEST_URI']);
$log->set('subscription_tier', $user->subscriptionTier);
$log->set('cart_value', $cart->total());
$log->set('response_status', 200);
$log->set('duration_ms', $duration);
// At the end of the request, emit the wide event
new FileWriter()->write($log);
The Log class implements JsonSerializable, making it directly encodable to JSON for structured logging systems.
Exception Logging
Knotlog provides an ExceptionLog class to capture rich exception context including stack traces:
use Knotlog\Misc\ExceptionLog;
try {
// Some code that may throw
} catch (Throwable $throwable) {
$log->set('exception', ExceptionLog::fromThrowable($throwable));
}
The Log class provides a convenient hasError() method that returns true if either exception or error keys are set—particularly useful when implementing log sampling to ensure errors are always captured.
PSR-15 Middleware Integration
One of Knotlog's most powerful features is its seamless integration with PSR-15 middleware. This allows you to automatically capture request and response context without manually instrumenting every endpoint.
LogRequestResponse
The LogRequestResponse middleware automatically logs request and response metadata:
use Knotlog\Http\LogRequestResponse;
// Logs request and response metadata
$stack->add(new LogRequestResponse($log));
This middleware captures the request before passing it to the next handler, then captures the response on the way back—adding both to your wide event automatically.
LogRequestError
The LogRequestError middleware catches uncaught exceptions, logs them, and generates appropriate error responses:
use Knotlog\Http\LogRequestError;
use Knotlog\Http\ServerErrorResponseFactory;
$errorFactory = new ServerErrorResponseFactory($responseFactory, $streamFactory);
// Logs uncaught exceptions and outputs an error response
$stack->add(new LogRequestError($errorFactory, $log));
When an exception bubbles up through your middleware stack, this middleware captures it as an ExceptionLog, adds it to your wide event, and generates a clean error response.
LogResponseError
The LogResponseError middleware flags any response with a 400+ status code as an error:
use Knotlog\Http\LogResponseError;
// Sets error to the reason phrase for 400+ status codes
$stack->add(new LogResponseError($log));
This is particularly useful with log sampling—it ensures that error responses are always logged even when you're only sampling a small percentage of successful requests.
Middleware Ordering
The middleware should be ordered (from first to last):
-
LogResponseError- flag error responses -
LogRequestResponse- log request/response metadata -
LogRequestError- catch uncaught exceptions
Note that some frameworks use last-in-first-out ordering for their middleware stacks.
PSR-3 Logger Integration
While Knotlog is designed to replace traditional logging patterns, it recognizes that many applications already have PSR-3 logger infrastructure in place. The LoggerWriter provides a bridge to route wide events through any PSR-3 compatible logger:
use Knotlog\Writer\LoggerWriter;
// Use with any PSR-3 logger (Monolog, etc.)
$writer = new LoggerWriter($psrLogger);
// Write the log event
$writer->write($log);
The LoggerWriter uses PSR-3 message interpolation to format your wide events. It automatically routes log events to the appropriate log level:
-
error()- when the log contains an error or exception (uses{error}placeholder) -
info()- for all other log events (uses{message}placeholder)
The entire log context is passed to the logger, allowing it to format and process the structured data according to its own implementation.
You can customize the message and error keys to match your logging schema:
$writer = new LoggerWriter($psrLogger, messageKey: 'msg', errorKey: 'err');
This integration means you can adopt wide logging patterns without abandoning your existing logging infrastructure. Your PSR-3 logger receives rich, structured context instead of scattered string messages.
Additional Features
Log Sampling
The SampledWriter decorator allows you to sample log events while always capturing errors:
use Knotlog\Writer\SampledWriter;
use Knotlog\Writer\FileWriter;
// Sample 1 in 100 requests (1%)
$writer = new SampledWriter(new FileWriter(), 100);
$writer->write($log);
All log events with errors or exceptions are always written—sampling only applies to successful requests. This is critical for high-traffic applications where logging every successful request would be prohibitively expensive.
Console Integration
Knotlog provides Symfony Console event listeners to apply wide logging to CLI commands:
use Knotlog\Console\LogCommandError;
use Knotlog\Console\LogCommandEvent;
use Symfony\Component\Console\ConsoleEvents;
// Logs command metadata on execution
$eventDispatcher->addListener(ConsoleEvents::COMMAND, new LogCommandEvent($log));
// Logs command error context on failure
$eventDispatcher->addListener(ConsoleEvents::ERROR, new LogCommandError($log));
The Bottom Line
Traditional logging asks "what is my code doing?" Wide logging asks "what happened to this request?" This shift in perspective—combined with Knotlog's first-class PSR-15 middleware and PSR-3 integration—transforms logging from a debugging afterthought into a powerful observability tool.
Stop scattering log statements throughout your code. Start capturing comprehensive, queryable context with Knotlog.
Learn more at github.com/shadowhand/knotlog and read the philosophy at loggingsucks.com.
Top comments (0)