Structured info and warn logs now reach Datadog with their fields
Shipped 2026-09-11
Twelve logger.info and logger.warn calls across the tunn3l connector and the
push-notification service recorded structured fields that never arrived in
Datadog. What was stored for passcode.sent, for example, was:
{ "message": "passcode.sent",
"attributes": { "date": 1789145375574, "service": "tunnelflight-api" } }No member_id, no channel, no masked recipient — every field the call was
written to capture.
Why
Winston hoists a message key out of the object it is given. So for a call like
logger.info({ message: 'passcode.sent', channel: 'email', member_id: 123 }),
logFormat receives the label as a string and the remaining fields as
separate metadata. The typeof message === 'object' branch never runs, and the
metadata is appended as a second line:
passcode.sent
{"channel":"email","member_id":123,...}Datadog ingests line by line, so the fields end up on a different event from the label they belong to. Searching for them returns nothing at all.
logger.error calls were unaffected, but only by accident: they use an error:
key rather than message:, so winston has nothing to hoist and the object
survives intact and serialises as one line. That is the entire difference between
email.send_failed arriving with every field and passcode.sent arriving with
none.
The fix
Renaming the key to event: keeps the object whole, and the existing info branch
serialises it as a single JSON line with every field attached. No shared logging
code was touched.
One detail worth knowing for anyone repeating this: the rename is anchored on the
logger call, not on the key. In the tunn3l connector the line immediately after a
log call is frequently a genuine API response — return { valid: 0, message: 'Member not found' } — and a broader search-and-replace would have quietly
changed what that connector returns to its callers.
The equivalent ten calls in the passcode service are fixed alongside the work in TUN-867.
Trade-off
With no message key, Datadog’s message column now shows the JSON rather than the
label. Full-text search still finds the event name, and the fields are finally
queryable. The alternative — repairing logFormat so the info and warn branches
merge message and metadata the way the error branch already does — would fix every
caller at once and keep the label, but it is shared code on every log line in the
API. That remains available as a follow-up.