Eleven Node.js logging practices for production services

Build a consistent Node.js logging contract with structured fields, severity, request context, error details, redaction, and verified delivery.

Best practices
Illustration of logs flowing through a collection pipeline

Useful Node.js logs let an engineer find an event, connect it to a request, and decide what happened without reconstructing the application from scattered text. That requires a consistent schema, meaningful severity, error details, and a delivery path whose limits are understood.

These eleven practices cover application logging and collection. Pino examples illustrate the approach; Winston and other libraries can implement the same contract. A framework supplies useful defaults, but the application still chooses what to record and the operator still verifies what reaches storage.

Practice What to verify
Use a logging library Shared configuration and a controllable level threshold
Emit structured records Values remain separate, typed fields
Set severity consistently The same condition has the same operational meaning
Write informative messages The action and outcome are clear
Include event time Format, units, and clock behavior are understood
Carry request context Events can be related without mixing requests
Preserve error details Useful stack information survives serialization
Exclude sensitive data Final output passes representative redaction tests
Support audit and operational questions Retention and access match the purpose
Choose an output path deliberately Slow consumers and shutdown are tested
Centralize collection Expected records arrive from every instance

1. Use a library for a shared logging contract

console.log, console.info, console.warn, console.error, and console.trace are useful tools, including in small scripts. They do not by themselves provide a centrally managed severity threshold, timestamp policy, or JSON event schema. You can build those capabilities around console, but a logging library avoids maintaining that infrastructure throughout the application.

Pino provides JSON output and child loggers. Winston provides configurable formats and transports. Decide which fits the framework and output requirements, then put configuration in one module. Fastify uses Pino when logging is enabled; that does not mean every Fastify application logs by default.

Avoid treating a library name as evidence that logs are complete. A suppressed event, lost buffer, or missing collector affects either choice.

2. Keep values in structured fields

Prefer a record such as:

{"level":"info","timestamp":"2026-08-25T07:12:46.743Z","filename":"file.jpg","msg":"Image uploaded"}

The filename can now be filtered independently of the message. Use stable keys and preserve numbers and booleans as their native JSON types. Avoid embedding an entire second JSON document inside a message string unless the consumer explicitly expects that format.

One JSON object per line works well with line-oriented collection. Let the serializer escape newline characters inside messages and stack traces. For local reading, a tool such as pino-pretty can render Pino records without changing the production schema.

3. Give severity an operational meaning

Use debug or trace for diagnostic detail, info for meaningful normal events, warn for a condition worth investigating, and error for a failed operation. Reserve fatal severity for failures that prevent the process from continuing. Expected client validation failures need not all become error-level service incidents.

This runnable .mjs example uses an environment-controlled threshold:

npm install pino
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  timestamp: pino.stdTimeFunctions.isoTime,
});
logger.info({ service: 'order-api' }, 'Service started');

Validate configuration values before deployment. Changing an environment variable outside a running process does not update that process automatically; a runtime level-control mechanism needs explicit implementation and access controls.

When collecting in Guance, map the application's levels to status with the DataKit logging configuration. Test both an information record and an error before relying on severity filters.

4. Describe the action and outcome

“Something went wrong” hides the operation. “Order payment failed” plus order_id, a safe error category, and request context tells the reader where to start. Keep a stable message and put changing values in fields:

logger.warn(
  { request_id: 'req-123', auth_outcome: 'rejected', reason: 'invalid_credentials' },
  'Sign-in rejected',
);

This records the outcome without exposing a password or revealing whether a particular account exists. Distinguish an observed result from a diagnosis: a timeout is evidence that a deadline expired, not proof that the network failed.

5. Include a consistent event timestamp

An ISO 8601 timestamp with an explicit offset or Z is convenient across systems. Pino can use pino.stdTimeFunctions.isoTime; Winston can add its timestamp() format. Epoch values are also valid if the parser knows the unit. Pino's default is milliseconds, not seconds.

Synchronize clocks and retain the distinction between event time and ingestion time. Clock skew, queueing, and replay can make arrival order differ from occurrence order. For elapsed durations, use a monotonic timer rather than subtracting wall-clock timestamps.

6. Bind context to the request

A child logger avoids repeating common fields:

const reqLogger = logger.child({
  request_id: 'req-abc-123',
  service: 'order-api',
});
reqLogger.info({ order_id: 'ORD-34567' }, 'Order created');

Pass it through the request's work or use a framework/context mechanism that preserves isolation between concurrent requests. Generate or validate incoming request identifiers instead of accepting arbitrary unbounded header values.

If tracing is active, include the current trace identifier and the integration's required service and environment fields. Follow the Guance correlation requirements. A request ID that happens to look like a trace ID does not establish an association.

7. Preserve useful error details and define crash behavior

Pino serializes an Error passed directly or under its default err key; the standard err serializer is already configured. Winston can use errors({ stack: true }) with a compatible formatting chain. Check the final record rather than assuming an arbitrary nested Error becomes JSON automatically.

try {
  throw new Error('Payment provider unavailable');
} catch (err) {
  logger.error({ err, order_id: 'ORD-34567' }, 'Payment processing failed');
}

Test uncaught exceptions and unhandled Promise rejections separately. One pattern is a synchronous diagnostic logger attached to uncaughtExceptionMonitor, combined with an explicit --unhandled-rejections=strict process policy. The Pino guide shows that setup. Avoid adding an uncaughtException listener that quietly resumes normal service, and avoid assuming an immediate process.exit() safely drains every asynchronous transport.

A supervisor can restart the process after failure. It does not recover logs that were never delivered. Node's process documentation describes these exception and I/O boundaries.

8. Keep secrets out of the final output

Select the fields the investigation actually needs. Do not serialize full user records, authorization headers, payment details, or request bodies merely because they are available. Add structured redaction rules, then test messages, exception strings, serializers, and nested objects as well.

A user ID can still identify or link activity to a person. Its appropriateness depends on purpose, access, retention, and your data policy; it is not automatically non-sensitive. A pseudonymous identifier may help, but pseudonymization alone does not make all privacy concerns disappear.

Make sensitive-data tests part of the initial logging work. A later central filter cannot undo every earlier copy in a local file, queue, or developer console.

9. Design logs for their other consumers

Logs can support audit questions, feature usage analysis, and rough timing measurements as well as debugging. Winston's startTimer() can produce a durationMs field; equivalent monotonic timing can be recorded with other libraries. Define precisely what interval the number measures.

An audit record may need restricted access, actor and target identifiers, integrity controls, and a different retention period. Sampled application logs are not automatically a complete audit trail. For service-level latency distributions and request rates, metrics and traces often provide a better basis than reconstructing totals from selectively retained events.

10. Make the output path observable too

Stdout is a practical container output when the platform owns log collection and rotation. Host services may instead use managed files or journald. Choose one collection route for each intended copy; collecting both stdout and a duplicate file can double ingestion.

Stdout is not universally asynchronous or free of blocking. Node's behavior depends on the operating system and destination type, and a logger may add its own buffering. Pino's worker transports move destination work off the main thread, but event construction and serialization still consume application resources. Consult Node's process I/O notes and Pino's asynchronous logging guidance.

Exercise slow consumers, full disks, queue saturation, and graceful shutdown. Decide what the system should do when it cannot keep up: block, drop, spill to disk, or reject work. Measure dropped records and queue pressure instead of calling the path reliable solely because it writes to stdout.

11. Verify centralized collection before alerting

Centralized logs make cross-instance queries possible and can preserve records after an application host fails, provided those records already reached durable storage. Configure collection, parsing, service identity, access, and retention together.

In Guance, emit a known test event from each instance and find it by service, time, and request ID. Then configure log detection against validated fields. A quiet result may mean no matching events, a parser error, or a broken collection path; monitor the expected reporting behavior separately.

Set retention and indexing around actual investigation and audit needs. Centralization creates the place to enforce those policies; it does not automatically satisfy every requirement.

Common questions

Does a small project need a logging library?

A short-lived script may work well with simple console output. A service with recurring operations benefits from adopting a consistent schema early. The important distinction is the operational need, not a blanket ban on console.

Can stdout become a bottleneck?

Yes. Destination behavior, buffering, serialization cost, and consumer speed all matter. Test the complete path at representative event sizes and volume, including a slow or unavailable collector.

Should logs contain user IDs?

Only when the purpose justifies them and access and retention are appropriate. Prefer the minimum identifier required for correlation; do not treat it as harmless merely because it is not a password.

What should we implement first?

Start with a shared logger, typed fields, severity, timestamps, error serialization, and sensitive-data controls. Add request context and verify delivery next. Tune sampling, destinations, and retention from measured volume and investigation needs.

Related reading

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting