Kurs NestJS · Moduł 12: Konteneryzacja i CI/CD

Observability - metryki z Prometheus

4 min czytania
W tej lekcji5

Prometheusa poznałeś już w module o monitoringu: cztery typy metryk, scraping i endpoint /metrics napisany ręcznie kontrolerem. Działało - ale przy każdej nowej metryce trzeba było pamiętać o rejestrze, a przy każdym module o wstrzyknięciu serwisu, który je trzyma.

NestJS ma na to gotowe rozwiązanie. @willsoto/nestjs-prometheus zbiera i eksponuje metryki aplikacji w formacie Prometheus - a przy okazji sprawia, że metryki stają się zwykłymi providerami, wstrzykiwanymi jak każdy inny.

Rejestracja metryk

Metryki deklarujesz w module, przez funkcje pomocnicze pakietu:

1@Module({
2  imports: [PrometheusModule.register()],
3  providers: [
4    makeCounterProvider({
5      name: 'http_requests_total',
6      help: 'Calkowita liczba zadan HTTP',
7      labelNames: ['method', 'route', 'status'],
8    }),
9    makeGaugeProvider({
10      name: 'active_connections',
11      help: 'Liczba aktywnych polaczen',
12    }),
13    makeHistogramProvider({
14      name: 'http_request_duration_seconds',
15      help: 'Czas obslugi zadania HTTP',
16      labelNames: ['method', 'route'],
17    }),
18  ],
19})
20export class MetricsModule {}

Kolejność wewnątrz makeCounterProvider jest stała: makeCounterProvider({, potem name: 'http_requests_total',, dalej help: '...',, na końcu }).

PrometheusModule.register() wystawia endpoint /metrics - tego już nie piszesz sam. Trzy pomocnicze funkcje odpowiadają trzem typom metryk, które znasz: Counter tylko rośnie (liczba żądań), Gauge rośnie i maleje (aktywne połączenia), Histogram mierzy rozkład w kubełkach (czasy odpowiedzi). Czwarty typ, Summary, oblicza kwantyle po stronie aplikacji.

Użycie w serwisie

Zarejestrowaną metrykę wstrzykujesz jak każdy provider - tyle że wskazując ją po nazwie:

1@Injectable()
2export class MetricsService {
3  constructor(
4    @InjectMetric('http_requests_total')
5    private readonly requestsCounter: Counter<string>,
6
7    @InjectMetric('http_request_duration_seconds')
8    private readonly requestDuration: Histogram<string>,
9  ) {}
10
11  recordRequest(method: string, route: string, status: number) {
12    this.requestsCounter.inc({ method, route, status: String(status) });
13  }
14
15  startTimer(method: string, route: string) {
16    return this.requestDuration.startTimer({ method, route });
17  }
18}

@InjectMetric('nazwa') działa jak @Inject z tokenem, który poznałeś przy providerach z useValue - metryka nie jest klasą, więc NestJS nie dobierze jej po typie.

Dwie metody warto rozróżnić. inc() podnosi licznik o jeden; to wszystko, co Counter potrafi. startTimer() zwraca funkcję, którą wywołujesz po zakończeniu pracy - dopiero wtedy histogram zapisuje zmierzony czas:

1const timer = this.metricsService.startTimer('GET', '/legions');
2
3await this.legionService.findAll();
4
5timer();

Ten wzorzec - weź funkcję, wykonaj pracę, wywołaj funkcję - jest wygodniejszy niż ręczne odejmowanie znaczników czasu i sam dba o jednostkę: histogramy Prometheusa liczą w sekundach, nie milisekundach.

Metoda RED - co mierzyć najpierw

Metryk można dodać setki. RED to skrót wskazujący trzy, od których zawsze zaczynasz, uporządkowane od najważniejszej:

  1. Rate - liczba żądań na sekundę. Mówi, czy w ogóle jest ruch.
  2. Errors - procent żądań zakończonych błędem. Mówi, czy ruch jest obsługiwany.
  3. Duration - czas odpowiedzi, zwykle jako p95 i p99. Mówi, czy jest obsługiwany dobrze.

Kolejność nie jest przypadkowa i warto ją zapamiętać: spadek Rate do zera znaczy, że nikt nie może się połączyć - to alarm natychmiastowy. Rosnące Errors to awaria widoczna dla użytkowników. Rosnące Duration to problem, który dopiero się rozwija.

Zwróć uwagę, dlaczego czas podajemy jako percentyle, a nie średnią. Średnia 200 ms brzmi dobrze, nawet gdy co dwudziesty użytkownik czeka pięć sekund - p95 mówi wprost: „95% żądań zmieściło się poniżej tej wartości". To jest liczba, która opisuje doświadczenie, a nie je zaciera.

Co zwraca /metrics

Endpoint wystawiony przez pakiet oddaje dane w formacie tekstowym Prometheusa - nie JSON, nie XML, nie CSV:

1# HELP http_requests_total Calkowita liczba zadan HTTP
2# TYPE http_requests_total counter
3http_requests_total{method="GET",route="/legions",status="200"} 1027
4http_requests_total{method="POST",route="/legions",status="201"} 43

Kształt jest prosty i warto go umieć przeczytać. Linie zaczynające się od # HELP niosą opis metryki, a od # TYPE - jej typ. Dalej idą same pomiary: nazwa, etykiety w nawiasach klamrowych, wartość.

Ten format wygląda ubogo obok JSON-a, ale właśnie o to chodzi: Prometheus odpytuje tysiące aplikacji co kilkanaście sekund, więc parsowanie musi być tanie. Gdy zajrzysz pod /metrics w przeglądarce i zobaczysz ścianę tekstu - to znaczy, że działa poprawnie.

Podsumowanie

Oczy Imperium patrzą, a metryki są zwykłymi providerami:

  • @willsoto/nestjs-prometheus zbiera i eksponuje metryki w formacie Prometheus - nie obsługuje WebSocketów, nie testuje endpointów, nie generuje dokumentacji,
  • PrometheusModule.register() wystawia endpoint /metrics za Ciebie,
  • metryki rejestrujesz jako providery: makeCounterProvider, makeGaugeProvider, makeHistogramProvider,
  • kolejność w rejestracji: makeCounterProvider({, name:, help:, }),
  • Counter może tylko rosnąć (liczba żądań); Gauge rośnie i maleje; Histogram mierzy rozkład w kubełkach; Summary oblicza kwantyle,
  • @InjectMetric('nazwa') wstrzykuje metrykę po nazwie, bo nie jest klasą i typ nie wystarcza,
  • inc() podnosi licznik; startTimer() zwraca funkcję, którą wywołujesz po pracy - histogram liczy w sekundach,
  • metoda RED od najważniejszego: Rate (żądania na sekundę) → Errors (procent błędów) → Duration (p95, p99),
  • czas podawaj jako percentyle, nie średnią - średnia ukrywa co dwudziestego użytkownika czekającego pięć sekund,
  • /metrics zwraca format tekstowy Prometheusa: # HELP, # TYPE, potem nazwa z etykietami i wartość - nie JSON, XML ani CSV.

W następnej lekcji pójdziemy o krok dalej niż liczby - poznasz distributed tracing, który pokazuje drogę pojedynczego żądania przez wiele usług. A na razie zapamiętaj: metryki mówią, ile i jak szybko; RED wskazuje, które trzy liczby sprawdzić najpierw.

Kod do tej lekcji: src/prometheus-metrics.ts
1// Observability - Metryki z Prometheus
2console.log("=== PROMETHEUS METRICS ===\n");
3
4// Typy metryk Prometheus
5interface MetricType {
6  name: string;
7  type: 'counter' | 'gauge' | 'histogram' | 'summary';
8  description: string;
9  example: string;
10}
11
12const metricTypes: MetricType[] = [
13  {
14    name: 'Counter',
15    type: 'counter',
16    description: 'Tylko rosnie (np. liczba zadan)',
17    example: 'http_requests_total{method="GET",status="200"} 1547',
18  },
19  {
20    name: 'Gauge',
21    type: 'gauge',
22    description: 'Rosnie i maleje (np. aktywne polaczenia)',
23    example: 'active_connections 42',
24  },
25  {
26    name: 'Histogram',
27    type: 'histogram',
28    description: 'Rozklad wartosci w bucketach (np. czasy odpowiedzi)',
29    example: 'http_request_duration_seconds_bucket{le="0.1"} 1400',
30  },
31  {
32    name: 'Summary',
33    type: 'summary',
34    description: 'Kwantyle (p50, p95, p99)',
35    example: 'request_duration_summary{quantile="0.95"} 0.25',
36  },
37];
38
39console.log("Typy metryk Prometheus:\n");
40metricTypes.forEach((m, i) => {
41  console.log(`${i + 1}. ${m.name} (${m.type})`);
42  console.log(`   Opis: ${m.description}`);
43  console.log(`   Przyklad: ${m.example}\n`);
44});
45
46// Metoda RED
47console.log("=== METODA RED ===\n");
48const red = {
49  Rate: 'Liczba zadan na sekunde',
50  Errors: 'Procent zadan z bledem',
51  Duration: 'Czas odpowiedzi (p95, p99)',
52};
53
54Object.entries(red).forEach(([k, v]) => {
55  console.log(`  ${k}: ${v}`);
56});
57
58console.log("\n=== ENDPOINT /metrics ===\n");
59console.log("# HELP http_requests_total Calkowita liczba zadan");
60console.log("# TYPE http_requests_total counter");
61console.log('http_requests_total{method="GET",status="200"} 1547');
62

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 pakiet @willsoto/nestjs-prometheus?

  2. 2. Czym charakteryzuje się metryka typu Counter w Prometheus?

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

Zadania praktyczne w grze

  • Edytor kodu

    Zdefiniuj trzy metryki: http_requests_total (Counter z labelNames: method, route, status), active_connections (Gauge), http_request_duration_seconds (Histogram z bucketami)

  • Układanie w pionie

    Uporządkuj elementy metody RED od najważniejszego do najrzadziej sprawdzanego:

  • Klikanie w kolejności

    Ułóż elementy rejestracji metryki Counter w PrometheusModule:

  • Edytor kodu

    Wstrzyknij metryki @InjectMetric, użyj requestsCounter.inc() i requestDuration.startTimer() / timer()

Przydatne artykuły