Log Management & Centralized Logging

Build a centralized logging system. Covers structured logging, log aggregation with ELK/Loki, log levels, retention policies, and searching logs effectively in production.

Log Management & Centralized Logging is the practice of collecting, storing, querying, and analyzing logs from multiple systems—services, hosts, containers, databases, and external APIs—into a single, unified system. It matters when you have more than two services, when services run across multiple environments, or when troubleshooting issues that span several components. It becomes critical when a user reports a failure and you must trace logs across a dozen microservices, each emitting structured and unstructured data in different formats, across different time zones.

Ingesting Logs from Multiple Sources

Logs arrive from diverse origins: application code, system daemons, container runtimes, and external services. To centralize them, you must define a consistent ingestion pipeline. The core workflow involves agents running on each host or in each container, or a sidecar or dedicated service that collects logs from stdin, files, or structured outputs.

The simplest ingestion is a file-based approach using rsyslog or systemd-journald. On a Linux host, journalctl exposes logs via a structured JSON format:

journalctl --output=json --follow --since="2024-05-10T08:00:00"

Each entry includes __REALTIME_TIMESTAMP, PRIORITY, MESSAGE, and a SYSLOG_IDENTIFIER. To forward these to a centralized system, configure rsyslog to send messages over UDP:


*.* @192.168.1.100:514;RSYSLOG_SyslogProtocol23

This sends all logs from the host to a central server at 192.168.1.100 on port 514, using the Syslog Protocol 23 (RFC 5424). The receiver must be configured to parse the structured fields and accept the SYSLOG-MSG format.

For containerized environments, fluentd or Vector are commonly used. A fluentd configuration for collecting logs from Docker containers looks like:

<!-- /fluentd/etc/fluent.conf -->
<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>

<match **>
  @type file
  path /var/log/fluentd/central-logs
  flush_interval 10s
  compress gzip
</match>

<match app.**>
  @type elasticsearch
  host 192.168.1.100
  port 9200
  index_name app-logs-${time_slice}
  time_key time
  time_format %Y-%m-%d
</match>

The forward source listens on TCP port 24224, and each Docker container emits logs via fluentd as a sidecar. The docker-compose.yml for a service includes:

services:
  app:
    image: myapp:v1.2
    volumes:
      - /var/log/app:/var/log/app
    command: ["sh", "-c", "tail -f /var/log/app/app.log | fluent-cat app.logs"]

The fluent-cat utility sends each line from the app’s app.log file into Fluentd’s input stream with a tag app.logs. Fluentd then enriches, filters, and forwards logs to Elasticsearch or another backend.

Structuring Log Data

Raw logs are often unstructured or semi-structured. Centralized logging requires a consistent format. The most effective approach is to use JSON for structured logs, especially when logs contain context beyond the message.

An application emitting structured logs using json format in Go might write:

log.Printf(`{"level":"info","message":"User login succeeded","user_id":123,"duration_ms":34,"timestamp":"2024-05-10T08:01:12.456Z"}`)

This output is consumed by a log agent and stored in a time-series database. The key is the consistent schema: timestamp, level, message, and structured fields like user_id, duration_ms. The absence of a timestamp field in a log line causes a drift in time alignment across services.

For systems that emit logs in text format, use a parser to extract structured fields. Fluentd’s parser plugin can split key-value pairs in a key=value format:

<parse>
  @type multiline
  multiline_start_regexp /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)/
  format /(?<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z) (?<level>\w+) (?<message>.*)/
</parse>

This parses timestamps and levels from lines like:

2024-05-10T08:01:12.456Z INFO User login succeeded

The parser extracts timestamp, level, and message, and can add additional fields via regex capture groups.

Querying and Searching Logs

Once logs are stored, the next task is querying and searching across thousands of log entries. The most common query interface is a time-range filter with keyword and field-based search.

A typical query in a centralized logging system might look like:

SELECT * FROM logs
WHERE timestamp >= '2024-05-10T08:00:00Z'
  AND level = 'error'
  AND service = 'auth'
  AND 'user_id' = 123
ORDER BY timestamp DESC
LIMIT 100

This query retrieves all error-level logs from the auth service for user ID 123, starting from 8:00 AM, sorted by time, returning the 100 most recent entries.

When logs are stored in Elasticsearch, such queries are executed via the query DSL. A full search request might be:

POST /logs/_search
{
  "query": {
    "bool": {
      "must": [
        { "term": { "level": "error" } },
        { "term": { "service": "auth" } },
        { "range": { "timestamp": { "gte": "2024-05-10T08:00:00Z" } } }
      ],
      "should": [
        { "match": { "message": "authentication failed" } },
        { "exists": { "field": "trace_id" } }
      ]
    }
  },
  "sort": [
    { "timestamp": { "order": "desc" } }
  ],
  "size": 100
}

The must clauses require all conditions, while should clauses boost relevance. The exists clause is crucial when tracing distributed requests: a log entry with trace_id implies it belongs to a single request across services.

Failure modes in querying include:

Correlating Logs Across Services

When logs are centralized, the next step is correlating events across services. The key enabler is a common trace ID that links requests across microservices.

A service generates a trace_id and propagates it through the request lifecycle. This ID is included in every log entry, and often in HTTP headers. The X-Trace-ID header is widely adopted:

GET /api/v1/users/123 HTTP/1.1
Host: auth.example.com
X-Trace-ID: 7f3a1d8c-b3b0-4d1e-9a6f-7b2c4d5e6f7a

Each service logs its part of the request, including the trace_id. A query for a specific trace ID returns all logs in a single request flow:

[
  {
    "service": "gateway",
    "trace_id": "7f3a1d8c-b3b0-4d1e-9a6f-7b2c4d5e6f7a",
    "level": "info",
    "message": "Request received",
    "timestamp": "2024-05-10T08:00:00.123Z"
  },
  {
    "service": "auth",
    "trace_id": "7f3a1d8c-b3b0-4d1e-9a6f-7b2c4d5e6f7a",
    "level": "error",
    "message": "Authentication failed",
    "error": "Invalid token",
    "timestamp": "2024-05-10T08:00:01.456Z"
  },
  {
    "service": "user",
    "trace_id": "7f3a1d8c-b3b0-4d1e-9a6f-7b2c4d5e6f7a",
    "level": "info",
    "message": "User retrieved",
    "user_id": 123,
    "timestamp": "2024-05-10T08:00:02.001Z"
  }
]

The trace ID allows visualizing the entire request flow. A failure in the auth service can be linked to the gateway’s request and the user service’s response, revealing the full context of the error.

Alerting and Monitoring

With logs centralized, you can set up alerts based on patterns in the log stream. The most common approach is to define alerting rules over log queries.

An alert rule might be:

alert:
  name: "High Error Rate in Auth Service"
  query: |
    SELECT
      count(*) as error_count,
      avg(duration_ms) as avg_duration
    FROM logs
    WHERE
      service = 'auth'
      AND level = 'error'
      AND timestamp >= now() - 5m
    GROUP BY
      time(1m)
  condition: |
    error_count > 10 OR avg_duration > 500
  notification:
    email: ["[email protected]", "[email protected]"]
    slack:
      webhook_url: "https://hooks.slack.com/services/abc/123/xyz"
      channel: "#alerts"

This rule runs every minute, checks the last five minutes of logs, and triggers an alert if there are more than 10 errors or an average duration over 500ms. The alert is sent via email and Slack.

The alert system must handle alert deduplication. If an error occurs every 30 seconds, the alert system must merge consecutive alerts into a single incident. A common failure is alert storms: a single issue generates 50 alerts in a minute, each with a unique trace_id and timestamp, but the alerting system does not group them by root cause.

Another sharp edge is alert threshold tuning. A threshold of error_count > 10 over 5 minutes works well for steady traffic but fails during peak load. A spike of 150 errors in 30 seconds triggers an alert, but the system does not account for baseline traffic.

Retention, Storage, and Performance

Logs grow rapidly. A single service might generate 1 GB of logs per day. Centralized logging systems must manage retention policies and storage costs.

A common configuration uses tiered storage: hot data in fast storage (SSD), warm data in object storage (S3), and cold data in archival storage (tape or Glacier).

Retention is configured by a retention policy:

{
  "index_patterns": ["app-logs-*"],
  "retention": [
    {
      "days": 7,
      "storage": "hot"
    },
    {
      "days": 30,
      "storage": "warm"
    },
    {
      "days": 365,
      "storage": "cold"
    }
  ]
}

This policy moves logs from hot to warm after 7 days, and to cold after 30 days. The system must trigger a migration task—such as a reindex or rollover—to move data across tiers.

A common performance pitfall is log ingestion lag. When logs arrive faster than they can be processed, a backlog builds. The fluentd agent may push logs to Elasticsearch, but the Elasticsearch cluster becomes overloaded. The logstash pipeline might fail to keep up with incoming logs, leading to:

The solution is to monitor ingestion metrics: queue size, processing latency, and throughput. A healthy system maintains a queue size of less than 10,000 events and a processing latency under 2 seconds.

Failure Modes and Common Pitfalls

These failures are silent but costly: logs are collected, but the data is inconsistent, incomplete, or inaccessible.

Centralized logging is not a one-time setup. It requires ongoing maintenance: monitoring ingestion, tuning queries, managing retention, and responding to alert storms. The most effective teams treat logs not as a record of past events, but as a real-time operational dashboard.

This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.