Building the message with the values in it produces a unique string per event, which means an aggregator sees a million distinct messages instead of one with a million occurrences.
// unaggregatable
$logger->error("Payment failed for order {$id}: declined");
// a constant message, and structured context
$logger->error('payment.failed', [
'order_id' => $id,
'reason' => 'declined',
]);
// PSR-3 also allows placeholders, which are filled by the handler:
$logger->error('Payment failed for order {order_id}', ['order_id' => $id]);
The placeholder form is the compromise that keeps the line readable to a human tailing a file while keeping the values addressable as fields — the handler decides whether to substitute. Context keys must be strings and values should be scalars or objects with a __toString; passing an entire model in serialises whatever it holds, which is how personal data ends up in a log index. Deciding the key names once and writing them down is worth more than any handler configuration.