Back to Docker: Images, Containers, Multi-Stage
Q2Question 2 of 10

Centralizing logs across many services/containers (ELK/EFK, Loki) and tracing a single request end-to-end (correlation ID, OpenTelemetry, Jaeger/Zipkin)

Key Concepts
  • Centralized logging means every service writes structured JSON logs to stdout and a collector ships them to one searchable store
  • ELK (Elasticsearch + Logstash + Kibana) gives rich full-text search; EFK swaps in Fluentd/Fluent Bit for Kubernetes
  • Loki indexes only labels (service, namespace) and stores log bodies compressed, making it cheaper but less suited to arbitrary text search
  • Distributed tracing uses a trace ID generated at the edge and propagated via W3C traceparent headers through every service call
  • OpenTelemetry is the vendor-neutral standard for auto-instrumentation; Jaeger and Zipkin store and visualize traces
  • Correlation: put the trace ID into the MDC so every log line carries it, enabling pivot from trace to logs instantly

Definition

Ship every service's structured logs to one searchable store, and stamp every request with a propagated trace ID so you can reconstruct its whole cross-service journey.
  • Centralized logging means every service writes structured JSON logs to stdout, a collector on each node ships them to a single store, and you query all logs in one place
  • ELK (Elasticsearch + Logstash + Kibana) indexes full log content for rich text search; EFK swaps in Fluentd/Fluent Bit as the shipper
  • Loki indexes only a small set of labels and stores log bodies compressed, queried with LogQL in Grafana, making it far cheaper at scale
  • Distributed tracing follows a single request across services by generating a trace ID at the edge and propagating it through every call via W3C traceparent headers
  • OpenTelemetry is the vendor-neutral instrumentation standard; Jaeger and Zipkin store and visualize the span waterfall

When a transfer from Rajesh SAV2001 to Priya CUR3001 passes through five different departments at the National Bank of India, each department used to keep its own private diary. Centralized logging is like photocopying every diary entry and filing it in one central records room that anyone can search instantly. Distributed tracing adds a single case number stamped on the transfer form the moment it enters the building, and every department writes that same case number in its diary. Now the investigator searches the central room for one case number and instantly sees the complete journey across all five departments in order.

Understanding the Concepts

What is centralized logging and why do you need it?

In a containerized world, applications run across dozens or hundreds of containers. Without centralized logging, debugging requires SSH-ing into each host and grepping through individual log files. Centralized logging ships all logs to one searchable store so you query all services in one place.

TEXT
# Without centralized logging:
kubectl logs transfer-service-pod-abc     # check one pod
kubectl logs fraud-service-pod-def        # check another pod
kubectl logs account-service-pod-ghi      # check yet another pod
# Slow, manual, and containers may be gone already

# With centralized logging:
# Search one store for all services at once:
{service="transfer"} |= "SAV2001" | json | traceId = "abc123"

How do you structure logs for centralization?

Structured logging means every service emits JSON with consistent fields instead of free-text lines. Structured logs are machine-parseable, so the store can index and filter them.

JAVA
// Spring Boot: configure structured JSON logging (logback-spring.xml)
// <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
//   <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
// </appender>

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class TransferService {
    private static final Logger log = LoggerFactory.getLogger(TransferService.class);

    public void processTransfer(String fromAccount, String toAccount,
                                java.math.BigDecimal amount) {
        MDC.put("fromAccount", fromAccount);
        MDC.put("toAccount", toAccount);
        MDC.put("amount", amount.toPlainString());
        log.info("Processing transfer");
        // Output: {"timestamp":"2024-03-15T10:05:03Z","level":"INFO",
        //          "service":"transfer-service","traceId":"abc123",
        //          "fromAccount":"SAV2001","toAccount":"CUR3001",
        //          "amount":"50000","message":"Processing transfer"}
    }
}

How is ELK different from Loki? (comparison table)

The choice between ELK and Loki depends on cost and search needs.

| Aspect | ELK (Elasticsearch) | Loki |
|---|---|---|
| Index strategy | Full-text of every log | Only labels (service, namespace) |
| Search power | Arbitrary text search | Label filter then grep |
| Storage cost | High (indexed content) | Low (compressed bodies) |
| Query language | KQL / Lucene | LogQL |
| UI | Kibana | Grafana (same UI as metrics) |
| Best for | Rich analytics, compliance | Cost-efficient, label-scoped search |

YAML
# Loki query (LogQL) -- filter by label, then grep
{service="transfer-service", namespace="production"}
  |= "SAV2001"
  | json
  | amount > 10000

# Elasticsearch query (KQL) -- arbitrary text search
fromAccount: "SAV2001" AND level: "ERROR" AND amount: [10000 TO *]

How does log collection work in Kubernetes?

In Kubernetes, applications write logs to stdout, and a collector running as a DaemonSet on each node tails the container log files, enriches them with pod metadata, and ships them onward.

YAML
# Fluent Bit DaemonSet (simplified)
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit
  namespace: logging
spec:
  template:
    spec:
      containers:
        - name: fluent-bit
          image: fluent/fluent-bit:2.2
          volumeMounts:
            - name: varlog
              mountPath: /var/log
            - name: config
              mountPath: /fluent-bit/etc/
      volumes:
        - name: varlog
          hostPath:
            path: /var/log
INI
# fluent-bit.conf
[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    Parser            docker
    Tag               kube.*

[FILTER]
    Name              kubernetes
    Match             kube.*
    # Enriches with pod name, namespace, labels

[OUTPUT]
    Name              loki
    Match             *
    Host              loki.logging.svc
    Port              3100
    Labels            job=fluent-bit

How does distributed tracing work?

At the edge, the API gateway or first service generates a unique trace ID for the whole request. Each unit of work becomes a span. The crucial step is propagation: when one service calls another, it passes the trace context in the W3C traceparent HTTP header.

TEXT
# W3C traceparent header format:
traceparent: 00-abc123def456789012345678abcdef01-1234567890abcdef-01
             ^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^  ^^
             ver          trace-id                    span-id      flags
JAVA
// OpenTelemetry auto-instrumentation handles this automatically
// In Spring Boot, add the dependency and the agent:
// java -javaagent:opentelemetry-javaagent.jar \
//      -Dotel.service.name=transfer-service \
//      -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
//      -jar transfer-service.jar

// The agent automatically:
// 1. Creates spans for incoming HTTP requests
// 2. Propagates traceparent header on outgoing HTTP calls
// 3. Creates spans for database queries
// 4. Exports spans to the OTLP collector

How do you correlate logs and traces?

The key to correlation is putting the trace ID into the logging context (MDC in Java) so every log line carries it.

JAVA
import io.opentelemetry.api.trace.Span;
import org.slf4j.MDC;

public class TraceLoggingFilter implements javax.servlet.Filter {
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws Exception {
        String traceId = Span.current().getSpanContext().getTraceId();
        MDC.put("traceId", traceId);
        try {
            chain.doFilter(req, res);
        } finally {
            MDC.remove("traceId");
        }
    }
}
TEXT
# Now you can pivot:
# 1. In Jaeger: find trace abc123, see fraud-svc span is slow
# 2. In Loki:  {service="fraud-service"} |= "abc123"
#    -> see the exact exception: "Timeout calling AML API after 3 retries"

What is tail-based sampling and why does it matter?

Tracing every request is expensive, so you sample. Tail-based sampling keeps all traces that contain an error or exceed a latency threshold while sampling only a fraction of fast successful ones.

YAML
# OpenTelemetry Collector with tail-based sampling
processors:
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors-always
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow-always
        type: latency
        latency:
          threshold_ms: 500
      - name: sample-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 10    # Keep 10% of normal traces

What are common mistakes?

The most common mistake is not standardizing field names across services, making cross-service queries impossible.

JSON
// WRONG: Different field names across services
// transfer-service: {"acct_from": "SAV2001"}
// account-service:  {"sourceAccount": "SAV2001"}
// fraud-service:    {"from_account": "SAV2001"}

// CORRECT: Standardized field names
// All services: {"fromAccount": "SAV2001", "toAccount": "CUR3001"}

What should a senior engineer know?

Never log secrets or full PII. Account IDs are fine as identifiers, but passwords and full card numbers must be redacted. Set retention tiers: hot storage for recent logs you query often, cheaper cold storage for older logs kept for compliance.

JAVA
// WRONG: Logging sensitive data
log.info("Card number: {}", cardNumber);  // Never!

// CORRECT: Redact sensitive fields
log.info("Card ending: {}", cardNumber.substring(cardNumber.length() - 4));

What are common interview follow-up questions?

  • Why not just use kubectl logs? Containers may be restarted or deleted, losing their logs. Centralized logging persists them.
  • What is the difference between Fluentd and Fluent Bit? Fluent Bit is lighter weight, written in C, better for Kubernetes DaemonSets. Fluentd is more feature-rich but heavier.
  • Can you use Loki with Jaeger? Yes, and Grafana can link from a Loki log line to a Jaeger trace using the trace ID field.

Visual Overview

DIAGRAM
  Centralized Logging in Kubernetes
  ┌──────────────┐  stdout   ┌──────────────┐  ship    ┌──────────────┐
  │ Transfer Pod │──────────▶│ Fluent Bit   │────────▶│ Loki / ELK   │
  │ (JSON logs)  │           │ (DaemonSet)  │         │ (central     │
  └──────────────┘           │              │         │  store)      │
  ┌──────────────┐  stdout   │              │         └──────┬───────┘
  │ Fraud Pod    │──────────▶│              │                │
  └──────────────┘           └──────────────┘                ▼
  ┌──────────────┐                                   ┌──────────────┐
  │ Account Pod  │                                   │ Grafana /    │
  └──────────────┘                                   │ Kibana       │
                                                     └──────────────┘

  Trace Propagation
  ┌─────────┐  traceparent  ┌─────────┐  traceparent  ┌─────────┐
  │ Gateway │──────────────▶│Transfer │──────────────▶│ Fraud   │
  │ traceId │  header       │ Service │  header       │ Service │
  │ abc123  │               │ abc123  │               │ abc123  │
  └─────────┘               └─────────┘               └─────────┘
       │                         │                         │
       └──── all spans exported to Jaeger/Zipkin ──────────┘

Key Takeaway

Centralizing logs means every service writes structured JSON to stdout, a collector like Fluent Bit running as a DaemonSet tails those logs, enriches them with pod metadata, and ships them to one searchable store. ELK indexes the full content for rich text search but is storage-heavy. Loki takes a cheaper approach by indexing only labels and compressing the log bodies, queried with LogQL in Grafana. The goal is always the same: one place to search instead of logging into every host.

Tracing a single request end-to-end is solved by trace context propagation. At the edge, the API gateway generates a unique trace ID, each unit of work becomes a span, and the crucial step is propagation through the W3C traceparent header when one service calls another. OpenTelemetry auto-instruments incoming and outgoing calls, and Jaeger or Zipkin draw the waterfall showing each service's duration and errors.

The two come together through correlation. Putting the trace ID into the MDC means every log line carries it, so from a trace in Jaeger you can pivot straight to the logs filtered by that exact trace ID. In the National Bank of India project, a transfer from Rajesh's SAV2001 to Priya's CUR3001 passed through four services. When a transfer failed intermittently, the team searched the central log store for one trace ID and instantly saw the complete ordered journey, while the trace waterfall showed the fraud service was the slow hop.

Remember one sentence: centralized structured logs give you one place to look, and a propagated trace ID gives you the thread that stitches a single request's whole cross-service journey together.

---

Share
Use ← → arrow keys to navigate