Release Notes
Version 1.2.1
📅 Release Date
August 24, 2026
📖 Overview
1.2.0 gave every line an event and made it say which service wrote it. Production then showed what that cost: 330 log
records from five services, in which nearly half the fields on a line were being paid for twice, and the one column a
person actually reads was blank.
Three things came out of reading them.
The data wrapper was making every useful field a path. data.status, data.ip, data.colo — a dashboard column
named for a nesting level rather than for what it holds, and a query prefix on every filter.
The line had no message. 1.2.0 renamed it to event, which is right for machines and wrong for the one column most
log viewers show by default. Both belong on the line: event for event = request.failed, message for the person
scanning a page of them at two in the morning.
And nothing could be turned off. Which fields a line carries is a property of where it is going — a runtime that stamps the service name on every record does not need a second copy, and one that stamps nothing does. That is a deployment's decision, and until now it took a code change in nine repositories.
🚀 Features
LogFields— decides which context fields reach the line, fromLOG_FIELDSin the environment,logging.fieldson the configuration surface, or the kernel default.{ "vars": { "LOG_FIELDS": "requestId,method,path,ip,status" } }*admits every field. An empty declaration admits none, which is deliberately not the same as leaving it unset:LOG_FIELDS=""is a deployment saying the level, the event and the message are the whole line, and it must not fall through to the surface.The kernel default is
*. Narrowing means knowing what the runtime already records, which is an adapter's knowledge, not the kernel's —@bayudwiyansatria/cloudflaresupplies its own list, and a kernel-only consumer sees no field disappear.An optional sentence on every log call.
debug,info,warnanderrortake a third argument:log.info('notification.sent', { provider }, `Notification sent via ${provider}`)Omit it and
messagefalls back to the event name, so the column is never empty. Existing call sites are unaffected.
⚠️ Breaking Changes
- The payload is flattened onto the line.
logger.info('article.created', { id: 7 })emits{ …, "id": 7 }, not{ …, "data": { "id": 7 } }. Anything readingdata.x— a saved query, a dashboard column, a Logpush consumer — readsxnow. No call site changes; the shape of what they produce does. - Every line carries a
message, defaulting to the event name.eventis unchanged and still the field to filter on. - Context fields can be filtered out. Under the kernel's own default nothing is, but a
logging.fieldson the surface or aLOG_FIELDSin the environment will drop what it does not name — includingserviceandtime, which were previously unconditional.
🐞 Fixes
redactno longer masks booleans. Found in production: every field on anauthentication.failedline came back[redacted], including the four booleans that said why it failed.{ "tokenPresented": "[redacted]", "apiKeyPresented": "[redacted]" }The line recorded a failure and withheld its cause — a security feature defeating a security log. A boolean cannot carry a credential: there are two of them and neither is a secret, so they pass through. Names that routinely hold numeric secrets —
cardNumber,cvv,cvc,pinCode,otp,iban— are exempt from that exemption and mask whatever type they arrive as, because digits in a log stream are as damaging unquoted as quoted.
⬆️ Upgrading
Nothing stops compiling. Two things to check:
- Anything reading
data.— saved queries, dashboard columns, Logpush consumers, alert rules — drops the prefix. timeandserviceare now listed fields. On the kernel's default they are still emitted; if you setlogging.fields, name them or lose them.