✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17.2.1.3 Logging Simplicity

A focused guide to Logging Simplicity, connecting core concepts with practical Docker and container operations.

Logging simplicity is the practice of keeping an application's own logging logic as minimal as possible, writing structured output to stdout and stderr and nothing more, while deliberately leaving log rotation, file management, and aggregation entirely to the surrounding container platform, rather than building that infrastructure-level capability directly into the application itself.

Why applications should not manage their own log files

An application that implements its own file-based log rotation, opening and closing log files, tracking size thresholds, archiving old entries, is duplicating capability that the container platform already provides natively and more consistently across every application sharing that same platform:

const rotatingFileStream = require('rotating-file-stream');
const stream = rotatingFileStream.createStream('app.log', { size: '10M', interval: '1d' });
logger.add(new winston.transports.Stream({ stream }));
console.log(JSON.stringify({ level: 'info', message: 'Server started' }));

The second approach delegates rotation, retention, and storage entirely to Docker's own logging driver configuration, which applies consistently across every container on the host regardless of what language or framework each one happens to use, rather than requiring each individual application to correctly implement and maintain its own, separate rotation logic.

Simplicity through stdout as the universal interface

Writing to stdout and stderr is a universal, language-agnostic interface that every container runtime and every log aggregation tool already knows how to capture and route, which means an application's logging code can remain genuinely simple, a direct write to standard output, without needing any awareness of where that output ultimately ends up:

import json
print(json.dumps({"level": "info", "message": "Request processed"}))
fmt.Println(`{"level":"info","message":"Request processed"}`)

This simplicity is not just convenient; it is what makes an application's logging behavior consistent and predictable regardless of which specific environment, local development, CI, staging, production, it happens to be running in, since the destination and handling of that output is entirely the platform's concern, not the application's.

Avoiding custom log shipping logic within the application

An application that directly implements its own log shipping, opening a network connection to a centralized logging service and sending entries itself, introduces unnecessary coupling and complexity, an additional network dependency, its own retry and buffering logic, that a properly configured logging driver or a separate, dedicated collector process already handles more robustly and consistently:

const logger = winston.createLogger({
  transports: [new winston.transports.Http({ host: 'logging.example.com', port: 9200 })],
});
const logger = winston.createLogger({
  transports: [new winston.transports.Console()],
});

The simpler approach writes only to the console, leaving the actual delivery to a centralized destination entirely to the platform's own logging driver or a separate collector, which keeps the application free of network dependencies and retry logic that have nothing to do with its actual business function.

Keeping log format simple and consistent, not elaborate

A consistent, simple, structured format, a flat JSON object with a small, predictable set of fields, level, message, timestamp, and a handful of contextual fields, is easier to maintain and query than an elaborate, deeply nested, frequently changing schema:

{"level": "info", "message": "Order processed", "orderId": "12345", "timestamp": "2024-06-01T12:00:00Z"}
{"event": {"type": "business", "category": "orders", "details": {"action": "processed", "metadata": {"orderId": "12345"}}}}

The flatter, simpler structure is both easier to write consistently across an application's codebase and easier for a centralized aggregation system to index and query effectively, compared to an elaborate, deeply nested structure that adds complexity without a corresponding benefit for most ordinary logging needs.

Letting the platform handle multi-line and special character concerns

Stack traces, multi-line error output, and special characters within log messages are concerns the platform's own logging driver and capture mechanism are already designed to handle correctly; an application attempting to manually escape, truncate, or reformat this kind of output itself is solving a problem that does not actually need solving at the application level:

console.error(err.stack);

A raw, unmodified stack trace written directly to stderr is captured correctly by Docker's logging mechanism without any special handling needed in the application code, which is simpler and more reliable than attempting custom formatting logic to handle multi-line output.

When a small amount of additional complexity is genuinely warranted

This simplicity principle does not mean an application should never use a logging library at all; structured logging libraries that handle consistent field formatting, log level filtering, and basic context propagation provide real, worthwhile value without violating the underlying simplicity goal, since they still ultimately write to stdout and leave everything beyond that to the platform:

const logger = require('pino')();
logger.info({ orderId: '12345' }, 'Order processed');

The distinction is between a logging library that helps structure and format output consistently while still writing to stdout, which is reasonable and helpful, versus one configured to manage its own files, rotation, or direct network shipping, which reintroduces the complexity this practice is specifically meant to avoid.

Common mistakes

  • Implementing custom, application-level log file rotation and retention logic that duplicates capability the container platform's own logging driver already provides.
  • Building direct network-based log shipping into the application itself, introducing unnecessary coupling and complexity that a properly configured logging driver or separate collector would handle more robustly.
  • Using an elaborate, deeply nested log structure that adds complexity without a corresponding querying or maintenance benefit over a simpler, flatter format.
  • Attempting custom handling of multi-line output or special characters at the application level, when the platform's own logging capture mechanism already handles this correctly.
  • Conflating a structured logging library that still writes to stdout with one configured for file management or direct network shipping, when only the latter actually reintroduces the complexity this practice avoids.

Logging simplicity keeps an application's own logging responsibility limited to producing clear, structured output to stdout and stderr, deliberately leaving rotation, retention, and centralized delivery entirely to the surrounding platform, which keeps application code simpler, more portable across environments, and free of infrastructure concerns that are not actually the application's own responsibility to manage.