Skip to content

Stack traces are never written to log files: Logger\Handler\Base hardcodes LineFormatter with includeStacktraces = false #41068

Description

@lbajsarowicz

Preconditions and environment

  • Magento 2.4-develop @ 3704898cb99 (reproducible on 2.4.7 / 2.4.8 as well)
  • monolog/monolog: ^3.6
  • Affected file: lib/internal/Magento/Framework/Logger/Handler/Base.php:61

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:

// lib/internal/Magento/Framework/Logger/Handler/Base.php:61
$this->setFormatter(new LineFormatter(null, null, true));

LineFormatter's constructor signature is:

public function __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

  1. Log a Throwable the PSR-3 way — the way Logger\Handler\System explicitly asks for (see below):
$this->logger->critical('Order placement failed', ['exception' => $e, 'orderId' => 1234]);
  1. Inspect 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.6 with the exact formatter arguments from Base.php:61:

[2026-01-01T00:00:00+00:00] main.CRITICAL: Order placement failed {"exception":"[object] (RuntimeException(code: 42): Something failed at /app/scratch/fmt.php:8)","orderId":1234} []

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\System already treats context['exception'] as the canonical way to report a Throwable, and routes such records to exception.log:

// lib/internal/Magento/Framework/Logger/Handler/System.php:59
if (isset($record['context']['exception'])) {
    $this->exceptionHandler->handle($record);
    return;
}

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 \n after 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):

  • one line, json_decode() succeeds
  • keys: message,context,level,level_name,channel,datetime,extra
  • 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

  1. 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.

  2. 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.

  3. 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.

  4. 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-patches entries against Logger/Handler/Base.php (see Exception log is missing backtrace #13128 (comment)). exception.log missing #36054 covers adjacent exception.log behaviour. 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:

  1. 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.
  2. 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”.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue: ready for confirmationReported on 2.4.xIndicates original Magento version for the Issue report.Triage: Dev.ExperienceIssue related to Developer Experience and needs help with Triage to Confirm or Reject it

    Type

    No type

    Projects

    Status
    Ready for Confirmation

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions