01 · How it works, in 5 steps
How does structured logging work?
- 1
Use a JSON logger
Configure your logging library to write one JSON object per line to stdout, with a timestamp, level, service name and message, and let the platform ship stdout to the log store.
- 2
Agree a field schema
Fix the names once —
wamid,phone_number_id,template.name,meta_error.code,trace_id— and use them in every service so queries work across the stack. OpenTelemetry's semantic conventions are a sensible base. - 3
Propagate context
Bind request-scoped fields such as the trace ID, tenant and order ID to a child logger at the start of each request or job, so every line inherits them automatically. Reuse the trace ID from your distributed tracing, so a log line opens its matching trace.
- 4
Mask at the source
Redact or hash phone numbers, names, OTP codes and message bodies in the logger configuration, before anything is written. Keep the last few digits if support needs to confirm a customer.
- 5
Log events, not chatter
Emit one line per meaningful event — send attempted, API error, status received, retry scheduled — at the right level, and sample high-volume success lines if cost demands it.
02Try it
Hands-on: use the controls to work through it yourself.
Click through one structured log line
{"ts": "2026-10-19T10:42:07.318+05:30","level": "warn","service": "whatsapp-webhooks","event": "message_status","msg": "Status update: failed","trace_id": "4bf92f3577b34da6a3ce929d0e0e4736","order_id": "ORD-58213","phone_number_id": "106540352242922","wamid": "wamid.HBgMOTE5ODEyMzQ1Njc4FQIAEhggNDRCQ0Q1RkM","status": "failed","template": {"name": "delivery_slot","language": "en"},"recipient": "91XXXXXX4821","meta_error": {"code": 131026,"title": "Message undeliverable"},"lag_ms": 1840}
Field 9: lag_ms — How long after Meta's status timestamp the handler processed it: a cheap early signal of webhook backlog.
03What is structured logging?
Structured logging means emitting log events as records with named fields rather than as sentences. Instead of a line such as 'Failed to send order_update to customer: error 131026', the service writes one JSON object per line with level, event, template, phone_number_id, meta_error.code and a masked recipient. Each field can then be filtered, grouped, counted and alerted on without fragile text parsing.
In a WhatsApp integration the most valuable fields are the platform's own identifiers: the wamid (WhatsApp Message ID) returned by every send and repeated in every status webhook, the phone_number_id of the sending number, the template name and language, the Graph API error.code, and the fbtrace_id that Meta support asks for. Add your own order_id or ticket_id and a trace_id that follows the request through queues and workers, and a single search reconstructs everything that happened to one message.
Structure also makes privacy enforceable. Phone numbers, names and message bodies are personal data under India's DPDP Act, and logs get copied, shipped and retained widely, so PII masking in the logger — not in the log viewer — keeps them out of the store entirely. This is operational logging for debugging and alerting; an audit log, which records who did what for accountability, has different completeness and retention rules.
Key takeaways
- Structured logging writes each log event as named fields, usually one JSON object per line, instead of free-form sentences.
- For WhatsApp, log the
wamid,phone_number_id, template name, Meta error code andfbtrace_idas separate fields. - Mask phone numbers and leave message content out before anything reaches the log store: logs hold personal data too.
- A shared
trace_idlinks the API call, the queue job and the status webhook into one searchable story. - Operational logs are for debugging and may be sampled; an audit log records who did what and must be complete.
04 · In code
Structured logging in code
import pino from "pino";const log = pino({base: { service: "whatsapp-sender" },redact: {paths: ["payload.to", "payload.template.components", "recipient", "*.otp"],censor: "[masked]",},});export async function sendTemplate(job) {const jlog = log.child({ trace_id: job.traceId, order_id: job.orderId });const res = await callCloudApi(job.payload); // POST /{PHONE_NUMBER_ID}/messagesif (res.error) {jlog.warn({event: "send_failed",phone_number_id: job.phoneNumberId,template: { name: job.payload.template.name },meta_error: { code: res.error.code, fbtrace_id: res.error.fbtrace_id },recipient_last4: job.payload.to.slice(-4),}, "Cloud API rejected the template send");return;}jlog.info({ event: "send_accepted", wamid: res.messages[0].id }, "Template accepted");}
Values go in fields, the message text stays constant, and redaction runs inside the logger so masked data never reaches stdout.
05 · Why it matters
Why does structured logging matter?
When a customer says 'I never got my OTP', unstructured logs mean grepping text across services and hoping the formats match. With structured logs, one query by order ID or wamid shows the send, the API response, the retry and the status webhook in order, and the same fields drive dashboards and alerts — error 130429 spiking, delivered statuses drying up for one number. Structured logs are also the raw material for SLIs, so the reliability dashboard and the evidence in the logs always agree — and, with metrics and traces, they are one of the three signals that observability is built on.
06 · Mistakes to avoid
Common mistakes with structured logging
Logging full payloads
Dumping whole webhook bodies or send requests puts phone numbers, names and message text in a store many people can read. Log identifiers and outcomes; mask or drop content.
Inconsistent field names
wamidin one service,messageIdin another andmsg_idin a third make cross-service queries impossible. Publish a schema and check it in code review or a linter.Interpolating values into the message
log.info('sent ' + wamid)buries the value in text again. Keep the message constant and put values in fields, so lines group cleanly.No correlation ID
Without a trace ID carried through the API call, the queue and the webhook handler, each service's logs are an island. Generate it at the edge and propagate it everywhere.
Using debug logs as the audit trail
Operational logs get sampled, rotated and dropped under load. Records of who edited a template or exported contacts belong in a separate, complete audit log.
07 · Key facts
Structured logging: key facts and limits
- Format
- One JSON object per line (JSON Lines) on stdout
- Correlation keys
- wamid, phone_number_id, trace_id, order_id
- Error fields
- meta_error.code, error_subcode, fbtrace_id
- Mask before writing
- Phone numbers, names, OTP codes, message bodies
- Trace context standard
- W3C traceparent (trace ID and span ID)
- CERT-In directions (India)
- Covered entities keep ICT system logs for a rolling 180 days
08Example: Tracing a missing delivery update in Ahmedabad
A logistics company in Ahmedabad gets a complaint that a customer never received a delivery-slot message. Support searches the logs for the order ID and finds four lines: the send attempt with template.name: delivery_slot, the API response with a wamid, a sent status a second later and, ninety seconds after that, a failed status with meta_error.code: 131026. The masked recipient ending in 4821 matches the customer, and the error code points to a number that cannot receive the message. The team switches that customer to SMS within minutes, without anyone reading raw message content.
09Structured Logging FAQs
What is the difference between structured and unstructured logging?+
What should I log for a WhatsApp API integration?+
Can I log customers' phone numbers?+
Is structured logging the same as an audit log?+
10 · Pro tips
Best practices for structured logging
- Log the FB Trace ID (
fbtrace_id) on every Graph API error — Meta support will ask for it. - Record durations as numbers in milliseconds (
duration_ms) so they can be charted, never as strings. - Set retention deliberately: the CERT-In Directions require covered entities to keep ICT system logs for a rolling 180 days, while privacy law favours keeping personal data no longer than needed.
- Add a unit test that fails if a ten-digit number ever appears in log output.
- Treat logged
wamidvalues as personal data too: the ID is opaque, but its encoded part can carry the recipient's number, so restrict who can read the log store.