Introduktion
“Bare tilføj mere logging” er standardsvaret, når produktionsproblemer er svære at fejlsøge. Men flere logs betyder ofte mere støj, ikke mere indsigt.
Ægte observability kræver en anden tilgang: at forstå dit system gennem målinger, traces og strukturerede logs, der arbejder sammen.
De tre søjler
Logs
Hvad der skete på et bestemt tidspunkt.
{
"timestamp": "2024-01-15T10:23:45Z",
"level": "error",
"message": "Payment failed",
"userId": "user_123",
"paymentId": "pay_456",
"error": "Card declined",
"traceId": "abc123"
}
Godt til: Detaljeret fejlfinding, audit-spor, forståelse af konkrete hændelser.
Dårligt til: Aggregering, trends, forståelse af systemomfattende adfærd.
Målinger
Numeriske målinger over tid.
http_requests_total{method="POST", path="/api/payments", status="500"} 42
http_request_duration_seconds{quantile="0.99"} 2.5
active_database_connections 45
Godt til: Alarmering, dashboards, forståelse af trends og mønstre.
Dårligt til: At forstå hvorfor noget skete, fejlfinding af konkrete forespørgsler.
Traces
Rejsen for en forespørgsel gennem dit system.
Trace: abc123
├── API Gateway (2ms)
├── Auth Service (15ms)
├── Payment Service (450ms)
│ ├── Validate Request (5ms)
│ ├── Check Fraud (200ms)
│ └── Process Payment (240ms)
│ └── External Provider (235ms) ← langsom!
└── Notification Service (25ms)
Godt til: At forstå latens, finde flaskehalse, fejlfinding af distribuerede systemer.
Dårligt til: Aggregering (for mange data), simple systemer.
Implementering af effektiv logging
Strukturér dine logs
Ustrukturerede logs er næsten ubrugelige i stor skala:
# Dårligt
console.log(`User ${userId} failed to pay: ${error}`);
# Godt
logger.error('Payment failed', {
userId,
paymentId,
amount,
currency,
errorCode: error.code,
errorMessage: error.message,
traceId: context.traceId
});
Log-niveauer betyder noget
Brug niveauer konsistent:
- ERROR: Noget fejlede, som ikke burde have fejlet
- WARN: Noget uventet, men håndteret
- INFO: Vigtige forretningshændelser
- DEBUG: Detaljeret information til fejlfinding (slået fra i produktion)
Inkludér kontekst
Hver log bør besvare: hvem, hvad, hvornår, hvor, hvorfor?
function processOrder(order: Order, context: Context) {
const logContext = {
orderId: order.id,
userId: order.userId,
traceId: context.traceId,
spanId: context.spanId
};
logger.info('Processing order', { ...logContext, amount: order.total });
try {
// ... behandl ordre
logger.info('Order processed successfully', logContext);
} catch (error) {
logger.error('Order processing failed', {
...logContext,
error: error.message,
stack: error.stack
});
throw error;
}
}
Implementering af målinger
De fire gyldne signaler
Start med disse for hver service:
- Latens: Hvor lang tid forespørgsler tager
- Trafik: Hvor mange forespørgsler du håndterer
- Fejl: Hvor mange forespørgsler fejler
- Mætning: Hvor “fuld” din service er
// Eksempel med Prometheus-klient
const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests',
labelNames: ['method', 'path', 'status'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5]
});
const httpRequestsTotal = new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'path', 'status']
});
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const labels = {
method: req.method,
path: req.route?.path || 'unknown',
status: res.statusCode
};
httpRequestDuration.observe(labels, duration);
httpRequestsTotal.inc(labels);
});
next();
});
Forretningsmålinger
Overvåg ikke kun infrastruktur - overvåg det, der betyder noget for forretningen:
const ordersProcessed = new Counter({
name: 'orders_processed_total',
help: 'Total orders processed',
labelNames: ['status', 'payment_method']
});
const orderValue = new Histogram({
name: 'order_value_dollars',
help: 'Order value in dollars',
buckets: [10, 50, 100, 500, 1000, 5000]
});
Advarsel om kardinalitet
Vær forsigtig med label-værdier. Høj kardinalitet slår målesystemer ihjel:
// Dårligt - userId har ubegrænsede værdier
httpRequests.inc({ userId: user.id });
// Godt - begrænset sæt af værdier
httpRequests.inc({ userType: user.type }); // 'free', 'premium', 'enterprise'
Implementering af distribueret tracing
Videreformidl kontekst
Send trace-konteksten gennem hele din forespørgselsflow:
// HTTP-klient
async function callService(url: string, context: Context) {
return fetch(url, {
headers: {
'X-Trace-Id': context.traceId,
'X-Span-Id': context.spanId,
'X-Parent-Span-Id': context.parentSpanId
}
});
}
// Message queue
async function publishMessage(queue: string, message: any, context: Context) {
await queue.publish({
...message,
_traceContext: {
traceId: context.traceId,
spanId: generateSpanId(),
parentSpanId: context.spanId
}
});
}
Instrumentér nøgleoperationer
Fokusér tracingen på:
- Kald til eksterne services
- Databaseforespørgsler
- Cache-operationer
- Message queue-operationer
- Vigtig forretningslogik
async function processPayment(payment: Payment, context: Context) {
return tracer.startSpan('processPayment', { parent: context.span }, async (span) => {
span.setAttributes({
'payment.id': payment.id,
'payment.amount': payment.amount,
'payment.currency': payment.currency
});
try {
const result = await paymentProvider.charge(payment);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.recordException(error);
throw error;
}
});
}
At forbinde søjlerne
Den reelle styrke kommer fra at forbinde logs, målinger og traces:
Trace-ID i alt
Inkludér trace-ID i alle logs og målinger:
logger.info('Payment processed', {
traceId: context.traceId, // Forbinder til trace
paymentId: payment.id
});
paymentDuration.observe(
{ traceId: context.traceId }, // Forbinder til trace
duration
);
Eksemplarer
Forbind målinger til konkrete traces:
// Når du ser et udsving i latens, kan du klikke igennem for at se
// de faktiske traces, der forårsagede det
httpLatency.observe(
{ method: 'POST', path: '/payments' },
duration,
{ traceId: context.traceId } // Eksemplar
);
Alarmeringsstrategi
Alarmér ved symptomer, ikke årsager
# Dårligt - alarmerer ved årsagen
- alert: HighCPU
expr: cpu_usage > 80%
# Godt - alarmerer ved symptomet
- alert: HighLatency
expr: http_request_duration_seconds{quantile="0.99"} > 2
Brug flere signaler
- alert: PaymentServiceDegraded
expr: |
(
rate(payment_errors_total[5m]) / rate(payment_requests_total[5m]) > 0.01
) and (
histogram_quantile(0.99, rate(payment_duration_seconds_bucket[5m])) > 5
)
annotations:
summary: "Payment service is degraded - high errors AND high latency"
Praktiske tips
Start enkelt
Prøv ikke at instrumentere alt på én gang:
- Tilføj de fire gyldne signaler til hver service
- Tilføj struktureret logging med trace-ID’er
- Tilføj tracing til eksterne kald
- Udvid baseret på, hvad du reelt har brug for at kunne fejlsøge
Gør dashboards nyttige
Et godt dashboard besvarer: “Er systemet sundt lige nu?”
Inkludér:
- Forespørgselsrate og fejlrate
- Latens-percentiler (p50, p95, p99)
- Mætningsmålinger (kødybde, connection pool)
- Vigtige forretningsmålinger
Øv dig i at bruge din observability
Afhold “game days”, hvor du:
- Injicerer fejl
- Forsøger at diagnosticere kun ved hjælp af observability-værktøjer
- Identificerer huller i instrumenteringen
Konklusion
God observability handler ikke om at indsamle flere data - det handler om at indsamle de rigtige data og gøre dem nemme at bruge.
Fokusér på:
- Strukturerede, kontekstrige logs
- De fire gyldne signaler til målinger
- Traces til at forstå forespørgselsflowet
- At forbinde alle tre med trace-ID’er
Når noget går galt i produktion, bør du kunne gå fra alarm til grundårsag på minutter, ikke timer.