Skip to content
System nominalUTC --:--:--Articles 041

What to Log on a Store, and What Has to Stay Out of the Log

Logs, metrics and traces answer different questions. What a WooCommerce store should record, what must never reach a file, and which alerts earn a page.

Tomas Lindqvist · @tlindqvist

Hardening and operations

Topic
INFRA
Section
Hosting & Infrastructure
Published
26 FEB 2026
Reading time
8 MIN
Words
1876W
Technical cover graphic for What to Log on a Store, and What Has to Stay Out of the Log
Generated schematic — INFRA series, REV 2.6.

Checkout starts failing for one card brand at 09:20 on a Tuesday. A customer emails at 11:40 to say the order form loops back on itself, and support passes it to you at 14:00. By then the store has a 40 MB error log full of deprecation notices from a plugin nobody touched, four days of access logs, and nothing whatsoever about the gateway call that returned the decline.

The failure was never invisible. It was unrecorded. The gateway answered, your code read the answer, a customer saw the result, and the only trace of the transaction was a row in the orders table that said the order did not complete.

Two decisions produce that outcome, and they are made separately: what to record, and what to refuse to record. A log file is a copy of your data living somewhere you are not looking, and a store that gets the second decision wrong has built a liability with a retention policy attached.

Logs, metrics and traces answer three different questions

These three get used interchangeably in conversation and they are not interchangeable. Picking the wrong one gives you either a file nobody reads or a graph that cannot explain anything.

A log is a record of a discrete event, written so a person can read it later. You reach for it to learn what happened in one request at 09:21: which gateway, which order, which response code, which plugin version. Logs are bad at rates. Counting lines to estimate a failure percentage gives you a number that depends on how verbose the code was that day.

A metric is a number sampled over time, built to be aggregated and compared. You reach for it to learn whether this hour is normal, and you alert on it. Metrics are bad at explaining: a dashboard that says checkout failures tripled tells you nothing about which of the five things in the checkout path broke.

A trace follows one request across the hops it touches: WordPress, your plugin, the gateway, the webhook that comes back, the ERP that picks the order up. You reach for it when the question is which hop spent the time, and on a store with one PHP application and three external calls, that is the question logs cannot answer.

SignalQuestion it answersRetention to plan for
LogsWhat happened in one request, at one moment, in detail14 to 30 days, rotated and compressed
MetricsIs this rate normal for this hour, and which way is it moving13 months, at reduced resolution after the first week
TracesWhich hop of a multi-service request spent the timeHours to days, sampled rather than complete

Four things on a WooCommerce store are worth writing down

Most of what a store logs is noise, and the noise is what makes the useful lines hard to find. Four categories carry nearly all the diagnostic value.

Deploys and version changes. One line per deploy, with the core version, the WooCommerce version and the plugin versions that changed. When a failure starts, the first useful question is what changed, and a deploy line answers it from the inside. Server-level deploys belong in the same timeline as the application logs, or you correlate two systems by hand while a customer waits.

Payment gateway call outcomes, with a correlation identifier. Record the gateway name, order ID, HTTP status, the gateway's error code, the round-trip latency, and an identifier you generate, send with the request, and can quote to the gateway's support team. Record the outcome, not the payload: a response body can carry a partial card number.

function myplugin_log_gateway_call( $order_id, $gateway, $response ) {
    $correlation = wp_generate_uuid4();

    // Send $correlation as a header on the outbound request so the gateway
    // can find the same call from their side.

    error_log( wp_json_encode( array(
        'event'       => 'gateway.call',
        'correlation' => $correlation,
        'order_id'    => (int) $order_id,
        'gateway'     => $gateway,
        'http_status' => (int) $response['status'],
        'code'        => $response['code'],
        'latency_ms'  => (int) $response['latency_ms'],
        'plugin'      => MYPLUGIN_VERSION, // defined by the plugin itself.
    ) ) );
}

The structured shape matters more than the destination. A line of JSON filters by order_id or code with one grep, and it does not depend on a message string staying identical across releases.

Queue and cron failures. A scheduled action that fails once is a warning; one that fails on every run is a stalled integration nobody notices until a customer asks where their tracking number is. Action Scheduler keeps its own record of failed actions, and that record is worth reading before you add anything. What the application log needs is a line naming the action, the hook and the exception, so the failure is visible without opening an admin screen.

Authentication failures. Failed logins through wp_login_failed, rejected REST calls, webhook endpoints returning 401. Individually these are uninteresting. As a rate they separate a customer who mistyped a password from somebody working through a credential list, and a rate needs the events to exist first.

What must never reach a log file

The list is short and absolute. Full card numbers, expiration dates and security codes. Passwords and password reset tokens. Session tokens, authentication cookies, API keys and authorization headers. Complete customer records: billing and shipping addresses, phone numbers, the bodies of transactional emails, and raw gateway webhook payloads, which often contain most of the above as fields you never chose.

The reason to be strict is that a log file does not stay where you wrote it. It gets rotated into an archive, copied by a backup job, shipped to a hosted aggregator, read by a support engineer, and pasted into a ticket. Each is a new copy of personal data in a new place with a new access list. A log you keep for ninety days is data you are storing for ninety days, whether or not you meant to.

This is why a debug log left enabled on production for a week is a data-protection incident waiting to be reported rather than a harmless oversight. Temporary debugging is when people log the most: they dump the request body, the full order object and the gateway response, because it is the fastest way to see what is wrong. The dump is also the whole customer record. With no end date, nobody turns it off, and the file becomes a customer database without indexes, permissions or a retention rule.

A workable default

Logging off by default on production, enabled for a specific window with an end date recorded in the change ticket, and reviewed before it is closed. WordPress ships WP_DEBUG_LOG, which writes to a file inside the web root on a default install. Confirm that path is not reachable over HTTP before you rely on it.

Alert on what a customer would notice, not on what PHP complained about

An alert should fire on a symptom a customer experiences, not on a cause that might produce one. Checkout failure rate. Order creation errors. Gateway 5xx responses as a share of attempts. The p95 response time of the checkout POST. Queue depth and the age of the oldest pending action. Those are things a customer feels.

The contrast is concrete. A plugin that emits a PHP warning on every front-end request produces roughly forty thousand lines a day on a store doing a few orders an hour. An alert on that warning fires forty thousand times and gets muted within a week. A checkout failure rate crossing two percent for five minutes means customers are losing money right now, and it is worth waking somebody up.

Every alert needs an owner and an action. If the documented response is to acknowledge it because it always does that, the alert is teaching everyone to ignore the next one. That habit is the real cost of a noisy alerting setup, and it is not recoverable by adding more alerts. Delete the alert, or fix the cause so the alert stops firing. Track how many alerts in the last month led to a change, and treat a low number as a defect in the alerts rather than a sign that the system is calm.

Set thresholds on a rate over a window, not on a single occurrence. One failed card payment is normal trading; a hundred in ten minutes is not. Compare against the same hour last week where you can, because a store's baseline moves with its traffic and a fixed number is too sensitive on a quiet morning and too slow on a sale day.

Retention and rotation, so a log cannot fill a disk

A log file with no rotation grows until the filesystem is full. On a single server that takes MySQL, PHP session files and the application down with it, which converts a logging problem into a total outage at whatever hour the disk fills. Rotation is not housekeeping; it keeps the store online.

Configure rotation by size and count, compress the rotated files, and confirm the compressed copies keep the same restrictive permissions as the original. Then set retention per stream from the table above rather than one number for everything. Traces are sampled and short lived. Metrics stay a year because trends need a year. Application logs sit in between, and access logs can be shorter unless something is investigating an incident.

Deletion is part of retention. A log archive kept indefinitely in a bucket is personal data you are still holding, and the retention rule you wrote is the only thing making that defensible.

You cannot alert on what you never instrumented

This is the limit that makes the rest of it follow-up work rather than a setup task: you cannot add a metric to an event that has already passed. The first failure of a new integration is diagnosed by hand, from whatever the gateway dashboard shows and whatever a customer can describe, because the store had no measurement of that call before it broke.

That is not a failure of planning. A new courier integration has no baseline and no historical rate, so there is no way to know which of its responses are normal. Instrumentation is the follow-up work, and the follow-up is what changes the second incident. A review that ends with a code fix and no change to what the store records leaves the next person diagnosing the same class of failure by hand.

Instrumentation also does not fix anything. A store with two hundred dashboards and no agreed baseline is worse off than one with four alerts that somebody owns, because the dashboards produce confident-looking screens and no decisions.

What to do on Monday

  1. Turn off debug logging on production, and if it is on, grep the existing file for Authorization, password fields and long digit sequences before you archive it.
  2. Add the four log lines above, each as structured output with a correlation identifier that the gateway also receives.
  3. Write down retention per stream, then configure rotation by size and count. Check the permissions on the rotated copies.
  4. Replace one cause-based alert with a symptom-based one, and delete any alert nobody has acted on in the last month.
  5. After the next incident, add the measurement that would have answered the question, before the ticket is closed.

Tomas Lindqvist · @tlindqvist

Hardening and operations

Threat models plugins, audits capability and nonce handling, and builds the observability that tells you an integration broke before a customer does.

Continue

Related in Hosting & Infrastructure