Logging becomes useful the moment you stop writing sentences for humans and start emitting events for machines. Here is the sequence that gets you from print statements to something you can query, alert on, and afford.
Replace free-text messages with JSON objects: a timestamp, a level, an event name, and fields. {"ts":"2026-04-01T10:00:00Z","level":"warn","event":"payment_retry","order_id":"A-771","attempt":2} is greppable, parseable and aggregatable; "Retrying payment for order A-771 (attempt 2)" is none of those. In Python, structlog or python-json-logger do this with a handler change. In Node, pino is the default choice and is fast enough to leave on in production.
ERROR means a human needs to act. WARN means a degraded path was taken and it happened often enough to notice. INFO is the audit trail of what the system did. DEBUG is for reproducing a specific problem and should be switchable per module at runtime rather than requiring a deploy. If everything is INFO, you have one level and a large bill.
Generate a request ID in your first middleware, put it in a context variable (contextvars in Python, AsyncLocalStorage in Node), and attach it to every log line automatically. When someone reports an error, one filter shows you every line from that request across every service. If you run OpenTelemetry, use the existing trace_id and span_id instead so logs and traces line up in the same view.
On a VM, drop a file in /etc/logrotate.d/: daily, rotate 14, compress, delaycompress, missingok, notifempty. Prefer copytruncate only if your app cannot reopen its file on SIGHUP — it can lose lines written during the copy. Under systemd, log to stdout and let journald handle it, then read with journalctl -u myapp -f --since "10 min ago". In containers, log to stdout and let the runtime deal with files; writing log files inside a container is a way to lose them.
A collector — Vector, Fluent Bit or the OpenTelemetry Collector — reads from files or stdout, parses, enriches with host and service metadata, and forwards. Do the sampling here, not in the application: keep 100% of ERROR and WARN, keep 100% of anything carrying an active trace, and sample high-volume INFO from healthy requests down to a few percent. That one rule typically removes most of the volume without removing anything you would have looked at.
Maintain a deny-list of field names — password, token, authorization, card_number, ssn — and a processor that drops or masks them in the logging pipeline itself. pino has redact paths built in; structlog does it with a processor. Redacting at the aggregator is too late: the secret already crossed a network and sat in a file.
A single ERROR is not an incident. Alert on rate and ratio: error events per minute above a threshold, or the proportion of requests logging a failure exceeding a percentage over a five-minute window. Add one alert for the absence of logs — a service that has stopped emitting anything is usually a worse problem than one emitting errors.
Free tools, guides, and resources across the SPUNK13 network.
Visit spunk.bet400+ Free Tools