lsr / logging
Laser framework core - Logging
Requires
- php: >=8.4
- ext-zip: *
- dibi/dibi: ^5
- lsr/helpers: ^0.3
- psr/clock: ^1.0
- psr/log: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- nette/di: ^3.2
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^12
- roave/security-advisories: dev-latest
Suggests
- nette/di: Required for LoggerExtension and the services.neon compatibility wrapper (^3.2).
Provides
None
Conflicts
- nette/di: <3.2
Replaces
None
README
lsr/logging provides a PSR-3 logger with composable storage, configurable formatting, exception/database-event helpers and log archiving. Its namespace is Lsr\Logging; the repository directory is named lsr-logger, but the Composer package is lsr/logging.
Requirements
- PHP
>=8.4and thezipextension. - PSR Log (
*), PSR Clock (^1.0), LSR helpers^0.3and Dibi^5; see composer.json. - A writable log directory when using the supplied file storage implementations. Storage creates missing directories and raises filesystem exceptions when it cannot write.
- Optional Nette DI
^3.2forLoggerExtensionand services.neon; direct logger construction does not require a DI container or OpenTelemetry.
Installation
composer require lsr/logging
Daily file logging
require __DIR__ . '/vendor/autoload.php'; use Lsr\Logging\Logger; $logger = new Logger(__DIR__ . '/var/log', 'application'); $logger->info('Application started', ['environment' => 'development']);
The default storage uses the legacy text formatter and writes files named application-YYYY-MM-DD.log. The date is resolved when writing, so a long-lived logger moves to the next day's file. PSR-3 level methods such as info(), warning() and error() are available through Logger.
Logger::exception($throwable) writes an error message and a debug trace. logDb($event) accepts a Dibi event. These helpers can include sensitive error/query details, so choose access controls and retention suitable for your application.
Choosing storage and format
Use LoggerFactory to select a formatter without assembling the storage pipeline yourself:
use Lsr\Logging\Formatter\JsonLogFormatter; use Lsr\Logging\LoggerFactory; $logger = (new LoggerFactory())->createDaily( __DIR__ . '/var/log', 'application-json', new JsonLogFormatter(), ); $logger->warning('Import rejected', ['recordId' => 42]);
JsonLogFormatter emits JSON with a timestamp, severity, message and optional context. Other supplied formatters include legacy text, LSR and syslog formats. A syslog formatter formats a record; it is not a network syslog transport.
Storage choices include SimpleFileStorage, DailyLogStorage and RotatingFileStorage. LoggerFactory::createRotating() creates size-based rotating file storage; its maxFileSize argument is in bytes. Rotation trims the oldest complete records within the same file, not numbered backups; an oversized newest record is kept whole. Custom implementations can use StorageInterface and LogFormatterInterface.
Stacks, levels and filters
StackStorage sends a record to every child in order, including other stacks. Its destination list is fixed at construction. It attempts every child before throwing StackStorageException; the exception's exceptions list retains ordered failures, including nested aggregates. Set ignoreExceptions: true on a stack for best-effort delivery. Nested stacks retain their own failure policies. This handles synchronous storage failures, not later background exporter failures.
FilteredStorage decorates any destination, including an entire stack. Its minimum level accepts a PSR-3 severity string or LogLevel; filtering runs after the threshold check. An optional callable or invokable LogFilterInterface receives a LogRecord and returns a replacement record, or null to drop it.
use Lsr\Logging\Filter\ContextBlacklistFilter; use Lsr\Logging\Formatter\JsonLogFormatter; use Lsr\Logging\Logger; use Lsr\Logging\Storage\FilteredStorage; use Lsr\Logging\Storage\SimpleFileStorage; use Lsr\Logging\Storage\StackStorage; $logger = new Logger(__DIR__ . '/var/log', 'application', new StackStorage([ new SimpleFileStorage(__DIR__ . '/var/log/all.log', new JsonLogFormatter()), new FilteredStorage( new SimpleFileStorage(__DIR__ . '/var/log/warnings.log', new JsonLogFormatter()), level: 'warning', filter: new ContextBlacklistFilter(['password', 'token']), ), ]));
Everything is allowed by default. Filters receive detached, normalized context so changes cannot affect sibling destinations or caller-owned objects. Normalization converts objects/exceptions to arrays and bounds recursive/deep values; filters do not receive the original objects. The blacklist removes exact, case-sensitive keys recursively. It does not redact text embedded in messages or string values.
LogRecord contains level, message, context and optional loggerName. Logger supplies its existing $fileName as the name. Storage implementations that need this metadata implement RecordStorageInterface; call LogRecord::storeTo() when forwarding to a child. Legacy StorageInterface::store() remains supported and receives no extra identity field in its context. Custom composites expose children through CompositeStorageInterface for integration discovery.
Logger::addStorage() composes its current destination with another stack destination. Existing direct-file logger output is unchanged. Record-aware pipelines reject unknown severities with Psr\Log\InvalidArgumentException; legacy standalone storage still accepts custom string levels.
Framework configuration
LoggerExtension owns all logger, clock, normalization, formatter, serializer, factory and archiver registrations. Configure named logger instances and reusable storages using native Nette constructor statements (FQNs) or @service references, not driver-name aliases:
extensions: logging: Lsr\Logging\DI\LoggerExtension logging: dir: '%constants.appDir%logs' default: @logging.loggers.app storages: local: Lsr\Logging\Storage\RotatingFileStorage( '%constants.appDir%logs/application.log', @loggerJsonFormatter, 5242880 ) stack: Lsr\Logging\Storage\StackStorage([@logging.storages.local]) loggers: app: storage: @logging.storages.stack imports: name: result-import storage: @logging.storages.stack
The example registers @logging.loggers.app, @logging.loggers.imports, and the storage services under @logging.storages.*. Only the configured default logger is autowired by type, with @logger retained as its compatibility alias. A logger's name defaults to its configuration key; dir can be overridden per logger. Omitting storage selects legacy daily-file logging. The extension defaults to an app logger when no loggers map is supplied. Constructor-reference cycles are rejected during compilation.
For existing applications, continue including services.neon instead of registering the extension separately. It now contains only the extension registration, basic configuration and legacy parameter defaults. It preserves logger.dir, logger.name, logger.logLife, the constants.appDir requirement, and application overrides of the logger service. There is no second set of service definitions in that file.
The extension preserves these helper service IDs: fsHelper, logArchiver, loggerClock, loggerContextNormalizer, loggerJsonContextSerializer, loggerSyslogContextSerializer, loggerLegacyFormatter, loggerJsonFormatter, loggerLsrFormatter, loggerSyslogFormatter, and loggerFactory.
For an OTEL destination, install a compatible lsr/otel release and register OtelExtension, then reference @otel.logging.storage in a stack. A FilteredStorage around that reference can redact or restrict only the exported records. OTEL is never a required dependency of this package.
Log archiving is a separate service; constructing or writing through a logger does not schedule retention work. Consult LogArchiver before wiring its operations into the application's scheduler.
PSR-20 clock migration
All formatter, daily storage and factory clock arguments now accept Psr\Clock\ClockInterface. SystemClock implements it, and @loggerClock remains available for DI overrides. This allows standard third-party clocks without an LSR adapter.
The former Lsr\Logging\Interface\ClockInterface was used only internally and has been replaced with PSR-20. Its method remains now(): DateTimeImmutable. No consumer migration is required; normal logger construction and default file output are preserved. New custom clocks should implement Psr\Clock\ClockInterface.
Development
CI runs the complete suite on PHP 8.4 and 8.5 without external services. Install the PHP extensions listed in .github/workflows/ci.yml, including zip, then run:
composer install --prefer-dist --no-interaction --no-progress composer cs vendor/bin/phpstan analyse --no-progress vendor/bin/phpunit --no-coverage
Keep proc_open enabled for concurrent-writer tests and run as a non-root user so filesystem permission checks are meaningful. The checkout must be writable; the bootstrap creates tests/logs/ and a read-only test directory within it. CI sets PHP's memory limit to 1 GB, matching composer test; use the same limit locally. composer test additionally enables Xdebug coverage mode, which needs a compatible coverage driver. See phpunit.xml and phpstan.neon.
Run composer cs to check PHP coding style and composer cs:fix (or composer cbf) to apply fixes with PHP CS Fixer. The rules and source paths are defined in .php-cs-fixer.php.
AI coding assistance
See LSR Skills for AI agent skills for working with the LSR framework.
License
Licensed under the MIT License.