apache/log4php
Apache log4php is a flexible logging framework for PHP modeled on log4j. It provides configurable loggers, appenders, layouts, filters, and log levels, with XML, INI, or PHP configuration for routing messages to files, databases, and other destinations.
Log4php is a port of Log4j: loggers, levels, appenders, and layouts configured from a file rather than from code. In a Laravel app, Monolog already covers this ground, so treat log4php as a legacy-integration or migration tool, not a default choice.
Minimal setup:
composer require apache/log4php
Create a config file (XML is the canonical format) at config/log4php.xml:
<log4php:configuration xmlns:log4php="http://logging.apache.org/log4php/">
<appender name="rolling" class="LoggerAppenderRollingFile">
<param name="file" value="/var/www/storage/logs/app.log" />
<param name="maxFileSize" value="10MB" />
<param name="maxBackupIndex" value="5" />
<layout class="LoggerLayoutPattern">
<param name="conversionPattern" value="%d{Y-m-d H:i:s} %-5p %c %m%n" />
</layout>
</appender>
<rootLogger level="INFO">
<appender_ref ref="rolling" />
</rootLogger>
<logger name="payments" additivity="false">
<level value="DEBUG" />
<appender_ref ref="rolling" />
</logger>
</log4php:configuration>
Bootstrap it once in a service provider (or AppServiceProvider::boot):
use Logger;
public function boot(): void
{
Logger::configure(base_path('config/log4php.xml'));
}
First use case: an existing module or vendor package already calls Logger::getLogger('...'), or you need log4j-style XML operational config (rolling files, per-package levels) that ops already knows how to edit. For brand-new application logging, use Laravel's Log facade and stop here.
First things to look at in the repo: Logger::configure() entry point, LoggerAppenderFile/RollingFile/DailyFile, LoggerLayoutPattern conversion specifiers, and the LoggerLevel constants. The docs are on the Apache logging site, not in the Laravel ecosystem.
Bootstrap once, per process. Wrap configuration in a singleton/provider and guard it:
$this->app->singleton('log4php', function () {
Logger::configure(base_path(config('logging.log4php.config')));
return Logger::getRootLogger();
});
Do not call Logger::configure() on every request or per log call; it re-parses the file each time. Long-running workers (queue, Octane, Sail) parse once at worker boot.
Environment-specific config. Pick the XML file from app()->environment() (log4php.local.xml, log4php.production.xml) and point the file appender at storage_path('logs/...'). Pass paths via config, not hardcoded XML, if you need per-deployment values — generate or template the XML, or use the PHP-array config format which allows env()-style lookup.
Config formats. XML, INI, properties, and a PHP file that returns an array are supported (Logger::configure() accepts the path). The PHP format is the easiest to keep inside Laravel's config system; keep it side-effect free and return the array.
Logger hierarchy. Use dotted logger names (payments.gateway, payments.refund) and set levels at the package level. additivity="false" is how you stop child loggers from also writing to the root appender. Root level should be INFO or WARN in production, DEBUG locally.
Appenders map to Laravel concerns:
LoggerAppenderDailyFile / RollingFile — storage/logs, rotation by date or size.LoggerAppenderConsole with php://stdout — Docker/Sail/docker logs.LoggerAppenderSyslog — containers with a log collector.LoggerAppenderPDO / LoggerAppenderMail — rarely worth it; buffered appenders need explicit flushing.LoggerAppenderNull — silencing noisy loggers or tests.Bridge into Laravel's logging stack instead of letting two logging systems diverge. Two options:
config/logging.php and Log::extend().Logger::getLogger() injected where needed, so application code calls one API (Log::info()); only the bridge talks log4php.If legacy code expects PSR-3, write a small PSR-3 adapter that delegates to Logger::getLogger() rather than teaching callers two APIs.
Context data. Use LoggerMDC::put('request_id', $id) and clear it at the end of the request/job; LoggerNDC::push()/pop() for nesting. These are static, so reset them in terminate() or job finally blocks to avoid context bleeding across queue jobs.
Custom extension points. Subclass LoggerAppender (override append()/close()), LoggerLayout (override format()), LoggerFilter (override decide()), or LoggerRenderer for object-to-string mapping. Register custom classes by fully qualified class name in the appender's class attribute — log4php instantiates them reflectively, so constructors must be no-arg.
Testing. Call Logger::resetConfiguration() in tearDown, and point tests at a LoggerAppenderNull or a temp file. Assert on file contents rather than mocking the static Logger API.
Logger, LoggerAppenderFile, …) with a legacy autoloader. Composer autoloading generally works, but any manual Logger::configure() before vendor/autoload.php loads will fatal. Name collisions with other global-namespace packages are possible.public/ for web requests in a typical Laravel/FPM setup — not the app root. Always use base_path()/storage_path().additivity on parent loggers. Also check that Logger::getLogger('payments.gateway') is matching the configured payments logger name.report()/render() pipeline or automatic stack-trace formatting. Log $e->getMessage() plus $e->getTraceAsString() explicitly, or verify the pattern layout's throwable support in your version before relying on it.%d{...}, %p, %c, %m, %n — do not paste Monolog format strings. Malformed conversion patterns fail quietly, producing unexpected output.Logger::configure() throws LoggerException for bad XML, missing appenders, or unwritable paths. A bad config at boot will 500 the whole app — wrap it and fail soft with a Monolog fallback rather than dying on logging configuration.Logger::shutdown(); rely on Laravel's terminating callback or call shutdown() explicitly. File appenders should be close()d at process end.'driver' => 'log4php' to config/logging.php without writing a bridge. This means two logging stacks, two sets of config, two retention policies — a real operational cost.TRACE, attach LoggerAppenderConsole to php://stdout, and run php artisan tinker to confirm configuration and logger names resolve. Check file permissions (storage/logs must be writable by the FPM user and queue worker user alike).Log channel for everything new, and plan to delete log4php once the last legacy call sites are converted. Do not introduce it into new Laravel packages.Bottom line: low stars, zero dependents, archived. Its only defensible day-to-day role in a Laravel codebase is honoring existing log4j-style configuration during a migration window. Everything it does for a typical app — rolling files, JSON/line layouts, level filtering, context — Monolog does with maintained code and native Laravel integration.
How can I help you explore Laravel packages today?