Definition
- 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.
# 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.
// 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 |
# 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.
# 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
# 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.
# W3C traceparent header format:
traceparent: 00-abc123def456789012345678abcdef01-1234567890abcdef-01
^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^
ver trace-id span-id flags
// 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.
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");
}
}
}
# 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.
# 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.
// 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.
// 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
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.
---