Log the decision, not just the outcome, or you cannot debug it
The claim When something in your application behaves unexpectedly — a customer was charged the wrong amount, an order was routed to the wrong warehouse, a discount was or was not a...
The claim
When something in your application behaves unexpectedly — a customer was charged the wrong amount, an order was routed to the wrong warehouse, a discount was or was not applied — the logs will usually tell you what happened but not why the system decided to do it. The outcome is recorded; the reasoning that produced it is not. Logging the inputs and the decision path, not just the final result, is the difference between diagnosing a problem in minutes and being unable to explain it at all, and it costs a few extra log lines placed where the decisions are made.
The gap between what and why
Most logging records outcomes: "order 8812 shipped from warehouse B", "customer charged $47.20", "discount not applied". These tell you what the system did, and when the outcome is correct that is all you need. But when the outcome is wrong, the outcome log is nearly useless, because it tells you the conclusion without any of the reasoning that led there. Why warehouse B and not A? Why $47.20 and not $42.00? Why no discount? The decision that produced the outcome — the inputs it considered and the rule it applied — happened invisibly, and without a record of it you are left guessing at logic you cannot see.
This is the difference between a log that lets you confirm the system worked and a log that lets you understand why it did what it did. The first is enough until something goes wrong; the second is what you need precisely when something has.
Log the inputs to the decision
The first thing to capture is what the code knew at the moment it decided. A routing decision, a pricing calculation, an eligibility check — each takes inputs, and those inputs are what you need to reconstruct why the decision went the way it did:
// not just the outcome, but what fed it:
log.info("routing decision", {
order_id: order.id,
ship_to_region: order.region,
warehouse_stock: { A: 0, B: 14 }, // the inputs
chosen: "B",
reason: "A out of stock for region", // the why
});
Now, when someone asks why order 8812 shipped from the farther warehouse, the log answers completely: warehouse A had zero stock, so B was chosen. You are not inferring the reasoning from the outcome; you recorded the reasoning as it happened. The inputs and the stated reason turn an inexplicable outcome into an obvious one.
Record which branch was taken and why
Where the code makes a choice between paths, log which path it took and the condition that decided it. This is especially valuable for the decisions that most often produce customer confusion — whether a discount, a fee, a tax, or an exception applied:
if (order.total >= freeShippingThreshold) {
log.info("free shipping applied", { total: order.total, threshold });
// ...
} else {
log.info("shipping charged", { total: order.total, threshold, short_by: threshold - order.total });
}
The second log line is the valuable one: it records not just that shipping was charged but that the order was a specific amount short of the free-shipping threshold. When the customer emails asking why they were charged shipping, the answer is in the log, exact and immediate, rather than requiring someone to reconstruct the threshold logic by hand and hope they get it right.
Carry an identifier through the whole decision
A decision often spans several steps and several components, and to reconstruct it you need to gather all the related log lines. A correlation identifier — a request ID, an order ID — attached to every log line in the chain lets you retrieve the complete story of one decision from across the whole system with a single query. Without it, the decision's log lines are scattered among everyone else's and cannot be assembled; with it, one filter produces the entire reasoning in order. This is the same identifier discipline that makes distributed logs useful, applied specifically so that the full path of a single decision can be pulled back together after the fact.
What not to log
Logging decision inputs runs directly into a privacy constraint, because the inputs to a decision often include exactly the sensitive data you must not scatter through your logs. The card number that determined whether a payment was accepted, the full personal record that fed an eligibility check, the credentials that governed an access decision — these must not appear in the log even though they were inputs to the decision. Log the fact and the non-sensitive shape of the input, not the sensitive value itself: log that the card was declined and the decline code, not the card number; log that the age check passed, not the date of birth. The goal is enough to reconstruct the reasoning, without turning your logs into a second copy of the data you are obliged to protect.
The test to apply
For each important decision your system makes — the ones that, when wrong, generate a customer complaint or an internal investigation — ask whether your current logs would let you explain a specific instance of it after the fact. Could you tell a customer exactly why they were charged what they were, routed where they were, granted or denied what they asked for, using only what is in the logs? If the answer is that you could see the outcome but would have to guess at the reasoning, then you are logging outcomes and not decisions, and the next time one of those decisions is questioned you will be reconstructing logic from the result instead of reading it from the record. Add the inputs and the reason at the point of decision, and the question "why did it do that" stops being an investigation and becomes a lookup.