Logger Architecture
The Logger package follows a short resolution pipeline:
- resolve a logger through
LoggerFactory - load logging config on first use when needed
- set the process-wide severity threshold in
LoggerConfig - reuse or create the adapter-specific
Logger - forward log calls to the adapter when the threshold check passes
Factory lifecycle
LoggerFactory::get() registers the factory in DI on first use and then resolves it from the container.
That gives the package a shared factory instance for the life of the container. The factory keeps an in-memory cache:
private array $instances = [];The practical effect is simple:
- the first call for an adapter builds the logger
- later calls for the same adapter reuse the same
Loggerobject
Config loading behavior
LoggerFactory::resolve() imports the logging config only when config()->has('logging') is false.
In practice that first access happens during boot: both the web and the console boot resolve a logger for the error handler (SetupErrorHandlerStage), so the config is loaded by the time application code runs.
After that, the factory reads:
logging.defaultfor the default adapter namelogging.<adapter>.levelfor the severity thresholdlogging.<adapter>for adapter-specific constructor parameterslogging.message.limitfor the number of messages themessageadapter keeps
Debug-mode override
Debug mode changes the package behavior in two ways.
The web app logs to message
While you work on a page in debug mode, you want its log lines in the debug toolbar, not in a file you have to open. So when is_debug_mode() is true and the running app is the web app (is_web_app()), the factory ignores the requested adapter and the configured default and resolves message instead.
A console command in debug mode has no toolbar to show its messages, so it keeps the requested adapter or the configured default and writes to the log file:
// inside a console command, in debug mode
info('Import finished'); // written by the default adapter, for example `single` to logs/app.logAsking for message explicitly with logger(LoggerType::MESSAGE) still works in the console in debug mode.
Threshold filtering is bypassed
In debug mode, Logger reports every message regardless of the configured threshold.
So debug mode is not just a different backend. It is also a different filtering policy.
Shared threshold state
LoggerConfig stores the active threshold in a static property.
That means each LoggerFactory::resolve() call can update the process-wide threshold before returning a logger. If you resolve different adapters with different configured levels, the most recently resolved one sets the threshold used by later Logger::log() calls.
This matters because the threshold is not stored per logger instance.
Message formatting pipeline
File adapters use a shared formatter that:
- prepends a timestamp and capitalized level
- JSON-encodes array messages
- appends
context['trace']when present - appends the final string to the target file
All other context keys are ignored by file adapters.
Message adapter flow
MessageAdapter appends each reported message to its in-memory list. It does not know about the debugger: the debug toolbar reads the list through logger() when it renders. See the message adapter.
