Skip to content

Bound the debug data kept in memory, log to files from the console, record the app type #593

Closed

Description

@armanist

Summary

Since #539 the debug toolbar reads its tabs at render time from data the packages keep in memory: the debug-mode logger's messages, the mailer's transport log and the database's query log. Nothing bounds them. In a web request the process ends after the response, so they stay small, but a console command or worker in debug mode keeps every log line, every SMTP line and every query for its whole run, and no toolbar ever reads them. Its log lines are never written to a file either.

Bound the logger and the mailer buffers with a limit from each package's own config, keep the console's log lines in the log file, and keep Idiorm's query log off outside debug mode. For that the app context records whether the run is the web app or the console, read through is_web_app() and is_console_app(), and the error handler uses the same fact instead of checking PHP_SAPI.

Current state (checked on master)

Place What it does
LoggerFactory::resolve() in debug mode always resolves the in-memory message adapter, for the web and the console alike, even when a caller asks for a file adapter (CronManager asks for single)
MessageAdapter appends every reported message; nothing removes them
MailerTrait appends to $transportLog the SMTP exchange (through the Debugoutput callback set by SmtpAdapter in debug mode) and the errors of failed API sends; nothing removes them, and the mailer instance lives for the whole process
IdiormDbal::getBaseConfig() passes the raw config()->get('app.debug') to Idiorm's logging option; DEBUG=false in .env arrives as the text "false", which PHP reads as true, so every query is kept even with debug mode off
AppContext holds the base directory and the container; nothing records whether the run is web or console
ErrorHandler::handleException() picks the console or the web output with PHP_SAPI === 'cli', the only such check in src

Proposed change

  1. The app context knows its type. AppContext::__construct(string $baseDir, DiContainer $container, string $type) takes AppType::WEB or AppType::CONSOLE, and getType() returns it. AppFactory passes the type it creates; WebContext and ConsoleContext copy it. Two helpers in src/App/Helpers/app.php, is_web_app() and is_console_app(), read it, so no package compares the type itself.
  2. The error handler uses it. ErrorHandler::handleException() chooses the console output when is_console_app() and the web page otherwise. A request served by php -S (php qt serve) is the web app, as before.
  3. The console logs to files in debug mode. LoggerFactory forces the message adapter only when is_web_app(). A console command in debug mode logs to the configured adapter, still at every level.
  4. The debug-mode logger keeps its last messages. MessageAdapter keeps the last logging.message.limit messages (default 1000) and drops the oldest; 0 means no limit. It trims after each message.
  5. The mailer's transport log keeps its last lines. At the start of each send(), the transport log is cut to its last mailer.transport_log_limit lines (default 1000, 0 means no limit). The start of send() is the one point every send reaches, also after a previous send threw, so the log holds at most the limit plus the lines of the send in progress. Both writers stay as they are.
  6. The query log is off outside debug mode. Idiorm's logging option reads is_debug_mode(), which converts the text "false" from .env to false. In debug mode the web app and the console keep their query log as before, so Database::lastQuery(), queryLog() and the toolbar's queries tab work in both.

Each package reads only its own config key, so the logger and the mailer stay independent of each other.

Out of scope

  • Clearing the buffers per request or per job in long-running processes belongs to the deferred and long-running execution work (Possibility to run defer tasks after response #168).
  • Capping Idiorm's query log. In debug mode it still keeps every query of a long-running command. Idiorm calls its own logging through self:: and keeps the log in a protected static property, so it cannot be capped from outside; that needs Quantum to record the queries itself, as a follow-up.

Behavior changes

Case Today After
logger() in a console command in debug mode in-memory message adapter, lines lost the configured adapter, lines written to the log file
debug-mode logger in a long web request keeps every message keeps the last logging.message.limit messages
mailer transport log keeps every line for the whole process cut to the last mailer.transport_log_limit lines before each send
Idiorm query log with DEBUG=false in .env keeps every query off
an uncaught exception console or web output chosen by PHP_SAPI chosen by the app type; the same result for php qt, php -S and PHP-FPM

The toolbar's tabs read the same sources as before; they change only once a limit is reached.

Breaking changes

  • AppContext::__construct() takes a third, required $type argument. Code that goes through AppFactory is unaffected.

Tests

  • AppContext::getType() returns the given type; AppFactory creates a web and a console context with the right type; WebContext and ConsoleContext keep it.
  • is_web_app() and is_console_app() are each true for their own type and false for the other.
  • ErrorHandler::handleException() writes the console output for a console context and renders the web page for a web context.
  • LoggerFactory in debug mode resolves message for the web app and the configured or requested adapter for the console.
  • MessageAdapter keeps the last limit messages in order, keeps everything with 0, and uses 1000 without the key.
  • The transport log is cut to its last limit lines at the next send, keeps everything with 0, and never cuts the lines of the send in progress.
  • Idiorm's logging option is on in debug mode for the web app and the console, and off outside debug mode, also when debug mode is the text "false".

Acceptance criteria

  • a console command in debug mode writes its log lines to the configured log file
  • the debug-mode logger and the mailer's transport log are bounded by their own config keys
  • Idiorm keeps no query log outside debug mode
  • no PHP_SAPI check is left in src; the web-or-console decision goes through is_web_app() or is_console_app()
  • composer test, composer cs:check, composer phpstan and composer rector:check pass

Notes

Relevant code:

  • src/App/AppContext.php, src/App/Factories/AppFactory.php, src/App/Adapters/Web/WebContext.php, src/App/Adapters/Console/ConsoleContext.php, src/App/Helpers/app.php
  • src/Tracer/ErrorHandler.php
  • src/Logger/Factories/LoggerFactory.php, src/Logger/Adapters/MessageAdapter.php
  • src/Mailer/Traits/MailerTrait.php
  • src/Database/Adapters/Idiorm/IdiormDbal.php

Follows #539.

Activity

  1. added this to the 3.1.0 milestone on Oct 22, 2026
  2. self-assigned this
    on Oct 22, 2026
  3. changed the title [-]Bound the debug-mode buffers and log to files from the console in debug mode[/-] [+]Bound the debug-mode buffers, log to files from the console in debug mode, and record the app type on the context[/+] on Oct 22, 2026
  4. changed the title [-]Bound the debug-mode buffers, log to files from the console in debug mode, and record the app type on the context[/-] [+]One bounded debug log: cap the debug-mode logger, route mailer diagnostics to it, log to files from the console, record the app type[/+] on Oct 22, 2026
  5. changed the title [-]One bounded debug log: cap the debug-mode logger, route mailer diagnostics to it, log to files from the console, record the app type[/-] [+]Bound the debug data kept in memory, log to files from the console, record the app type[/+] on Oct 22, 2026
  6. armanist commented on Oct 23, 2026

    @armanist
    CollaboratorAuthor

    Config values from .env

    The Idiorm fix above has the same cause as three other places: values read from .env arrive as text, and these places use them without converting them. This ticket fixes them too, each at the point of use.

    Place Today After
    MailerTrait::send() tests mailer.mail_trap as is, so MAIL_TRAP=false from .env arrives as the text "false" and still traps every mail reads it with FILTER_VALIDATE_BOOLEAN, like app.debug, resource_cache and auth.two_fa
    SessionAuthAdapter passes remember_lifetime to Cookie::set(..., int $time) in a strict_types file, so REMEMBER_LIFETIME set in .env throws a TypeError on a "remember me" login casts it with (int)
    RedisAdapter, MemcachedAdapter, RedisRateLimitAdapter pass port to the extension as is; Memcached::addServer() takes an int, and REDIS_PORT from .env is text cast it with (int)

    Values written as booleans and integers in the config files behave as before.

    Tests

    • The text "false" for mailer.mail_trap sends through the transport; the text "true" saves to the trap.
    • A "remember me" login with remember_lifetime as the text "2592000" sets the cookie.
    • Each adapter connects with the port given as text.
  7. added 2 commits that reference this issue on Oct 23, 2026
  8. added 6 commits that reference this issue on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

debugbarDebugbar and debugger integration

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions