Observability ud over logging: Traces, målinger og forståelse af produktion

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:

  1. Latens: Hvor lang tid forespørgsler tager
  2. Trafik: Hvor mange forespørgsler du håndterer
  3. Fejl: Hvor mange forespørgsler fejler
  4. 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:

  1. Tilføj de fire gyldne signaler til hver service
  2. Tilføj struktureret logging med trace-ID’er
  3. Tilføj tracing til eksterne kald
  4. 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:

  1. Injicerer fejl
  2. Forsøger at diagnosticere kun ved hjælp af observability-værktøjer
  3. 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.