Kurs NestJS · Moduł 6: Obsługa błędów i monitoring

Monitoring i Alerting - systemy wczesnego ostrzegania

5 min czytania
W tej lekcji6

Aplikacja działa. Żaden wyjątek nie poleciał, logi milczą, health check świeci na zielono. A jednak użytkownicy piszą, że „strona zamula". Sprawdzasz - odpowiedzi przychodzą po ośmiuset milisekundach zamiast po stu. Od kiedy? Nie wiadomo. Logi zapisują zdarzenia, a to jest trend: coś, co narastało tygodniami i czego nie widać w żadnym pojedynczym wpisie.

Rzym stawiał na granicach wieże sygnałowe. Nie meldowały pojedynczych zdarzeń - mierzyły ruch: ilu jeźdźców przejechało, jak długo trwa przeprawa, ilu wartowników nie wróciło. Dopiero z tych liczb widać było, że coś się psuje, zanim padła brama. Tym są metryki.

Cztery typy metryk

Standardem zbierania metryk jest Prometheus - system, który zbiera i przechowuje liczby opisujące aplikację. Oferuje cztery typy pomiaru, od najprostszego do najbardziej złożonego:

  1. Counter - tylko rośnie. Liczba obsłużonych żądań, liczba błędów. Nigdy nie maleje; przy restarcie zaczyna od zera.
  2. Gauge - może rosnąć i maleć. Liczba aktywnych połączeń, zajęta pamięć, długość kolejki. Zdejmuje bieżącą wartość, jak wskazówka na mierniku.
  3. Histogram - rozkład wartości w kubełkach (buckets). Zamiast jednej liczby zapamiętuje, ile pomiarów wpadło w przedział 0-100 ms, ile w 100-500 ms i tak dalej.
  4. Summary - podobnie jak histogram, ale percentyle liczone po stronie klienta, czyli w Twojej aplikacji, a nie w Prometheusie.

Do czasu trwania żądań HTTP właściwy jest Histogram. Counter powiedziałby tylko, ile żądań było, a Gauge - ile trwało ostatnie. Histogram pokazuje kształt: że dziewięć na dziesięć żądań mieści się poniżej 200 ms, a co dziesiąte przekracza sekundę. To ten kształt zdradza problem, którego średnia by nie pokazała.

Definicja metryki

Metrykę tworzysz raz, opisując ją trzema polami:

1import { Counter } from 'prom-client';
2
3@Injectable()
4export class PrometheusService {
5  private readonly httpRequestCounter = new Counter({
6    name: 'http_requests_total',
7    help: 'Total HTTP requests',
8    labelNames: ['method', 'route', 'status'],
9  });
10
11  recordRequest(method: string, route: string, status: number) {
12    this.httpRequestCounter.inc({ method, route, status: String(status) });
13  }
14}

Kolejność pól jest umowna, ale zawsze ta sama: name to identyfikator metryki, help - opis czytany przez człowieka, labelNames - lista wymiarów, po których będzie można ciąć dane.

Etykiety są tu najciekawsze. Dzięki nim jeden licznik odpowiada na wiele pytań: ile było żądań POST, ile trafiło na /legions, ile zakończyło się kodem 500. Bez etykiet potrzebowałbyś osobnego licznika na każdą kombinację.

Uwaga na pułapkę: etykieta o wielu możliwych wartościach mnoży liczbę serii danych. Wstawienie identyfikatora użytkownika jako etykiety utworzy tyle serii, ilu masz użytkowników - i położy Prometheusa. Etykiety mają mieć skończony, mały zbiór wartości.

Metryki systemowe za darmo

Zanim napiszesz własne metryki, warto włączyć te wbudowane:

1import { collectDefaultMetrics, register } from 'prom-client';
2
3collectDefaultMetrics();

collectDefaultMetrics() zbiera domyślne metryki systemowe - zużycie procesora, pamięci i opóźnienie pętli zdarzeń Node.js. Ta ostatnia jest szczególnie cenna: rosnące opóźnienie event loopu znaczy, że coś blokuje wątek i aplikacja przestaje nadążać, choć żaden endpoint jeszcze nie pada.

Zwróć uwagę, czego ta funkcja nie robi: niczego nie wysyła, nie tworzy wykresów ani nie zeruje liczników. Jedynie zaczyna zbierać.

Endpoint /metrics

Prometheus nie przyjmuje danych przysyłanych przez aplikację - sam po nie przychodzi. Twoim zadaniem jest wystawić je pod ustalonym adresem:

1@Controller()
2export class MetricsController {
3  @Get('/metrics')
4  async getMetrics(@Res() res: Response) {
5    res.set('Content-Type', register.contentType);
6    res.send(await register.metrics());
7  }
8}

register to rejestr wszystkich zdefiniowanych metryk, a register.metrics() zwraca je w formacie tekstowym, który Prometheus rozumie. Nagłówek Content-Type musi wskazywać tekst zwykły, nie JSON - stąd register.contentType, które ustawia właściwą wartość za Ciebie.

Ten model nazywa się scrapingiem: Prometheus co kilkanaście sekund odpytuje ten adres i zapisuje to, co zastał. Aplikacja nie musi wiedzieć, kto ją obserwuje ani czy ktokolwiek w ogóle.

Pięć etapów wdrożenia

Cała droga od zera do wykresu wygląda tak:

  1. Instalacja pakietu prom-client.
  2. Zdefiniowanie metryk - Counter, Histogram, tyle ile potrzeba.
  3. Utworzenie endpointu /metrics.
  4. Konfiguracja scrapera Prometheus, żeby wiedział, gdzie i jak często pytać.
  5. Wizualizacja metryk w Grafanie - i dopiero tu powstają wykresy oraz alerty.

Podział ról między dwoma ostatnimi bywa myląco zacierany. Prometheus zbiera i przechowuje, Grafana rysuje i alarmuje. To rozdzielenie sprawia, że możesz wymienić jedno bez drugiego - i że aplikacja nie zna żadnego z nich.

Podsumowanie

Wieże sygnałowe stoją, ruch jest mierzony:

  • logi zapisują zdarzenia, metryki pokazują trendy - tego drugiego nie widać w żadnym pojedynczym wpisie,
  • Prometheus zbiera i przechowuje metryki; nie loguje błędów ani niczego nie naprawia,
  • cztery typy od najprostszego: Counter (tylko rośnie), Gauge (rośnie i maleje), Histogram (rozkład w kubełkach), Summary (percentyle liczone po stronie klienta),
  • do czasu trwania żądań HTTP właściwy jest Histogram - pokazuje kształt, którego średnia nie zdradzi,
  • metrykę definiują trzy pola: name, help, labelNames,
  • etykiety pozwalają ciąć dane, ale muszą mieć mały zbiór wartości - identyfikator użytkownika jako etykieta położy Prometheusa,
  • collectDefaultMetrics() zbiera metryki systemowe - CPU, pamięć, opóźnienie event loopu; niczego nie wysyła,
  • Prometheus sam przychodzi po dane (scraping), więc wystawiasz endpoint /metrics z Content-Type ustawionym na tekst zwykły przez register.contentType,
  • pięć etapów: instalacja prom-client, zdefiniowanie metryk, endpoint /metrics, konfiguracja scrapera, wizualizacja w Grafanie,
  • Prometheus zbiera, Grafana rysuje i alarmuje.

W następnej lekcji zejdziemy z poziomu trendów do pojedynczego błędu - poznasz techniki debugowania, gdy wiadomo już, że coś jest nie tak, ale nie wiadomo gdzie. A na razie zapamiętaj: log mówi, co się stało raz; metryka mówi, co dzieje się stale - i to ona ostrzega, zanim padnie brama.

Kod do tej lekcji: src/monitoring/monitoring-system.ts
1// Monitoring i Alerting - Systemy Wczesnego Ostrzegania
2// Metryki, alerty i dashboard dla Imperium
3import { Injectable, Logger } from '@nestjs/common';
4
5// ===========================================
6// 1. Serwis Metryk (styl Prometheus)
7// ===========================================
8
9@Injectable()
10export class MetricsService {
11  private logger = new Logger('Metrics');
12
13  // Liczniki (Counter) - rosna tylko w gore
14  private counters = new Map<string, number>();
15
16  // Histogramy - rozklad wartosci
17  private histograms = new Map<string, number[]>();
18
19  // Gauge - wartosc chwilowa (moze rosnac i malec)
20  private gauges = new Map<string, number>();
21
22  // Zwieksz licznik
23  incrementCounter(name: string, value: number = 1) {
24    const current = this.counters.get(name) || 0;
25    this.counters.set(name, current + value);
26  }
27
28  // Zapisz wartosc histogramu
29  observeHistogram(name: string, value: number) {
30    if (!this.histograms.has(name)) {
31      this.histograms.set(name, []);
32    }
33    this.histograms.get(name).push(value);
34  }
35
36  // Ustaw gauge
37  setGauge(name: string, value: number) {
38    this.gauges.set(name, value);
39  }
40
41  // Pobierz wszystkie metryki
42  getMetrics() {
43    const result: Record<string, any> = {};
44
45    // Counters
46    this.counters.forEach((v, k) => {
47      result[k + '_total'] = v;
48    });
49
50    // Histograms - srednia i percentyle
51    this.histograms.forEach((values, k) => {
52      const sorted = [...values].sort((a, b) => a - b);
53      const sum = values.reduce((a, b) => a + b, 0);
54      result[k + '_avg'] = Math.round(sum / values.length);
55      result[k + '_p95'] = sorted[Math.floor(sorted.length * 0.95)];
56      result[k + '_count'] = values.length;
57    });
58
59    // Gauges
60    this.gauges.forEach((v, k) => {
61      result[k] = v;
62    });
63
64    return result;
65  }
66}
67
68// ===========================================
69// 2. Serwis Alertow
70// ===========================================
71
72@Injectable()
73export class AlertService {
74  private logger = new Logger('Alerting');
75  private alerts: Array<{
76    name: string;
77    severity: string;
78    message: string;
79    timestamp: Date;
80  }> = [];
81
82  // Definicje progow alertow
83  private thresholds = {
84    error_rate: { warn: 0.01, critical: 0.05 },
85    response_time_ms: { warn: 500, critical: 2000 },
86    memory_percent: { warn: 80, critical: 95 },
87  };
88
89  checkThreshold(metric: string, value: number) {
90    const threshold = this.thresholds[metric];
91    if (!threshold) return;
92
93    if (value >= threshold.critical) {
94      this.fireAlert(metric, 'CRITICAL', metric + ' = ' + value);
95    } else if (value >= threshold.warn) {
96      this.fireAlert(metric, 'WARNING', metric + ' = ' + value);
97    }
98  }
99
100  private fireAlert(name: string, severity: string, message: string) {
101    const alert = { name, severity, message, timestamp: new Date() };
102    this.alerts.push(alert);
103    this.logger.warn('ALERT [' + severity + ']: ' + message);
104  }
105
106  getActiveAlerts() {
107    return this.alerts.slice(-20);
108  }
109}
110
111console.log('=== Monitoring & Alerting ===');
112console.log('Counter - zlicza zdarzenia (requesty, bledy)');
113console.log('Histogram - rozklad czasow odpowiedzi');
114console.log('Gauge - aktualna wartosc (pamiec, polaczenia)');
115console.log('Alert thresholds: warn -> critical');
116

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służy Prometheus w kontekście monitorowania aplikacji NestJS?

  2. 2. Który typ metryki Prometheus najlepiej nadaje się do mierzenia czasu trwania żądań HTTP?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij PrometheusService z polem httpRequestCounter (Counter z labelNames: ['method', 'route', 'status_code']) i httpRequestDuration (Histogram z buckets: [0.1, 0.5, 1, 2, 5, 10])

  • Układanie w pionie

    Uporządkuj typy metryk Prometheus od najprostszych do najbardziej złożonych

  • Klikanie w kolejności

    Ułóż elementy definicji Counter z labelNames w poprawnej kolejności

  • Edytor kodu

    Uzupełnij MetricsController z @Get('/metrics'), który ustawia Content-Type na 'text/plain' i zwraca register.metrics() z prom-client

  • Układanie w pionie

    Uporządkuj etapy wdrażania monitoringu Prometheus w aplikacji NestJS od instalacji do wizualizacji

Przydatne artykuły