Kurs NestJS · Moduł 6: Obsługa błędów i monitoring
Monitoring i Alerting - systemy wczesnego ostrzegania
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:
- Counter - tylko rośnie. Liczba obsłużonych żądań, liczba błędów. Nigdy nie maleje; przy restarcie zaczyna od zera.
- 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.
- 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.
- 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:
- Instalacja pakietu
prom-client. - Zdefiniowanie metryk - Counter, Histogram, tyle ile potrzeba.
- Utworzenie endpointu
/metrics. - Konfiguracja scrapera Prometheus, żeby wiedział, gdzie i jak często pytać.
- 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
/metricszContent-Typeustawionym na tekst zwykły przezregister.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');
116Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służy Prometheus w kontekście monitorowania aplikacji NestJS?
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