IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya28/34-dars25 daqiqa
Mundarija (38)

Observability: metrikalar, trace va xatolarni kuzatish — prom-client, OpenTelemetry

Qisqacha: Observability (kuzatuvchanlik) — ilova ichida nima bo'layotganini tashqaridan, kodni o'zgartirmasdan bilish imkoni. Uning uch ustuni bor. Log — bitta so'rov bilan nima bo'ldi. Metrika — umumiy manzara raqamlarda: nechta, qanchalik tez, qanchasi xato. Trace — bitta so'rovning vaqti qayerga ketdi. vazifalar-api ga Prometheus /metrics endpoint'i qo'shiladi: RED metrikalari, yo'l yorlig'i — marshrut shabloni (/v1/vazifalar/:id), haqiqiy URL emas. OpenTelemetry trace'lari kanon tajribasida o'tkazuvchanlikni 12–23 % kamaytirdi va loyihaga kirmadi.

Bu darsda

  • Log, metrika va trace qaysi savolga javob berishini ajratasiz.
  • Counter, gauge va gistogramma; RED va USE metodlarini qo'llaysiz.
  • Gistogrammadan p99 qanday hisoblanishini va yorliq kardinalligi xavfini tushuntirasiz.
  • OpenTelemetry'da trace, span va avto-instrumentatsiyani, ESM tuzog'ini ko'rasiz.
  • vazifalar-api ga /metrics qo'shasiz va CLI buyrug'idagi ikki yashirin xatoni tuzatasiz.

Oldin bilishingiz kerak: Logging asoslari, Pino chuqur, Correlation ID va so'rov kuzatuvi, So'rov konteksti: nestjs-cls, Health check endpoint'lari.

O'tgan ikki darsda orkestratorga "tirikmisan, tayyormisan" degan savollarga javob berishni o'rgandik. Lekin "ha, tirikman" yetarli emas. Ilova tirik, lekin har o'ninchi so'rov 3 soniya kutyapti — buni health ko'rsatmaydi. Bu darsda ilovaga o'z ichini ko'rsatadigan "asboblar paneli"ni o'rnatamiz.

1. Nega bu kerak?

Shanba, 10:00. Telegram'da foydalanuvchi yozadi: "ilova juda sekin". Sizda nima bor?

  • Log bor (25-qismdan beri pino, har so'rovda sorovId). Lekin minutiga 2 000 qator. Qaysi biri "sekin"? Hamma so'rov sekinmi yoki bittasi?
  • Health 200 qaytaryapti. U faqat "tirikman" deydi.

Savollar esa aniq: sekinlik qachon boshlandi? Hamma yo'ldami yoki bittasidami? Xatolar ko'paydimi? Qaysi qadam vaqt yeyapti — baza, AI, JSON? Bularga loglarni qo'lda varaqlab javob topish — soatlab ish.

O'xshatish: avtomobil. Log — videoregistrator: har lahzani yozadi, lekin butun yo'lni ko'rish uchun soatlab tomosha qilish kerak. Metrika — asboblar paneli: tezlik, yoqilg'i, harorat — bir qarashda. Trace — bitta safarning GPS izi: qayerda tirbandlikda turdingiz, qayerda tez yurdingiz.

2. Uch ustun

Log Metrika Trace
Savol bu so'rov bilan nima bo'ldi? tizim umuman qanday? vaqt qayerga ketdi?
Shakl matn yoki JSON qator son + yorliqlar span'lar daraxti
Hajm har so'rovga qator so'rovlar sonidan deyarli mustaqil har so'rovga bir necha span
Kanonda pino + sorovId (25-qism) prom-client, /metrics (shu dars) tajriba, kirmadi

Uchalasi bir-birini to'ldiradi. Metrika "p99 oshdi" deydi — trace "vaqt bazada ketdi" deydi — log "aynan shu so'rovda shu SQL xatosi" deydi. Ularni bog'laydigan ip — so'rov identifikatori (sorovId, trace'da trace_id).

3. Metrikalar

3.1 Uch tur

Metrika — vaqt bo'yicha o'lchanadigan son. Prometheus (eng keng tarqalgan ochiq metrika tizimi) va uning Node kutubxonasi prom-client uch asosiy turni beradi:

Tur Nima Misol
Counter (hisoblagich) faqat oshadi so'rovlar soni, xatolar soni
Gauge (o'lchagich) oshadi va kamayadi xotira, faol ulanishlar
Histogram (gistogramma) qiymatlarni savatlarga sanaydi javob vaqti

Savat (bucket) — Feature flag'lar darsidagi savat bilan bir g'oya: qiymat tushadigan oraliq. Gistogramma har javob vaqtini saqlamaydi, faqat "10 ms gacha — nechta, 25 ms gacha — nechta" deb sanaydi.

Counter "hozir nechta" demaydi — "boshidan beri nechta" deydi. Tezlikni (soniyasiga nechta) monitoring tizimi ikki o'lchov farqidan hisoblaydi. Shuning uchun counter jarayon qayta ishga tushganda noldan boshlanishi xavfli emas — Prometheus hujjatiga ko'ra counter "faqat oshadi yoki qayta ishga tushganda nolga qaytadi", va rate() bu sakrashni taniydi.

Prometheus'da to'rtinchi tur ham bor — Summary (xulosa). U kvantillarni (p50, p99) ilovaning o'zida hisoblaydi. Kamchiligi: bir nechta nusxaning summary'larini qo'shib bo'lmaydi — hujjat har nusxa p99 larining o'rtachasini olishni "yomon" deb ataydi. Gistogramma savatlari esa nusxalar bo'yicha qo'shiladi, keyin kvantil hisoblanadi. Shuning uchun kanon (va ko'p jamoalar) gistogramma tanlaydi.

3.2 RED va USE

Nimani o'lchash kerak? Ikki tayyor ro'yxat bor.

RED — xizmat (so'rov qabul qiladigan narsa) uchun:

  • Rate — soniyasiga nechta so'rov;
  • Errors — ulardan qanchasi xato (5xx);
  • Duration — qancha vaqt oldi (p50, p95, p99).

USE — resurs (CPU, xotira, disk, pool) uchun:

  • Utilization — qanchasi band (CPU 80 %);
  • Saturation — navbatda qancha kutyapti (event loop kechikishi);
  • Errors — resurs xatolari.

Kanon RED'ni o'zi yozadi (ikki metrika), USE'ni esa prom-client ning collectDefaultMetrics() tayyor beradi: CPU vaqti, xotira, event loop kechikishi, GC.

3.3 /metrics ichida nima bor

Loyiha qadamidagi kodni ishga tushirib, uchta so'rov yubordik va /metrics ni o'qidik (biz ishga tushirdik, xotira rejimi; qisqartirilgan):

text
# HELP http_sorovlar_jami HTTP so'rovlar soni
# TYPE http_sorovlar_jami counter
http_sorovlar_jami{metod="GET",yol="/health/live",status="200",ilova="vazifalar-api"} 1
http_sorovlar_jami{metod="GET",yol="/health/startup",status="200",ilova="vazifalar-api"} 1
http_sorovlar_jami{metod="GET",yol="/health/ready",status="200",ilova="vazifalar-api"} 1
http_sorovlar_jami{metod="GET",yol="/v1/vazifalar/:id",status="404",ilova="vazifalar-api"} 2
http_sorovlar_jami{metod="GET",yol="boshqa",status="404",ilova="vazifalar-api"} 1
# HELP http_sorov_davomiyligi_soniya HTTP javob vaqti (s)
# TYPE http_sorov_davomiyligi_soniya histogram
http_sorov_davomiyligi_soniya_bucket{le="0.005",ilova="vazifalar-api",metod="GET",yol="/health/ready"} 0
http_sorov_davomiyligi_soniya_bucket{le="0.01",ilova="vazifalar-api",metod="GET",yol="/health/ready"} 1
process_cpu_seconds_total{ilova="vazifalar-api"} 0.29600000000000004
process_resident_memory_bytes{ilova="vazifalar-api"} 205643776
nodejs_eventloop_lag_p99_seconds{ilova="vazifalar-api"} 0.027836415

Format oddiy matn: nom{yorliq="qiymat",…} son. Uch narsaga qarang:

  1. GET /v1/vazifalar/1 va /v1/vazifalar/99 — ikki xil URL, lekin bitta qator: yol="/v1/vazifalar/:id", son 2.
  2. /yoq/12345 — bunday marshrut yo'q. U yol="boshqa" ga tushdi, 12345 hech qayerda yo'q.
  3. Gistogramma le (less or equal — "shu qiymatgacha") savatlari jamlanma: 0.01 savatida 1 — so'rov 10 ms dan tez tugagan.

3.4 Gistogrammadan p99

Prometheus har bir javob vaqtini saqlamaydi — faqat savatlardagi sonlarni. p99 ni u savatlardan taxmin qiladi: kerakli o'rin tushgan savatni topadi va ichida qiymatlar tekis tarqalgan deb hisoblaydi. Bu histogram_quantile funksiyasining g'oyasi. Brauzerda ko'ramiz:

js
// Prometheus histogram_quantile g'oyasi: savatlar jamlanma
// (le — "shu qiymatgacha"). Kerakli o'rindagi savatni topib, ichida
// chiziqli bo'lamiz — qiymatlar savatda tekis tarqalgan deb olinadi.
function kvantil(q, savatlar) {
  const jami = savatlar.at(-1).son;
  const orin = q * jami;
  let oldingiLe = 0;
  let oldingiSon = 0;
  for (const { le, son } of savatlar) {
    if (son >= orin) {
      if (le === Infinity) return oldingiLe; // eng katta chegara
      const ulush = (orin - oldingiSon) / (son - oldingiSon);
      return oldingiLe + (le - oldingiLe) * ulush;
    }
    oldingiLe = le;
    oldingiSon = son;
  }
}

// 1000 ta so'rov: le (soniya) → shu vaqtgacha tugaganlar soni
const savatlar = [
  { le: 0.005, son: 120 },
  { le: 0.01, son: 610 },
  { le: 0.025, son: 930 },
  { le: 0.05, son: 985 },
  { le: 0.1, son: 996 },
  { le: 0.25, son: 1000 },
  { le: Infinity, son: 1000 },
];
for (const q of [0.5, 0.95, 0.99]) {
  const ms = kvantil(q, savatlar) * 1000;
  console.log(`p${q * 100}: ${ms.toFixed(1)} ms`);
}

Konsolda:

text
p50: 8.9 ms
p95: 34.1 ms
p99: 72.7 ms

p99 — 990-o'rin. U 50 ms va 100 ms savatlari orasiga tushdi (985 va 996), shu oraliqning ~45 % qismi — 72.7 ms. Muhim xulosa: aniqlik savatlarga bog'liq. Haqiqiy p99 51 ms ham, 99 ms ham bo'lishi mumkin edi. Shuning uchun savatlar sizni qiziqtiradigan chegaralar atrofida zich bo'lishi kerak. Kanon savatlari: 5, 10, 25, 50, 100, 250, 500 ms, 1, 2.5, 5, 10 s — oddiy API so'rovi va sekin AI javobi ikkalasini qamraydi.

Tekshirib ko'ring: SLA "so'rovlarning 99 % i 300 ms dan tez" deydi. Kanon savatlarida 300 ms yo'q (250 va 500 bor). Muammo nimada?

Javob

p99 250 ms va 500 ms orasiga tushsa, Prometheus uni chiziqli taxmin qiladi — 300 ms dan oshdi-oshmadi aniq aytolmaysiz. SLA chegarasi o'lchanadigan bo'lsa, aynan shu qiymatni savat qiling: 0.3. Gistogramma savatlari — kod ichidagi qaror, ularni keyin o'zgartirish eski ma'lumot bilan solishtirishni buzadi.

3.5 Kardinallik: jim portlash

Har noyob yorliqlar to'plami — Prometheus xotirasida alohida qator (time series). Kanon counter'ida yorliqlar: metod × yo'l × status. Taxminan 5 metod, 30 marshrut, 10 status — 1 500 qator, gistogrammada esa har biri yana 12 savat bilan. Bu normal.

Endi yo'l yorlig'iga haqiqiy URL qo'yaylik: /v1/vazifalar/1, /v1/vazifalar/2, … 10 000 vazifa — 10 000 × 5 × 10 = 500 000 qator, gistogrammada esa millionlab. Prometheus xotirasi tugaydi, so'rovlar sekinlashadi. Bu — yuqori kardinallik (high cardinality), metrika tizimlaridagi eng qimmat xato. Undan ham yomoni: hujumchi tasodifiy URL'lar yuborib (/yoq/1, /yoq/2, …) metrika tizimingizni ataylab to'ldira oladi.

Qoidalar:

  • Yorliqqa faqat cheklangan to'plam: metod, marshrut shabloni, status sinfi.
  • Foydalanuvchi id'si, email, sorovId, to'liq URL — hech qachon yorliq emas (ular log va trace uchun).
  • Noma'lum marshrut — bitta umumiy qiymat (boshqa).

3.6 Kanon topgan detal: {/*splat}

Kanon kodi marshrut shablonini req.route.path dan oladi — Express uni so'rov marshrutga mos kelganda to'ldiradi. Birinchi yugurishda noma'lum yo'llar uchun ham shablon chiqdi: {/*splat}. Sabab: Nest 404 javobini o'zining "hammasini ushlovchi" marshruti orqali beradi, uning Express 5 sintaksisidagi yo'li — {/*splat} (Express 5 yo'l sintaksisi). Shuning uchun kodda shart bor: shablon ichida * bo'lsa — boshqa. Biz o'tkazgan sinovda 405 javobi esa yol="/v1/teglar" ga tushdi — chunki u haqiqiy @All() ishlovchisidan qaytadi (marshrut topilgan, metod noto'g'ri).

4. Trace va OpenTelemetry

4.1 Trace va span

Trace (iz yozuvi) — bitta so'rovning butun yo'li. U span'lardan (oraliq) iborat: har span — bitta ish bo'lagi, boshlanish va tugash vaqti bilan. Span'lar daraxt hosil qiladi: HTTP so'rovi (ildiz) → middleware → handler → SQL so'rovi → tashqi API chaqiruvi.

Bir nechta xizmat bo'lsa (API → AI xizmati → baza), trace ularni bitta zanjirga bog'laydi. Buning uchun har chiqayotgan so'rovga W3C standart sarlavhasi qo'shiladi: traceparent: 00-<trace-id>-<span-id>-01. Qabul qilgan xizmat shu trace-id bilan davom etadi. Correlation ID darsidagi X-Request-Id ning standartlashgan, kuchliroq shakli.

OpenTelemetry (OTel) — trace, metrika va loglar uchun ochiq standart va SDK'lar to'plami. Uning qulay tomoni — avto-instrumentatsiya: http, express, pg, redis kabi modullarni o'zi o'rab, span'larni kodni o'zgartirmasdan yasaydi. Span'lar keyin eksportyor orqali tizimga yuboriladi — Jaeger, Grafana Tempo yoki bulut xizmati.

4.2 Kanon tajribasi: narxi

Kanon alohida tajriba loyihasida bir xil Express ilovani (1 ms "ish" + JSON javob) yuklama ostida o'lchadi. Ikki rejim: OTel o'chiq va yoqiq (@opentelemetry/sdk-node 0.300.0, HTTP va Express avto-instrumentatsiyasi, span'lar xotiradagi eksportyorga). autocannon: 10 soniya, 50 ulanish. Kanon chiqishi:

text
OTel o'chiq: 825 req/s, p50 59 ms, p99 84 ms, xatolar 0
OTel YOQIQ : 725 req/s, p50 66 ms, p99 103 ms, xatolar 0
spanlar: 9080; bitta trace ichida: GET [1] 0.93 ms
atributlar (HTTP span): {"http.request.method":"GET","url.scheme":"http","url.path":"/v1/vazifalar/7","http.response.status_code":200}
OTel o'chiq: 900 req/s, p50 53 ms, p99 83 ms, xatolar 0
OTel YOQIQ : 695 req/s, p50 73 ms, p99 105 ms, xatolar 0
spanlar: 8780; bitta trace ichida: GET [1] 0.78 ms
OpenTelemetry narxi: soniyasiga so'rovlar
  • 1-yugurish, o'chiq825 req/s
  • 1-yugurish, OTel725 req/s
  • 2-yugurish, o'chiq900 req/s
  • 2-yugurish, OTel695 req/s

Manba: IlmHamroh kanon o'lchovi, 2026-11-03: Express 5.3.0, @opentelemetry/sdk-node 0.300.0, autocannon 10 s × 50 ulanish, Node 24.21.0; bitta mashina, band (boshqa jarayonlar bilan), har rejim ikki marta

O'tkazuvchanlik 12 % va 23 % kamaydi, p99 esa 84 ms dan 103–105 ms ga oshdi (~1.25 baravar). Bitta mashina, yuklangan — mutlaq raqamga emas, nisbatga qarang. Bu ilova deyarli hech narsa qilmaydi, shuning uchun span yaratish xarajati ko'zga yaqqol tashlanadi. Haqiqiy so'rov 20–50 ms bazada tursa, ulush ancha kichik bo'ladi.

4.3 Natijani halol o'qish: GET [1] va ESM tuzog'i

Kanon chiqishidagi g'alati joy: har trace'da bitta span — GET ([1] — SERVER turi). Express middleware va handler span'lari yo'q, http.route atributi yo'q. Kanon buni "instrumentation-express Express 5 ni qamramaganga o'xshaydi, tekshirilmadi" deb qoldirgan.

Biz tekshirdik. @opentelemetry/instrumentation-express 0.72.0 paketining o'zida qo'llab-quvvatlanadigan versiyalar — express >=4.0.0 <6, ya'ni Express 5 bor. Keyin kichik sinov yozdik: bitta so'rov, span'lar ro'yxati. Express'ni uch xil yuklab ko'rdik:

text
$ node span.mjs import
rejim: import, spanlar: 1
SERVER | GET | ildiz | http.route=-

$ node span.mjs require
rejim: require, spanlar: 3
INTERNAL | middleware - jsonParser | ota bor | http.route=-
SERVER | GET /v1/vazifalar/:id | ildiz | http.route=/v1/vazifalar/:id
INTERNAL | request handler - /v1/vazifalar/:id | ota bor | http.route=/v1/vazifalar/:id

$ node --import ./ilgak.mjs span.mjs import
rejim: import, spanlar: 3
INTERNAL | middleware - jsonParser | ota bor | http.route=-
SERVER | GET /v1/vazifalar/:id | ildiz | http.route=/v1/vazifalar/:id
INTERNAL | request handler - /v1/vazifalar/:id | ota bor | http.route=/v1/vazifalar/:id

Sabab — Express 5 emas, modul tizimi. OTel avto-instrumentatsiyasi CommonJS require ni ushlab, modulni o'rab qo'yadi. ESM import esa boshqa yo'ldan yuklanadi — uni ushlash uchun Node'ga loader ilgagi kerak. Kanon tajribasi Express'ni await import("express") bilan yuklagan — shuning uchun faqat http span'i qoldi (http ni Express o'zi ichida require qiladi). ilgak.mjs — ikki qator:

js
import { register } from "node:module";
register("@opentelemetry/instrumentation/hook.mjs", import.meta.url);

OpenTelemetry'ning rasmiy ESM yo'riqnomasi (2026-11-03 holatida) xuddi shu ilgakni boshqa yo'l bilan ulaydi: node --experimental-loader=@opentelemetry/instrumentation/hook.mjs --import ./telemetry.js app.js. Yangi register() usuli hujjatda hali "kelajakdagi yo'l" deb turibdi. Ikkalasi ham bitta hook.mjs faylini ulaydi; biz register() ni sinadik — natija yuqorida.

Uchinchi yugurishda span daraxti to'liq: SERVER span'ining nomi marshrut shabloni bilan (GET /v1/vazifalar/:id), ostida middleware va handler. Bizning vazifalar-api ham ESM ("type": "module") — OTel ulansa, xuddi shu ilgak kerak bo'ladi.

flowchart TB
  S["SERVER: GET /v1/vazifalar/:id"]
  M["INTERNAL: middleware - jsonParser"]
  H["INTERNAL: request handler"]
  S --> M
  S --> H

Diagramma — uchinchi yugurishdagi bitta trace. Ildiz — HTTP so'rovi, ichida ikki bola. Bazaga ulansa, handler ostida pg span'i paydo bo'lardi (biz sinamadik).

4.4 Ikkinchi tuzoq: jim API o'zgarishi

Sinovni yozayotganda yana bir narsaga duch keldik. @opentelemetry/sdk-trace 3.0.0 da span protsessorlari eksportyorni obyekt ichida oladi: new SimpleSpanProcessor({ exporter }). 2.x dagi kabi new SimpleSpanProcessor(eksportyor) yozsak — xato yo'q, lekin span'lar 0 ta, faqat yopishda:

text
rejim: import, spanlar: 0
TypeError: Cannot read properties of undefined (reading 'shutdown')

Tarjimasi: "aniqlanmagan qiymatning shutdown xususiyatini o'qib bo'lmaydi". Protsessor { exporter } ni kutgan, biz eksportyorning o'zini berdik — u ichidan exporter maydonini qidirdi va undefined topdi. Kanon ham shu tuzoqqa tushgan (BatchSpanProcessor da: "Cannot read properties of undefined (reading 'export')"). Yana bir to'siq: @opentelemetry/sdk-trace-base 2.12 ni @opentelemetry/api 1.10 bilan o'rnatib bo'lmadi — npm ERESOLVE (peer bog'liqlik ziddiyati). OTel ekotizimi tez o'zgaryapti: versiyalarni qotiring va yangilashda changelog'ni o'qing.

4.5 Nega kanonda yo'q

Kanon OTel'ni loyihaga qo'shmadi. Sabablari: /metrics + pino loglari + sorovId bitta xizmat uchun yetarli; trace'ning asosiy foydasi — ko'p xizmatli tizimda, bizda esa bitta API va bitta ishchi. OTLP eksportyor, Jaeger, Tempo, Grafana — sinalmagan (qo'shimcha konteynerlar). Narx — o'lchangan (12–23 %, bo'sh ilovada). OTel chuqur — 34-qismdagi Distributed tracing va OpenTelemetry darsida.

5. Xatolarni kuzatish va alertlar

5.1 Sentry

Log xatoni yozadi, lekin uni hech kim o'qimasligi mumkin. Xato kuzatish tizimi (error tracking) har ushlanmagan xatoni yig'adi, bir xillarini guruhlaydi, stack trace va so'rov kontekstini ko'rsatadi, yangi xato paydo bo'lsa xabar beradi. Eng mashhuri — Sentry (o'z serveringizga qo'yiladigan ochiq muqobili — GlitchTip).

NestJS uchun rasmiy hujjat bo'yicha tartib (hujjatda 2026-11-03 da tekshirildi; biz sinamadik): @sentry/nestjs paketi, alohida instrument.ts faylida Sentry.init({ dsn }) va bu fayl main.ts da birinchi import qilinadi (OTel kabi — modullarni o'rash uchun). DSN — loyihangizning Sentry manzili, u .env da turadi. Uch narsani unutmang: release (qaysi versiyada xato), tracesSampleRate (hamma so'rovni emas, masalan 10 % ini yuborish) va shaxsiy ma'lumotni tozalash (email, token xato kontekstiga tushmasin). Kanonda Sentry yo'q — DSN yo'q, sinalmagan. Chuqur — Sentry chuqur darsida (34-qism).

5.2 Alert: simptomga, sababga emas

Metrika o'zi hech kimni uyg'otmaydi. Alert (ogohlantirish) — qoida: "shart 5 daqiqa davomida rost bo'lsa — navbatchiga xabar". Prometheus'da qoidalar PromQL so'rov tilida yoziladi. Hujjat bo'yicha ikki namuna (sinalmagan, 2026-11):

text
# 5xx ulushi 5 daqiqada 5 % dan oshdi
sum(rate(http_sorovlar_jami{status=~"5.."}[5m]))
  / sum(rate(http_sorovlar_jami[5m])) > 0.05

# p99 1 soniyadan oshdi
histogram_quantile(0.99,
  sum by (le) (rate(http_sorov_davomiyligi_soniya_bucket[5m]))) > 1

rate(…[5m]) — counter'ning so'nggi 5 daqiqadagi soniyasiga o'sishi («Uch tur» bo'limidagi g'oya). histogram_quantile — biz yozgan kvantil funksiyasining o'zi.

Asosiy qoida: alert foydalanuvchi his qiladigan simptomga qo'yiladi (xatolar ulushi, sekinlik), sababga emas (CPU 90 %). CPU 90 % bo'lib, hamma so'rov tez javob olsa — muammo yo'q, tunda uyg'otishning hojati yo'q. Alertlar, SLO va navbatchilik — SLI/SLO, incident va postmortem darsida.

6. Loglar: stdout va stderr

6.1 Kanon topgan ikki xato

Observability ishi paytida kanon 30-qismdan qolgan ochiq masalani ko'rib chiqdi: CLI buyrug'i (npm run buyruq -- eksport csv > fayl) loglarni qayerga yozadi? Tekshiruvda esa yangi xato chiqdi: buyruq umuman ishlamas ekan. Biz ikkalasini loyihaning oldingi bosqichida takrorladik (xotira rejimi, dist/buyruq.js eksport csv; vaqt belgilari va yo'llarni qisqartirdik):

text
--stdout
{"daraja":"warn","time":"…","ilova":"vazifalar-api","msg":"ILOVA_SIRI berilmagan — vaqtinchalik tasodifiy sir (dev): …"}
id,matn,bajarildi,teglar,yaratildi,yangilandi
{"daraja":"info","time":"…","ilova":"vazifalar-api","msg":"ombor ulanishlari yopildi"}
--stderr
Error: so'rov egasi aniqlanmagan (CLS)
    at get egasi (file:///…/dist/vazifalar/vazifalar.service.js:51:19)
    at get ombor (file:///…/dist/vazifalar/vazifalar.service.js:55:34)
    at VazifalarService.royxat (file:///…/dist/vazifalar/vazifalar.service.js:58:21)

Birinchi xato — loglar stdout'da. eksport csv > vazifalar.csv bilan CSV faylning boshi va oxirida JSON qatorlar qoladi — Excel yoki boshqa dastur uni buzilgan fayl deb o'qiydi. Unix odati: stdout — natija (ma'lumot), stderr — diagnostika (log, xato, progress). Shunda > fayl faqat ma'lumotni oladi.

Ikkinchi xato — "so'rov egasi aniqlanmagan (CLS)". 30-qismda VazifalarService egani so'rov kontekstidan oladigan bo'ldi (nestjs-cls). HTTP so'rovida kontekstni middleware ochadi. CLI'da esa HTTP yo'q — kontekst hech qachon ochilmagan. Buyruq o'sha bosqichdan beri yiqilardi, va buni hech kim sezmagan: CLI'ni test qamramagan edi. Tarjimasi: "so'rov egasi aniqlanmagan".

6.2 Tuzatish

Ikkala tuzatish — loyiha qadamida. Natija (biz ishga tushirdik, xuddi shu buyruq):

text
--stdout
id,matn,bajarildi,teglar,yaratildi,yangilandi
--stderr
{"daraja":"warn","time":"…","ilova":"vazifalar-api","msg":"ILOVA_SIRI berilmagan — vaqtinchalik tasodifiy sir (dev): …"}
{"daraja":"info","time":"…","ilova":"vazifalar-api","msg":"ombor ulanishlari yopildi"}

stdout'da faqat CSV (xotira rejimi bo'sh — sarlavha qatori), loglar stderr'da, chiqish kodi 0. Kanon Postgres rejimida ham tekshirdi: stdout — 13 qator toza CSV.

Tekshirib ko'ring: Docker konteynerlarida HTTP server loglari stdout'ga yoziladi va bu to'g'ri hisoblanadi. CLI uchun nega boshqacha?

Javob

Server uchun stdout'ning "natija" vazifasi yo'q — uning natijasi HTTP javob. Shuning uchun loglar stdout'ga ketadi va Docker ularni yig'adi (12-factor odati). CLI'da esa stdout — buyruqning natijasi: uni faylga yoki boshqa dasturga yo'naltirishadi. Log u yerga aralashsa, natija buziladi. Qoida bir: stdout'ga faqat natija, natija bo'lmasa — log ham bo'lishi mumkin.

7. Ko'p uchraydigan xatolar

Xato Oqibat To'g'risi
Yorliqda haqiqiy URL yoki user id Prometheus xotirasi portlaydi marshrut shabloni, boshqa
/metrics ochiq internetda ichki ma'lumot oqadi token yoki ichki tarmoq
O'rtacha vaqtga qarash sekin 1 % ko'rinmaydi p95, p99
SLA chegarasi savatda yo'q p99 taxmini noaniq chegarani savat qilish
CPU'ga alert tunda behuda uyg'otish simptomga: xato, sekinlik
ESM'da OTel ilgaksiz span'lar jim yo'qoladi --import + register()
CLI loglari stdout'da > fayl buziladi loglar stderr'ga

8. Mashqlar

Mashqlar kurs/mashqlar/31/28-kuzatuv/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

1-mashq (oson): bo'shliqlarni to'ldiring

  • Faqat oshadigan metrika turi —
  • RED'dagi D harfi —
  • Yorliqlar to'plamining noyob qiymatlari soni —
  • Trace'dagi bitta ish bo'lagi —
  • CLI buyrug'i loglarini qaysi oqimga yozish kerak —
Yechim
  • counter — gauge esa oshadi va kamayadi.
  • Duration — javob vaqti, odatda gistogramma.
  • kardinallik — yuqori bo'lsa metrika tizimi portlaydi.
  • span — boshlanish, tugash, atributlar va otasi bilan.
  • stderr — stdout esa natija uchun.

2-mashq (o'rta): /metrics dan RED xulosa

/metrics matnidan http_sorovlar_jami qatorlarini o'qib, har yo'l uchun jami so'rovlar va 5xx ulushini (foizda, 2 xonagacha) hisoblang. Ishora: RegExp bilan qatordan yo'l, status va sonni ajrating, Map bilan yo'l bo'yicha yig'ing.

Yechim
js
const matn = `
http_sorovlar_jami{metod="GET",yol="/v1/vazifalar",status="200"} 950
http_sorovlar_jami{metod="GET",yol="/v1/vazifalar",status="503"} 50
http_sorovlar_jami{metod="POST",yol="/v1/vazifalar",status="201"} 300
http_sorovlar_jami{metod="POST",yol="/v1/vazifalar",status="422"} 20
http_sorovlar_jami{metod="GET",yol="/health/ready",status="200"} 720
`;

function redXulosa(matn) {
  const yollar = new Map();
  for (const qator of matn.trim().split("\n")) {
    const m = qator.match(/yol="([^"]+)",status="(\d+)"\} (\d+)/);
    if (m === null) continue;
    const [, yol, status, son] = m;
    const y = yollar.get(yol) ?? { jami: 0, xato5xx: 0 };
    y.jami += Number(son);
    if (status.startsWith("5")) y.xato5xx += Number(son);
    yollar.set(yol, y);
  }
  return [...yollar].map(([yol, y]) => ({
    yol,
    jami: y.jami,
    xatoFoiz: Number((y.xato5xx / y.jami * 100).toFixed(2)),
  }));
}

console.log(redXulosa(matn));

Konsolda:

text
[
  { yol: '/v1/vazifalar', jami: 1320, xatoFoiz: 3.79 },
  { yol: '/health/ready', jami: 720, xatoFoiz: 0 }
]

422 xato emasmi? RED'dagi "Errors" — bizning xatolarimiz (5xx). 4xx — mijozning noto'g'ri so'rovi; uni alohida kuzatish foydali (hujum yoki frontend xatosi belgisi), lekin xizmat sog'lig'i ko'rsatkichiga qo'shilmaydi. Haqiqiy Prometheus'da bu hisob rate() bilan vaqt oralig'ida qilinadi; bu yerda — counter'ning bitta suratida.

3-mashq (qiyin): URL'dan marshrut shabloni

Shablonlar ro'yxati berilgan: /v1/vazifalar, /v1/vazifalar/:id, /v1/vazifalar/:id/qadamlar, /health/ready. yorliq(yol) funksiyasini yozing: URL'ni (so'rov parametrlarisiz) mos shablonga aylantirsin, mos kelmasa — "boshqa". Keyin 1 003 ta so'rovda xom URL va shablon bo'yicha nechta noyob metrika qatori bo'lishini solishtiring. Ishora: shablondan RegExp yasang (:id → [^/]+), Set bilan noyoblarni sanang.

Yechim
js
const SHABLONLAR = [
  "/v1/vazifalar",
  "/v1/vazifalar/:id",
  "/v1/vazifalar/:id/qadamlar",
  "/health/ready",
];

// "/v1/vazifalar/:id" → /^\/v1\/vazifalar\/[^/]+$/
const naqshlar = SHABLONLAR.map((s) => ({
  shablon: s,
  re: new RegExp(`^${s.replace(/:[a-z]+/g, "[^/]+")}$`),
}));

function yorliq(yol) {
  const toza = yol.split("?")[0];
  return naqshlar.find((n) => n.re.test(toza))?.shablon ?? "boshqa";
}

const sorovlar = [];
for (let id = 1; id <= 500; id++) {
  sorovlar.push(`/v1/vazifalar/${id}`);
  sorovlar.push(`/v1/vazifalar/${id}/qadamlar`);
}
sorovlar.push("/v1/vazifalar?limit=20", "/wp-admin.php");
sorovlar.push("/health/ready");

console.log(yorliq("/v1/vazifalar/42"));
console.log(yorliq("/v1/vazifalar/42/qadamlar"));
console.log(yorliq("/wp-admin.php"));
console.log("xom URL qatorlari:", new Set(sorovlar).size);
console.log("shablon qatorlari:", new Set(sorovlar.map(yorliq)).size);

Konsolda:

text
/v1/vazifalar/:id
/v1/vazifalar/:id/qadamlar
boshqa
xom URL qatorlari: 1003
shablon qatorlari: 5

1 003 o'rniga 5 qator — 200 baravar kam. /wp-admin.php — botlar har saytga yuboradigan so'rov; u boshqa ga tushdi va yangi qator ochmadi. Bizning RegExp yo'l ichidagi nuqta kabi maxsus belgilarni qochirmaydi — shablonlarimizda ular yo'q. Kanon kodi bu ishni o'zi qilmaydi: Express shablonni req.route.path da tayyor beradi.

9. Loyiha qadami: /metrics va CLI tuzatishlari

9.1 Paket

bash
npm i --save-exact prom-client@15.1.3

Kanon uni 7 kundan eski versiya bilan o'rnatdi; tranzitiv bog'liqliklari — bintrees, tdigest. Prod bog'liqliklari 42 tadan 43 taga chiqadi.

9.2 src/kuzatuv/metrikalar.ts

Yangi papka va fayl. Kanon faylidagi uzun qatorlarni telefon uchun bo'ldik, izohlarni qisqartirdik, ma'nosi aynan:

ts
// metrikalar.ts — 31-qism #28: Prometheus metrikalari (prom-client).
// RED: Rate (so'rovlar soni), Errors (5xx), Duration (gistogramma);
// USE — jarayon metrikalari (CPU, xotira, event loop kechikishi —
// collectDefaultMetrics). Yorliq (label) kardinalligi: yo'l —
// MARSHRUT SHABLONI (/v1/vazifalar/:id), haqiqiy URL EMAS (har id
// yangi qator — Prometheus xotirasi portlaydi); mos marshrut
// topilmasa — "boshqa".
// /metrics — METRIKA_TOKEN berilsa Bearer talab qilinadi.
import type { NextFunction, Request, Response } from "express";
import {
  Counter, Histogram, Registry, collectDefaultMetrics,
} from "prom-client";

export class Metrikalar {
  readonly reestr = new Registry();
  readonly sorovlar = new Counter({
    name: "http_sorovlar_jami",
    help: "HTTP so'rovlar soni",
    labelNames: ["metod", "yol", "status"] as const,
    registers: [this.reestr],
  });
  readonly davomiylik = new Histogram({
    name: "http_sorov_davomiyligi_soniya",
    help: "HTTP javob vaqti (s)",
    labelNames: ["metod", "yol"] as const,
    buckets: [
      0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10,
    ],
    registers: [this.reestr],
  });

  constructor(standart = true) {
    this.reestr.setDefaultLabels({ ilova: "vazifalar-api" });
    if (standart) collectDefaultMetrics({ register: this.reestr });
  }

  /** Express middleware — Nest marshrutlaridan OLDIN ulanadi;
   *  javob tugaganda (finish) yoziladi — shablon o'shanda ma'lum */
  middleware() {
    return (
      req: Request, res: Response, keyingi: NextFunction,
    ): void => {
      if (req.path === "/metrics") { keyingi(); return; }
      const t0 = process.hrtime.bigint();
      res.on("finish", () => {
        const shablon = (req.route as { path?: unknown } | undefined)
          ?.path;
        // mos marshrut yo'q — Nest'ning 404 "catch-all" yo'li
        // ({/*splat}) yoki shablon yo'q → "boshqa"
        const bor = typeof shablon === "string"
          && !shablon.includes("*");
        const yol = bor ? `${req.baseUrl}${shablon}` : "boshqa";
        const s = Number(process.hrtime.bigint() - t0) / 1e9;
        const status = String(res.statusCode);
        this.sorovlar.inc({ metod: req.method, yol, status });
        this.davomiylik.observe({ metod: req.method, yol }, s);
      });
      keyingi();
    };
  }

  /** GET /metrics (Prometheus matn formati) */
  handler(token: string | undefined) {
    return async (req: Request, res: Response): Promise<void> => {
      const kerak = token !== undefined;
      if (kerak && req.get("authorization") !== `Bearer ${token}`) {
        res.status(401).type("text/plain").send("401\n");
        return;
      }
      res.type(this.reestr.contentType)
        .send(await this.reestr.metrics());
    };
  }
}

To'rt qaror:

  • O'z Registry — global reestr emas. Testlarda har ilova nusxasi o'z metrikalariga ega, bir-biriga aralashmaydi.
  • res.on("finish") — yozuv javob tugaganda: marshrut shabloni ham, status ham shunda aniq. process.hrtime.bigint() — nanosekund aniqlikdagi monoton soat (process obyekti).
  • collectDefaultMetrics testda o'chiq (standart = false) — u taymerlar ochadi va testlar sekinlashadi.
  • Token — METRIKA_TOKEN berilsa, Prometheus Authorization: Bearer … yuborishi kerak. Berilmasa — /metrics ochiq: uni faqat ichki tarmoqqa chiqaring.

9.3 Ulash: src/ilova.ts

Importlarga:

ts
import type { NextFunction, Request, Response } from "express";
import { Metrikalar } from "./kuzatuv/metrikalar.ts";

app.disable("x-powered-by") dan keyin, app.use(sorovId()) dan oldin:

ts
  // 31 #28: Prometheus RED metrikalari va /metrics — Nest
  // guard'laridan tashqarida (METRIKA_TOKEN bilan himoya)
  const metrikalar = new Metrikalar(c.NODE_ENV !== "test");
  app.use(metrikalar.middleware());
  const metrikaYoli = metrikalar.handler(c.METRIKA_TOKEN);
  app.use("/metrics",
    (req: Request, res: Response, keyingi: NextFunction) => {
      if (req.method !== "GET") { keyingi(); return; }
      void metrikaYoli(req, res);
    });

Nega Express darajasida, Nest controller'ida emas? Ikki sabab. Birinchisi — middleware hamma so'rovni ko'rishi kerak, hatto guard rad etganlarini ham (401, 403 — ular ham metrika). Ikkinchisi — /metrics ni global KirishGuard (28-qism auth) bloklamasin: Prometheus sessiya cookie'si bilan kelmaydi. Kanonda bu ikki qator bitta uzun qatorda edi — metrikaYoli o'zgaruvchisiga ajratdik, xulq bir xil.

9.4 Sozlama

src/config.ts — YOPISH_KECHIKISH_MS dan keyin:

ts
    /** 31 #28: /metrics uchun Bearer token (berilmasa — kirishsiz;
     *  u holda /metrics faqat ichki tarmoqda ochiq bo'lsin!) */
    METRIKA_TOKEN: z.string().min(16).optional(),

.min(16) — "123" kabi taxmin qilinadigan token bilan ilova ishga tushmaydi. .env.example ga:

bash
# /metrics uchun Bearer (berilmasa — kirishsiz: faqat ichki tarmoq)
# METRIKA_TOKEN=

9.5 Test

test/kuzatuv.test.ts ga ikkinchi test. Kanonda tekshiruvlar assert.match va RegExp bilan; biz telefon uchun includes ga o'girdik — ma'nosi bir xil:

ts
test("#28 /metrics: RED — yo'l yorlig'i marshrut SHABLONI, "
  + "mos kelmagan — 'boshqa'; METRIKA_TOKEN bo'lsa Bearer talab",
  async () => {
    const T = "metrika-token-lokal-sinov";
    const app = await ilova({ METRIKA_TOKEN: T });
    const http = app.getHttpServer();
    await request(http).post("/v1/vazifalar")
      .send({ matn: "Metrika" }).expect(201);
    await request(http).get("/v1/vazifalar/1").expect(200);
    await request(http).get("/v1/vazifalar/2").expect(404);
    await request(http).get("/yoq/manzil/12345").expect(404);
    await request(http).get("/metrics").expect(401);
    const m = (await request(http).get("/metrics")
      .set("Authorization", `Bearer ${T}`).expect(200)).text;
    const id = 'metod="GET",yol="/v1/vazifalar/:id"';
    const ilv = 'ilova="vazifalar-api"';
    assert.ok(m.includes(
      `http_sorovlar_jami{${id},status="200",${ilv}} 1`));
    assert.ok(m.includes(
      `http_sorovlar_jami{${id},status="404",${ilv}} 1`));
    assert.ok(m.includes('yol="boshqa",status="404"'));
    // haqiqiy URL yorliqqa tushmaydi (kardinallik)
    assert.ok(!m.includes("12345"));
    const b = 'http_sorov_davomiyligi_soniya_bucket{le="0.005"';
    assert.ok(m.includes(b));
    await app.close();
  });

Eng muhim qator — !m.includes("12345"): test kardinallik qoidasini qotiradi. Kimdir keyinchalik "qulayroq" deb yorliqqa req.originalUrl qo'ysa, test yiqiladi.

9.6 CLI: src/buyruq.ts

Importlarga:

ts
import type { Config } from "./config.ts";
import { ClsService } from "nestjs-cls";
import { CLS_EGASI, CONFIG } from "./tokenlar.ts";
import { pino } from "pino";
import { logYarat } from "./umumiy/log.ts";

const LOG_DARAJALARI = new Set([
  "fatal", "error", "warn", "info", "debug", "trace", "silent",
]);

Kontekst yaratish — log stderr'ga (dest: 2 — fayl deskriptori 2, ya'ni stderr), keyin CLS konteksti qo'lda ochiladi:

ts
  // 31: log — STDERR'ga. stdout — buyruq natijasi (eksport > fayl);
  // avval pino logi stdout'ga tushib, CSV ichida JSON qolardi
  const daraja = process.env["LOG_LEVEL"] ?? "";
  const log = logYarat(
    LOG_DARAJALARI.has(daraja) ? daraja : "info",
    pino.destination({ dest: 2, sync: true }),
  );
  ctx = await NestFactory.createApplicationContext(
    AppModule.forRoot({
      idempotentlikOmbori: new XotiraIdempotentlikOmbori(), log,
    }),
    { logger: ["error", "warn"], abortOnError: false },
  );
} catch (x) {
  // … o'zgarmadi
}
// 31: 30-qism B dan beri VazifalarService egasini CLS'dan oladi
// (so'rov kontekstida). CLI'da so'rov yo'q → "egasi aniqlanmagan
// (CLS)" bilan yiqilardi. Kontekst qo'lda: egasi — EGASI_ID (config)
const cls = ctx.get(ClsService);
cls.enter();
cls.set(CLS_EGASI, ctx.get<Config>(CONFIG).EGASI_ID);

cls.enter() — joriy asinxron zanjir uchun yangi kontekst ochadi; undan keyingi hamma await lar shu kontekstni ko'radi. sync: true — CLI qisqa yashaydi, log yozilmay qolib ketmasin. LOG_DARAJALARI — LOG_LEVEL=abc kabi xato qiymat pino'ni yiqitmasin. Kanonda log yaratish ham bitta uzun qatorda edi — daraja va log o'zgaruvchilariga ajratdik.

9.7 Tekshiruv

bash
npm run tip
npx vitest run test/kuzatuv.test.ts test/marshrutlar.test.ts
npm run build && SAQLASH=xotira node dist/buyruq.js eksport csv

Biz loyiha nusxasida (Node 24.21.0, bazasiz): tsc --noEmit xatosiz,

text
 ✓ |asosiy| test/marshrutlar.test.ts (2 tests) 633ms
 ✓ |asosiy| test/kuzatuv.test.ts (2 tests) 692ms
 Test Files  2 passed (2)
      Tests  4 passed (4)

buyruq chiqishi — «Tuzatish» bo'limidagi kabi: stdout'da faqat CSV. Kanon Postgres va Redis bilan jonli tekshiruvida /metrics ni ham o'qidi — AI yo'li ham shablon bilan:

text
http_sorovlar_jami{metod="POST",yol="/v1/auth/kirish",status="200",ilova="vazifalar-api"} 1
http_sorovlar_jami{metod="GET",yol="/v1/vazifalar",status="200",ilova="vazifalar-api"} 1
http_sorovlar_jami{metod="POST",yol="/v1/vazifalar/:id/qadamlar",status="200",ilova="vazifalar-api"} 2

Hali commit qilmang — bosqichning yana ikki darsi bor.

10. Real ishda

  • Stek: Prometheus (yig'ish) + Grafana (panel) + Alertmanager (xabar) — ochiq manbali standart. Loglar uchun Loki, trace'lar uchun Tempo yoki Jaeger. Bulutda — Datadog, New Relic, Grafana Cloud. Hammasi 34-qismda.
  • Qayerda uchraydi: har prodakshn xizmatida /metrics va panel bor. Navbatchi muhandis birinchi navbatda RED paneliga qaraydi.
  • Intervyu savollari: "Log, metrika va trace farqi?", "Counter va gauge farqi?", "Nega o'rtacha emas, p99?", "Yuqori kardinallik nima va undan qanday saqlanasiz?", "OpenTelemetry nima beradi?"
  • AI xususiyatlari: token sarfi va AI javob vaqti ham metrika bo'lishi kerak. Kanonda tokenlar hozircha logda (ai: { tokenlar }) — metrikaga o'tkazish keyingi qadam.

Xulosa

  • Log — bitta so'rov, metrika — umumiy manzara, trace — vaqt qayerga ketdi; ularni so'rov identifikatori bog'laydi.
  • Xizmatga RED (so'rovlar, xatolar, davomiylik), resursga USE; counter, gauge, gistogramma.
  • p99 gistogramma savatlaridan taxmin qilinadi — savatlar muhim chegaralar atrofida zich bo'lsin.
  • Yorliqda faqat cheklangan to'plam: marshrut shabloni, boshqa; haqiqiy URL — hech qachon.
  • OTel avto-instrumentatsiyasi ESM'da loader ilgagisiz jim ishlamaydi; kanon o'lchovida narxi 12–23 % o'tkazuvchanlik.
  • Alert simptomga qo'yiladi; CLI'da stdout — natija, loglar — stderr.

Keyingi dars: Resilience naqshlari — tashqi xizmat (AI provayderi) sekinlashsa yoki yiqilsa, ilovamiz yiqilmasligi uchun timeout, retry, circuit breaker va bulkhead.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Observability: metrikalar, trace va xatolarni kuzatish — prom-client, OpenTelemetry — IlmHamroh