Logging & Monitoring — ELK, Grafana, OpenTelemetry
Xây dựng observability cho hệ thống: structured logging, metric collection, tracing, và dashboard với Grafana.
Observability là khả năng hiểu hệ thống từ output của nó — logs, metrics, traces. Không có observability, bạn đang bay mù.
Three Pillars of Observability#
LOGS: "User 123 just paid $50"
METRICS: "Error rate: 0.1%, P95 latency: 250ms"
TRACES: "Request abc took 500ms: auth(50ms) → api(200ms) → db(250ms)"textStructured Logging#
JSON Format — Machine Readable#
// ❌ Tệ
console.log(`User ${userId} logged in from ${ip}`);
// ✅ Tốt — structured JSON
logger.info({
event: 'user.login',
userId: 123,
ip: '192.168.1.1',
timestamp: new Date().toISOString(),
duration: 150,
});typescriptPino — Fastest Logger for Node.js#
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' } // Đẹp cho dev
: undefined,
redact: {
paths: ['req.headers.authorization', 'req.body.password', 'db.query'],
censor: '[REDACTED]',
},
serializers: {
req: pino.stdSerializers.req,
err: pino.stdSerializers.err,
},
});
// Express middleware
app.use(pinoHttp({ logger }));
// Usage
app.get('/api/users/:id', async (req, res) => {
logger.info({ userId: req.params.id }, 'Fetching user');
try {
const user = await db.user.findUnique({ where: { id: req.params.id } });
logger.info({ userId: req.params.id, found: !!user }, 'User fetched');
res.json(user);
} catch (err) {
logger.error({ err, userId: req.params.id }, 'Failed to fetch user');
res.status(500).json({ error: 'Internal error' });
}
});typescriptLog Levels#
logger.fatal('System crashed — cannot recover'); // 60
logger.error('Payment failed'); // 50
logger.warn('Rate limit approaching'); // 40
logger.info('User registered'); // 30
logger.debug('Query: SELECT * FROM users'); // 20
logger.trace('Entering function x'); // 10typescriptProduction nên set level info hoặc warn. debug và trace chỉ cho development.
Metrics — Đo Lường#
Prometheus + OpenMetrics#
import prometheus from 'prom-client';
// Collect metrics
const register = new prometheus.Registry();
prometheus.collectDefaultMetrics({ register });
// Custom metrics
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
registers: [register],
});
const activeUsers = new prometheus.Gauge({
name: 'active_users',
help: 'Number of active users',
registers: [register],
});
// Express middleware
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
end({ method: req.method, route: req.route?.path || 'unknown', status: res.statusCode });
});
next();
});
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});typescriptRED Metrics (Microservices)#
const requestRate = new prometheus.Counter({
name: 'requests_total',
help: 'Total requests',
labelNames: ['service', 'method'],
});
const errorRate = new prometheus.Counter({
name: 'errors_total',
help: 'Total errors',
labelNames: ['service', 'error_type'],
});
const latency = new prometheus.Histogram({
name: 'request_duration_seconds',
help: 'Request latency',
labelNames: ['service'],
buckets: [0.01, 0.05, 0.1, 0.5, 1],
});typescriptDistributed Tracing#
Theo dõi một request xuyên qua nhiều service:
import { trace, context } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
// Setup
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter()));
provider.register();
// Manual tracing
const tracer = trace.getTracer('order-service');
async function createOrder(userId: number, items: Item[]) {
const span = tracer.startSpan('createOrder', {
attributes: { userId, itemCount: items.length },
});
return await context.with(trace.setSpan(context.active(), span), async () => {
try {
const user = await getUser(userId); // span con tự động
const total = await calculatePrice(items);
const order = await saveOrder(user, items, total);
span.setStatus({ code: SpanStatusCode.OK });
return order;
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
});
}typescriptAuto-instrumentation (không cần sửa code)#
node --require @opentelemetry/auto-instrumentations-node/register app.jsbashTự động trace HTTP, gRPC, DB queries, Redis, etc.
ELK Stack — Centralized Logging#
App → Filebeat/Logstash → Elasticsearch → KibanaplaintextDocker Compose#
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0
environment:
- discovery.type=single-node
kibana:
image: docker.elastic.co/kibana/kibana:8.15.0
ports:
- "5601:5601"
depends_on:
- elasticsearch
logstash:
image: docker.elastic.co/logstash/logstash:8.15.0
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.confyamlGrafana Stack#
App → Prometheus (metrics) → Grafana (dashboard)
App → Loki (logs) → Grafana
App → Tempo (traces) → GrafanaplaintextDocker Compose#
services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
loki:
image: grafana/loki
ports:
- "3100:3100"
tempo:
image: grafana/tempo
ports:
- "3200:3200"
- "4317:4317" # OTLP gRPCyamlDashboard Mẫu (Grafana)#
{
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{route}}"
}]
},
{
"title": "P95 Latency",
"type": "heatmap",
"targets": [{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))"
}]
},
{
"title": "Error Rate",
"type": "stat",
"targets": [{
"expr": "sum(rate(errors_total[5m])) / sum(rate(requests_total[5m])) * 100"
}]
}
]
}jsonAlerting#
# prometheus-alerts.yml
groups:
- name: api-alerts
rules:
- alert: HighErrorRate
expr: rate(errors_total[5m]) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate > 1% for 5 minutes"
- alert: HighLatency
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2
for: 5m
labels:
severity: warningyamlSức Khỏe Hệ Thống — Health Check#
app.get('/health', async (req, res) => {
const checks = {
database: { status: 'healthy' },
redis: { status: 'healthy' },
externalAPI: { status: 'healthy' },
};
try {
await db.$queryRaw`SELECT 1`;
} catch {
checks.database = { status: 'unhealthy', error: 'Cannot connect' };
}
const allHealthy = Object.values(checks).every(c => c.status === 'healthy');
res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? 'healthy' : 'degraded',
checks,
uptime: process.uptime(),
});
});typescriptKết Luận#
Observability không phải “cài tool là xong”. Nó là văn hóa:
- Logs — structured JSON, đủ context để debug
- Metrics — RED (Rate, Errors, Duration) cho API
- Traces — theo dõi request xuyên service
- Dashboard — visualize để phát hiện bất thường
- Alert — thông báo khi có vấn đề
Bắt đầu với: JSON logs → Prometheus metrics → Grafana dashboard. Thêm tracing và alert khi scale.