Design a complete observability stack. Covers the three pillars (logs, metrics, traces), tool selection (Grafana, Datadog, OpenTelemetry), SLOs, alerting, and dashboard design for production systems.
This page describes how to build and operate a unified observability stack using standard tools and protocols. It focuses on integrating logs, metrics, and traces into a cohesive system that can be deployed without vendor lock-in or proprietary tooling.
Use this approach when building systems that require consistent visibility across multiple services, where you need to correlate data between logging, metrics, and tracing systems. The unified stack works best when you have a small to medium-sized deployment with shared infrastructure, and when the cost of vendor-specific tooling outweighs its benefits.
Log aggregation should use standard structured formats such as JSON. Example log line:
{"timestamp":"2023-06-15T14:30:45.123Z","level":"INFO","service":"user-service","message":"User login successful","user_id":"12345"}
Use a log forwarder like Fluentd or Vector to collect logs from all services:
# Example vector configuration for log collection
[sources.main]
type = "file"
include = ["/var/log/app/*.log"]
Configure log rotation with logrotate:
/var/log/app/application.log {
rotate 7
daily
compress
delaycompress
missingok
notifempty
create 0644 root root
}
Use standard HTTP endpoints for metrics exposure. For example, a Go application using Prometheus client:
import "github.com/prometheus/client_golang/prometheus"
var (
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
},
[]string{"method", "endpoint"},
)
)
func init() {
prometheus.MustRegister(httpRequestDuration)
}
Metrics should be scraped at regular intervals. Example configuration for Prometheus:
scrape_configs:
- job_name: 'application'
static_configs:
- targets: ['localhost:8080']
Use OpenTelemetry SDK to add tracing to applications. Example in Python:
from opentelemetry import trace
from opentelemetry.trace import SpanKind
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("process_order", kind=SpanKind.SERVER):
# Business logic here
pass
Traces should export to a collector that supports OpenTelemetry Protocol (OTLP):
receivers:
otlp:
protocols:
grpc:
exporters:
logging:
loglevel: debug
service:
pipelines:
traces:
receivers: [otlp]
exporters: [logging]
Use a consistent identifier across logs, metrics, and traces. The trace ID is the most common:
{"trace_id":"a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6","span_id":"1234567890abcdef"}
All three data types should be queryable through a single interface. For example, using the jq tool to extract trace IDs from logs:
grep "trace_id" /var/log/app/application.log | jq -r '.trace_id' | sort | uniq
Use Prometheus for metrics storage with retention configuration:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert.rules"
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
For logs, use a standard file-based system with compression:
# Example log directory structure
/logs/
/application/
application.log
application.log.1.gz
application.log.2.gz
Alerts should be defined using PromQL expressions:
groups:
- name: application-alerts
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01
for: 2m
labels:
severity: page
annotations:
summary: "High error rate detected"
Configure alertmanager to send notifications via webhook:
route:
receiver: 'webhook'
receivers:
- name: 'webhook'
webhook_configs:
- url: 'http://alertmanager.example.com/webhook'
Logs, metrics, and traces can significantly impact system performance. Monitor CPU and memory usage of collection agents. A misconfigured log forwarder can cause 10% overhead on application performance.
Limit the amount of data collected:
# Example fluentd filter to limit log volume
<filter application.log>
@type grep
<regexp>
key message
pattern ^INFO
</regexp>
</filter>
Configure buffering for transient failures:
# Vector configuration with buffer
[sources.main]
type = "file"
include = ["/var/log/app/*.log"]
buffer = { type = "disk", max_size = 1073741824 }
If a log forwarder loses connection, logs can be lost silently. Monitor the forwarder's connection status and set up alerts for disconnections.
Prometheus scraping failures can be silent. Configure health checks to detect missing metrics:
curl -f http://localhost:8080/metrics || echo "Metrics endpoint down"
Tracing systems can drop spans during high load. Monitor queue depths and set up alerts for backpressure.
Trace IDs can be missing from logs, causing correlation failures. Implement strict logging policies where all log entries must include trace context.
Create a test that verifies the complete flow:
# Test that logs contain trace IDs
curl -s http://localhost:8080/endpoint | jq -r '.trace_id' > /tmp/trace_id
# Verify logs contain that trace ID
grep "$(cat /tmp/trace_id)" /var/log/app/application.log
Verify alert rules with synthetic data:
# Simulate high error rate
curl -X POST http://localhost:8080/metrics/increment?error=1
Store all configuration files in version control:
# Example directory structure
/configs/
/prometheus/
prometheus.yml
/vector/
vector.toml
/alertmanager/
alertmanager.yml
Use environment variables to adjust configurations:
export PROMETHEUS_RETENTION=30d
export LOG_LEVEL=INFO
Monitor each component with its own health endpoint:
# Check Prometheus
curl http://localhost:9090/-/healthy
# Check Vector
curl http://localhost:9001/health
Collect metrics about your observability stack itself:
var (
collectorUp = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "collector_up",
Help: "Whether the collector is up",
},
[]string{"component"},
)
)
When migrating from a legacy logging system, ensure all historical data can be indexed:
# Example script to reindex old logs
for file in /old/logs/*.log; do
zcat "$file" | jq '.trace_id |= . // ""' > "${file}.new"
done
Maintain compatibility with existing alerting systems by supporting multiple output formats.
Ensure logs have appropriate permissions:
chmod 640 /var/log/app/application.log
chown root:app /var/log/app/application.log
Protect metrics endpoints from unauthorized access:
# Example nginx configuration
location /metrics {
allow 10.0.0.0/8;
deny all;
}
Schedule regular cleanup of old logs and metrics:
# Remove logs older than 30 days
find /var/log/app -name "*.log" -mtime +30 -delete
Update configurations in a controlled manner with rollbacks available.
Use trace ID to cross-reference between systems:
# Find all entries for a specific trace ID
grep "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" /var/log/app/*.log
Monitor resource usage of collection agents and adjust accordingly.
The unified observability stack requires careful attention to configuration, consistent data formats, and robust error handling. The key is to ensure that logs, metrics, and traces all work together without introducing complexity or silent failures.
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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy