quillstack / logger
A simple logger based on PSR-3: Logger Interface.
Requires
- php: ^8.1
- ext-json: *
- psr/log: ^3.0
- quillstack/local-storage: ^0.6
- quillstack/output: ^0.6
- quillstack/storage-interface: ^0.6
Requires (Dev)
- phpstan/phpstan: ^2.0
- quillstack/unit-tests: ^0.9
README
A logger based on PSR-3, with no dependencies beyond it. Full documentation: https://quillstack.org/logger
Somewhere to write what happened, and a way to say how much of it is worth writing. A handler decides where the entries go; there is no formatter to configure and no processor to register.
Requirements
- PHP 8.1 or newer
Installation
composer require quillstack/logger
Usage
use Quillstack\LocalStorage\LocalStorage; use Quillstack\Logger\Handlers\StorageHandler; use Quillstack\Logger\Logger; $logger = new Logger(new StorageHandler('/var/log/app.log', new LocalStorage())); $logger->info('Order {id} placed by {user}', ['id' => 42, 'user' => 'ada']);
2026-08-23T10:44:17+00:00 info: Order 42 placed by ada {"id":42,"user":"ada"}
The eight methods of PSR-3 are there — emergency(), alert(), critical(), error(),
warning(), notice(), info(), debug() — and log() takes the level as an argument.
What is passed alongside goes into the message
A placeholder written {id} is replaced by what the context holds under id, which is what
PSR-3 §1.2 describes and what every example of using a logger shows:
$logger->error('Payment failed for {id} after {attempts} tries', ['id' => 42, 'attempts' => 3]); // Payment failed for 42 after 3 tries
The context is still handed to the handler, because it is the part something can read rather
than someone. A placeholder nothing answers is left as it was: a message reading Order placed
says less than one showing what was expected.
How much is worth writing
$logger = new Logger($handler, LogLevel::WARNING); $logger->debug('not written'); $logger->warning('written');
Anything below the minimum is not passed on, which is how debug is turned off on a server
without taking the calls out of the code. setMinimumLevel() changes it later.
A level which is not one of the eight is refused with Psr\Log\InvalidArgumentException — the
one the specification names — rather than written as a level nothing can ever filter on.
Where the entries go
$logger ->setHandler(new StorageHandler('/var/log/app.log', new LocalStorage())) ->addHandler(new ConsoleHandler());
setHandler() replaces whatever was there; addHandler() puts one beside it, so a file and a
terminal both get the entry.
| Handler | Writes |
|---|---|
Handlers\StorageHandler |
through any storage — a file, or wherever else one is implemented |
Handlers\ConsoleHandler |
where a person watching can see it, coloured by level |
A handler is one method, so somewhere else to write is one class:
use Quillstack\Logger\HandlerInterface; final class SyslogHandler implements HandlerInterface { public function log(mixed $level, Stringable|string $message, array $context = []): bool { return syslog($this->priority($level), (string) $message); } }
In an application
The framework asks the container for Psr\Log\LoggerInterface, so pointing that at a logger
is all it takes:
$app = new App(__DIR__ . '/../.env', [ LoggerInterface::class => $logger, ]);
Anything asking for a logger then has one, and the error middleware writes to it.
Technical documentation
| Class | What it is |
|---|---|
Logger |
the logger, implementing Psr\Log\LoggerInterface |
Level |
the eight levels and how they rank; SEVERITY, name(), reaches() |
Message |
puts the context into the message |
HandlerInterface |
log(mixed $level, Stringable|string $message, array $context = []): bool |
Exceptions\UnknownLevelException |
a Psr\Log\InvalidArgumentException |
Exceptions\HandlerNotSetException |
there is nowhere to write |
Unit tests
composer test
composer test:coverage
composer stan
Docker
docker-compose up -d
docker exec -w /var/www/html -it quillstack_logger sh
License
MIT. See LICENSE.