Structured Node.js Logs Piped into journald
Write JSON logs from Node to stdout, let systemd capture them in journald, and query a single stream instead of rotating application log files.
Netbay Cloud Team
Netbay Engineering
On this page
A log file next to server.js is a second database you now have to rotate, ship, and permission. systemd already captures stdout and stderr into journald for every unit. If Node writes one JSON object per line to stdout, you get structured logs without a sidecar, without winston transports aimed at disk, and without a forgotten /var/log/app.log that fills the High-Speed SSD. This post is that pipeline: a tiny logger, a unit that points StandardOutput at the journal, and the journalctl queries you will actually run at 2 a.m.
Unstructured console.log("user " + id + " failed") is searchable only with grep. JSON with a level, a msg, and a request id is searchable with journalctl and with any collector that tails the journal. Pick a schema and do not change field names weekly.
Write JSON lines, not pretty text
A log line is one object, one newline, no ANSI colors, no multi-line stack dumps unless the stack is a single JSON string field. JSON.stringify on a plain object is enough. Do not use a template literal to interpolate secrets into the message. Put stable keys on the object so queries can filter.
const levels = { error: 50, warn: 40, info: 30, debug: 20 };
function log(level, msg, extra) {
const row = {
ts: new Date().toISOString(),
level: level,
lvl: levels[level] || 30,
msg: msg,
pid: process.pid
};
if (extra) {
const keys = Object.keys(extra);
for (let i = 0; i < keys.length; i++) {
row[keys[i]] = extra[keys[i]];
}
}
const line = JSON.stringify(row);
if (level === 'error') process.stderr.write(line + '\n');
else process.stdout.write(line + '\n');
}
const http = require('http');
http.createServer(function (req, res) {
const rid = req.headers['x-request-id'] || 'none';
log('info', 'request', { method: req.method, url: req.url, rid: rid });
res.end('ok\n');
}).listen(3000, '127.0.0.1');stderr for errors lets journald keep the priority. stdout for info. Never log Authorization, cookie, or DATABASE_URL. Redact by omitting the key, not by printing four stars that still prove the header was present on a debug line you left enabled.
Libraries like pino are fine if they write JSON to stdout and you do not also enable a pretty transport in production. Pretty printing is a local TTY tool. On the server it destroys structure.
Point the unit at the journal and nowhere else
StandardOutput=journal and StandardError=journal are the defaults for Type=simple, but say them anyway so a drop-in cannot redirect to a file. SyslogIdentifier= sets SYSLOG_IDENTIFIER so the logs are greppable even if the unit name changes. LogRateLimitIntervalSec and LogRateLimitBurst protect the journal from a tight loop that would otherwise drown disk.
sudo systemctl edit --full nodeapp.service
# inside [Service]:
# StandardOutput=journal
# StandardError=journal
# SyslogIdentifier=nodeapp
# LogRateLimitIntervalSec=10s
# LogRateLimitBurst=200
journalctl -u nodeapp.service -n 50 --no-pager
journalctl -u nodeapp.service -o cat --since "10 min ago"
journalctl -u nodeapp.service SYSLOG_IDENTIFIER=nodeapp -g '"level":"error"' --since today
journalctl -u nodeapp.service -o json-pretty -n 5-o cat prints the raw JSON line the app wrote, which is what you want to pipe to jq on a laptop. -o json-pretty wraps each line in journald metadata (cursor, realtime timestamp, pid). Use cat for app fields, json-pretty when you need _PID or _SYSTEMD_UNIT. -g is grep; it is not a JSON query language, so keep field names short and unique.
Storage lives in /var/log/journal on persistent images. journald.conf can set SystemMaxUse=500M so logs cannot eat the disk the app needs. That cap is the rotation policy. You do not need logrotate for Node if Node never opens a log file.
Correlate requests, then stop
Pass X-Request-Id from nginx (a $request_id map) into Node and include rid on every line for that request. When a user pastes a time and a URL, journalctl -g the rid and you have the whole story. Do not invent a second correlation system in a log shipper until this one is in the unit.
Ship off-box only after the local journal is correct. A collector that reads the journal (journalbeat, vector, alloy) is safer than one that tails a file Node forgot to flush. For a single Lucknow VPS, journalctl plus a size cap is enough until you have a retention requirement in writing.
Colors, progress bars, and npm install output do not belong on stdout of the service. If a library prints tables, wrap the start so only your logger writes. Mixed formats make -g '"level":"error"' miss real failures.
The takeaway: JSON to stdout, journald as the store, journalctl as the UI. Files are how logs go missing. The unit already has a stream; write to it on purpose.
Turn on journald-backed Node logs on a Netbay Ubuntu 24.04 instance and delete the app log file — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance