You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every log channel Magento ships (system.log, exception.log, debug.log, and every custom handler extending Logger\Handler\Base) gets its formatter from a single line:
publicfunction __construct(
?string$format = null,
?string$dateFormat = null,
bool$allowInlineLineBreaks = false,
bool$ignoreEmptyContextAndExtra = false,
bool$includeStacktraces = false// <-- never set by Magento
)
So includeStacktraces is false for the entire application. This is the only place in app/ or lib/ where a Monolog formatter is instantiated — there is no DI hook, no di.xml argument, and no deployment-config switch to change it.
Steps to reproduce
Log a Throwable the PSR-3 way — the way Logger\Handler\System explicitly asks for (see below):
But because the formatter discards traces, a developer who writes the correct PSR-3 call gets less information than one who writes $this->logger->critical($e) — that pattern relies on Throwable::__toString() (Throwable extends Stringable since PHP 8.0) and therefore does emit a full trace, just as an unstructured blob.
Core has been pushed into the wrong pattern by this bug. In current 2.4-develop, 269 call sites stringify the Throwable as the log message and only 9 use ['exception' => $e]. The incentive is backwards: the correct call loses the trace. Fixing the formatter is a precondition for fixing the call sites (filed separately).
Enabling stack traces on LineFormatter alone produces invalid JSON
Base.php:61 also passes $allowInlineLineBreaks = true, which un-escapes \nafter the context has been JSON-encoded. Verified with includeStacktraces = true on top of the current arguments:
one logical record spans 5 physical lines
the context fragment no longer decodes: json_decode() → Control character error, possibly incorrectly encoded
So includeStacktraces = true on LineFormatter recovers the trace for humans but produces log entries that are neither one-line nor valid JSON — unusable for Elastic/Loki/Datadog/CloudWatch ingestion without a multiline-join rule per Magento version.
JsonFormatter gets both right. Same record, new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true, false, true):
context.exception is a structured object: class, message, code, file, trace
trace is an array of "file:line" strings — frame arguments are excluded, so no customer data or credentials from call arguments can leak into the log
Why this matters for observability
No trace means no root cause. A critical entry that names only the throw site is a symptom report. For a throw site reached from twenty different code paths — Framework/DB/Adapter/Pdo/Mysql.php, View/Element/AbstractBlock.php, any interceptor — the log identifies the victim, never the cause. Diagnosis falls back to reproducing in a debugger, which for intermittent production failures often means it is never diagnosed.
No trace means no grouping. Every error-tracking system (Sentry, New Relic, Elastic APM, Datadog) fingerprints an error by its trace. With no trace, all occurrences of a shared throw site collapse into one bucket, and genuinely distinct bugs become indistinguishable. Frequency data — the input to "which bug do we fix first" — is lost.
Not machine-readable means not alertable. With entries spanning an unpredictable number of lines and a context payload that fails JSON parsing, an ingestion pipeline cannot reliably split records, so it cannot count them, so it cannot alert on a rate change. Merchants get error visibility only when someone SSHs in and reads a file.
Two parts, both backward-compatible if the second is opt-in:
Make the formatter configurable instead of hardcoded — inject a FormatterInterface into Logger\Handler\Base with the current LineFormatter as the DI default. This alone resolves Cannot set custom filename for \Psr\Log\LoggerInterface #36083 and lets merchants opt into JsonFormatter without patching core.
Enable includeStacktraces on the default formatter. Because this changes the shape of existing log files, it should be switchable — a deployment_config flag (e.g. log/formatter + log/include_stacktraces) or an argument in di.xml, so merchants who parse logs today are not broken by an upgrade.
Ideally ship a JsonFormatter-based handler as a documented opt-in: one line per record, valid JSON, structured trace, no frame arguments.
I have a PR ready and will link it here.
Release note
Stack traces are no longer omitted from log files, and the log formatter can be replaced without patching Magento\Framework\Logger\Handler\Base.
Triage and priority
Severity: S0- Affects critical data or functionality and leaves users without workaround.
Severity: S1- Affects critical data or functionality and forces users to employ a workaround.
Severity: S2- Affects non-critical data or functionality and forces users to employ a workaround.
Severity: S3- Affects non-critical data or functionality and does not force users to employ a workaround.
Severity: S4- Affects aesthetics, professional look and feel, “quality” or “usability”.
Preconditions and environment
2.4-develop@3704898cb99(reproducible on 2.4.7 / 2.4.8 as well)monolog/monolog: ^3.6lib/internal/Magento/Framework/Logger/Handler/Base.php:61Every log channel Magento ships (
system.log,exception.log,debug.log, and every custom handler extendingLogger\Handler\Base) gets its formatter from a single line:LineFormatter's constructor signature is:So
includeStacktracesisfalsefor the entire application. This is the only place inapp/orlib/where a Monolog formatter is instantiated — there is no DI hook, nodi.xmlargument, and no deployment-config switch to change it.Steps to reproduce
Throwablethe PSR-3 way — the wayLogger\Handler\Systemexplicitly asks for (see below):var/log/exception.log.Expected result
The entry contains the stack trace, so the origin of the failure can be identified.
Actual result
One frame — the throw site — and nothing else. Verified against the shipped
monolog/monolog ^3.6with the exact formatter arguments fromBase.php:61:There is no
[stacktrace]block. Whatever called the failing code is unrecoverable from the log.Additional information
This makes the PSR-3 pattern a downgrade, which is why nobody uses it
Logger\Handler\Systemalready treatscontext['exception']as the canonical way to report aThrowable, and routes such records toexception.log:But because the formatter discards traces, a developer who writes the correct PSR-3 call gets less information than one who writes
$this->logger->critical($e)— that pattern relies onThrowable::__toString()(Throwable extends Stringablesince PHP 8.0) and therefore does emit a full trace, just as an unstructured blob.Core has been pushed into the wrong pattern by this bug. In current
2.4-develop, 269 call sites stringify theThrowableas the log message and only 9 use['exception' => $e]. The incentive is backwards: the correct call loses the trace. Fixing the formatter is a precondition for fixing the call sites (filed separately).Enabling stack traces on
LineFormatteralone produces invalid JSONBase.php:61also passes$allowInlineLineBreaks = true, which un-escapes\nafter the context has been JSON-encoded. Verified withincludeStacktraces = trueon top of the current arguments:json_decode()→Control character error, possibly incorrectly encodedSo
includeStacktraces = trueonLineFormatterrecovers the trace for humans but produces log entries that are neither one-line nor valid JSON — unusable for Elastic/Loki/Datadog/CloudWatch ingestion without a multiline-join rule per Magento version.JsonFormattergets both right. Same record,new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true, false, true):json_decode()succeedsmessage,context,level,level_name,channel,datetime,extracontext.exceptionis a structured object:class,message,code,file,tracetraceis an array of"file:line"strings — frame arguments are excluded, so no customer data or credentials from call arguments can leak into the logWhy this matters for observability
No trace means no root cause. A
criticalentry that names only the throw site is a symptom report. For a throw site reached from twenty different code paths —Framework/DB/Adapter/Pdo/Mysql.php,View/Element/AbstractBlock.php, any interceptor — the log identifies the victim, never the cause. Diagnosis falls back to reproducing in a debugger, which for intermittent production failures often means it is never diagnosed.No trace means no grouping. Every error-tracking system (Sentry, New Relic, Elastic APM, Datadog) fingerprints an error by its trace. With no trace, all occurrences of a shared throw site collapse into one bucket, and genuinely distinct bugs become indistinguishable. Frequency data — the input to "which bug do we fix first" — is lost.
Not machine-readable means not alertable. With entries spanning an unpredictable number of lines and a context payload that fails JSON parsing, an ingestion pipeline cannot reliably split records, so it cannot count them, so it cannot alert on a rate change. Merchants get error visibility only when someone SSHs in and reads a file.
It pushes merchants onto patches. Exception log is missing backtrace #13128 reported this exact symptom in January 2018 and was closed the same day; the thread accumulated years of follow-ups and community
composer-patchesentries againstLogger/Handler/Base.php(see Exception log is missing backtrace #13128 (comment)). exception.log missing #36054 covers adjacentexception.logbehaviour. Every merchant who wants stack traces is maintaining a patch against a framework file.Suggested fix
Two parts, both backward-compatible if the second is opt-in:
FormatterInterfaceintoLogger\Handler\Basewith the currentLineFormatteras the DI default. This alone resolves Cannot set custom filename for \Psr\Log\LoggerInterface #36083 and lets merchants opt intoJsonFormatterwithout patching core.includeStacktraceson the default formatter. Because this changes the shape of existing log files, it should be switchable — adeployment_configflag (e.g.log/formatter+log/include_stacktraces) or an argument indi.xml, so merchants who parse logs today are not broken by an upgrade.Ideally ship a
JsonFormatter-based handler as a documented opt-in: one line per record, valid JSON, structured trace, no frame arguments.I have a PR ready and will link it here.
Release note
Stack traces are no longer omitted from log files, and the log formatter can be replaced without patching
Magento\Framework\Logger\Handler\Base.Triage and priority