Log the Shape of an Enquiry, Not Its Words
When a contact form or enquiry API misbehaves, the fastest debugging aid is a log of the payload. It is also the easiest way to copy names, phone numbers and free text into a system that more people can read than the enquiry inbox. This article proposes a design where the log records the shape of an enquiry and not its content. Everything here is a recommendation to adapt, not a description of anything already built.
The primary artifact: an allowlist
The rule is that a field reaches the log only if it is named on a list. Unknown fields are dropped, never passed through. A denylist fails silently when someone adds a new field next month. An allowlist fails safe, because the new field is simply absent until someone decides it belongs.
Each allowlisted field has one of three treatments:
- Pass: copied as is. Only for values that carry no personal content, such as an identifier you generated, a timestamp, a form type, or validation error codes.
- Derive: replaced by a fact about the value, such as a present or absent flag, a length, or a count.
- Drop: not logged at all. This is the default.
Worked example (hypothetical)
An enquiry arrives with 10 fields: enquiry_id, received_at, form_type, name, email, phone, message, attachment_names, referrer_url, validation_errors.
The allowlist applies as follows.
- Pass (4): enquiry_id, received_at, form_type, validation_errors (codes only, such as EMAIL_FORMAT).
- Derive (3 sources, 5 outputs): name becomes name_present, email becomes email_present, phone becomes phone_present, message becomes message_length, attachment_names becomes attachment_count.
- Drop (1): referrer_url, because query strings in a URL may carry tokens or personal values.
Check the counts: 4 passed plus 5 derived gives 9 output keys. The 10 inputs split into 4 passed, 5 derived sources and 1 dropped, which is 10. Wait, the derived sources are name, email, phone, message and attachment_names, so that is 5 sources, not 3. The correct split is 4 passed, 5 derived sources and 1 dropped, totalling 10.
The redacted diagnostic fixture for this enquiry is then:
enquiry_id: E-1001, received_at: 2026-11-02T09:15:00Z, form_type: quote_request, validation_errors: [EMAIL_FORMAT], name_present: true, email_present: true, phone_present: false, message_length: 214, attachment_count: 1, dropped_unknown_fields: 0
That is 10 keys, 9 above plus the dropped_unknown_fields counter, which tells you the allowlist is working without revealing what was dropped. If a form later gains a field called company, the counter reads 1 and nobody has to guess.
The fixture is useful: it shows an invalid email, a missing phone and a message of known length, which is usually enough to reproduce a validation bug.
Layered constraints
Treat the allowlist as one layer among several, not a guarantee. Other layers worth considering: apply redaction at the point where the log record is built, not in a later cleanup job; keep error messages from libraries out of logs when they might echo input; restrict who can read diagnostic logs; and keep a separate, access-controlled path for the rare case where raw content is truly needed.
Stop conditions
Stop and reassess the design if any of these appear:
- Someone proposes passing the message body 'just for now'. Derive its length instead.
- A derived field starts to reveal content, for example a hash of an email that could be matched against a known list. Prefer a boolean.
- The dropped_unknown_fields counter is nonzero in production for a form you thought you knew. Review the form before changing the allowlist.
Acceptance tests (proposed, not performed)
- Feed the 10 field sample above and compare the output with the fixture key for key.
- Add an eleventh unknown field containing a recognisable marker string. Expect it absent from output and the counter equal to 1.
- Search all log output produced by the test run for the marker strings placed in name, email, phone and message. Expect no matches.
- Send a payload with a malformed shape, such as message as a list. Expect a record with an error code and no echoed content.
- Remove the allowlist file. Expect the logger to emit only identifier and timestamp, not everything.
The last test matters most: a missing configuration should degrade toward logging less.