IlmHamroh
JavaScript Full-stack/30-qism. NestJS30/65-dars19 daqiqa
Mundarija (30)

NestJS so'rov konteksti: nestjs-cls, ClsService va AsyncLocalStorage

Qisqacha: nestjs-cls — AsyncLocalStorage ustidagi Nest moduli. So'rov boshida middleware kontekst ochadi: so'rov ID, joriy foydalanuvchi, so'rov logger'i. Keyin istalgan singleton xizmat ClsService orqali cls.get("kalit") va cls.getId() bilan shu so'rovning qiymatini oladi — req ni uzatmasdan va REQUEST scope'siz. Parallel so'rovlar aralashmaydi. So'rovdan tashqarida (cron, navbat) kontekstni cls.run(...) ochadi.

Bu darsda

  • REQUEST scope nega qimmat ekanini va CLS uni qanday almashtirishini haqiqiy hisob bilan ko'rasiz.
  • ClsModule.forRoot sozlamalarini (global, middleware, generateId, idGenerator, setup) ishlatasiz.
  • ClsService ning get, set, getId, isActive, run metodlarini bilasiz.
  • ClsMiddleware ni nega Nest marshrutlaridan oldin, app.use bilan ulash kerakligini tushuntirasiz.
  • vazifalar-api dagi o'z AsyncLocalStorage imizni nestjs-cls ga almashtirib, config/log/kontekst qadamini commit qilasiz.

Oldin bilishingiz kerak: AsyncLocalStorage, Correlation ID va so'rov kuzatuvi, Provider scope'lari, NestJS Logger va nestjs-pino.

O'tgan darsda Nest'ning tizim loglari bizning pino orqali chiqadigan bo'ldi. Endi oxirgi qism: xizmat ichidagi log qaysi so'rovga tegishli ekanini qanday bilamiz.

1. Nega bu kerak?

25-qismda bir muammoni hal qilgandik. VazifalarService.yarat ichida "vazifa yaratildi" deb log yozamiz. Lekin bir vaqtda o'nta so'rov kelsa — qaysi qator qaysi so'rovniki? Javob — correlation ID: har log qatorida so'rovning sorovId si (Correlation ID).

Uni qanday yetkazamiz? req ni har funksiyaga parametr qilib uzatish — kodni iflos qiladi. 24-qismdagi AsyncLocalStorage bu muammoni hal qiladi: so'rov boshida kontekst.run(req.log, next), keyin istalgan chuqurlikda kontekst.getStore() (AsyncLocalStorage). vazifalar-api da bu 10 qatorlik o'z kodimiz edi.

Nest'da yana bir yo'l bor: REQUEST scope. Lekin u qimmat. Keyinroq, 28-qismdagi auth Nest'ga ko'chganda, kontekstga yana bir narsa qo'shiladi — so'rov egasi (Passport NestJS'da). Bunday muhim narsa uchun Nest ekotizimidagi tayyor, sinalgan vosita kerak.

O'xshatish: kasalxonadagi bilaguzuk. Bemorga kirishda bilaguzuk taqiladi: ism, raqam. Keyin har shifokor, har laborant tarixni so'ramasdan bilaguzukka qaraydi. Bemorni bo'limdan bo'limga olib borishda qog'oz uzatish shart emas. CLS — so'rovning bilaguzugi.

2. REQUEST scope'ning narxi

2.1 Tajriba

Provider scope'lari darsida Scope.REQUEST ni ko'rgandik: provider har so'rovda yangidan yaratiladi va @Inject(REQUEST) bilan so'rovni oladi. Ikki yondashuvni demo loyihada solishtirdik (biz ishga tushirdik: NestJS 12.2.0, nestjs-cls 7.0.1).

REQUEST scope bilan:

ts
@Injectable({ scope: Scope.REQUEST })
class RSalom {
  constructor(@Inject(REQUEST) private readonly req: Request) {
    rYaratildi++;
  }
  salom(): string {
    return `salom, ${String(this.req.headers["x-ism"])}`;
  }
}

CLS bilan — oddiy singleton:

ts
@Injectable()
class SalomXizmati {
  constructor(private readonly cls: ClsService) {
    yaratildi++;
  }
  async salom(): Promise<string> {
    await new Promise((r) => setTimeout(r, 20));
    const ism = String(this.cls.get("ism"));
    return `${this.cls.getId()} → salom, ${ism}`;
  }
}

Uchta parallel so'rov (x-request-id va x-ism sarlavhalari bilan), keyin nusxalar soni:

text
id-1 → salom, Ali
id-2 → salom, Malika
id-3 → salom, Jasur
SalomXizmati nusxalari: 1

REQUEST scope'li variantda ketma-ket uch so'rov:

text
REQUEST scope: RSalom nusxalari 3 so'rovda: 3

2.2 Farqni o'qiymiz

  • CLS: bitta nusxa, uch so'rov — har biri o'z qiymatini oldi. setTimeout(20) ataylab qo'yilgan: uchala so'rov bir vaqtda "uxlab", keyin davom etdi. Shunda ham id-1 Ali'ga, id-3 Jasur'ga tegishli qoldi. Aralashmadi.
  • REQUEST scope: har so'rov — yangi nusxa. Uch so'rovda uchta. Ming so'rovda — ming nusxa va ming marta constructor.

Eng qimmati — ko'piklanish (scope bubbling): REQUEST scope'li provider'ni inject qilgan har provider ham REQUEST scope'li bo'lib qoladi. RSalom ni controller oldi — controller ham har so'rovda yangidan yaratiladi. Agar log xizmati REQUEST scope'li bo'lsa, unga tayanadigan hamma narsa — xizmatlar, omborlar, controller'lar — har so'rovda qayta quriladi. Bu Provider scope'lari darsidagi qoida.

CLS bu muammoni yo'qotadi: xizmatlar singleton qoladi, qiymat esa AsyncLocalStorage orqali so'rovga bog'lanadi.

Tekshirib ko'ring: VazifalarService → VazifaOmbori → LogXizmati. LogXizmati ni Scope.REQUEST qildingiz. 100 so'rovda VazifalarService nechta nusxada yaratiladi?

Javob

100 ta (va controller ham 100 ta). Scope pastdan yuqoriga ko'piklanadi: LogXizmati REQUEST bo'lsa, unga tayangan VazifaOmbori ham, unga tayangan VazifalarService ham REQUEST bo'ladi. CLS bilan esa hammasi — bittadan.

3. nestjs-cls asoslari

3.1 Modulni ulash

ts
ClsModule.forRoot({
  global: true,
  middleware: {
    mount: true,
    generateId: true,
    idGenerator: (req: Request) =>
      String(req.headers["x-request-id"] ?? "yangi-id"),
    setup: (cls, req: Request) => {
      cls.set("ism", req.headers["x-ism"] ?? "mehmon");
    },
  },
}),
Sozlama Nima qiladi
global ClsService hamma modulda (import'siz)
middleware.mount kontekstni ochadigan middleware'ni Nest o'zi ulasinmi
generateId har kontekstga ID berish (cls.getId())
idGenerator ID qanday olinadi (sukut — tasodifiy)
setup kontekst ochilganda birinchi qiymatlarni qo'yish

mount: true — ClsMiddleware Nest middleware'i sifatida hamma marshrutga ulanadi. Middleware'dan tashqari guard: { mount: true } va interceptor: { mount: true } variantlari ham bor — WebSocket, mikroservis kabi HTTP'dan boshqa transportlar uchun. Ular hujjatda; biz HTTP'da middleware bilan sinadik.

3.2 ClsService metodlari

Metod Nima
cls.set(kalit, qiymat) joriy kontekstga yozish
cls.get(kalit) joriy kontekstdan o'qish
cls.getId() kontekst ID si (generateId)
cls.isActive() hozir kontekst ichidamizmi
cls.run(fn) yangi kontekst ochib, fn ni ichida bajarish

3.3 So'rovdan tashqarida

Xuddi shu demoda, ilova ichida, lekin so'rovdan tashqarida:

ts
const cls = app.get(ClsService);
console.log("so'rovdan tashqarida isActive:", cls.isActive());
await cls.run(async () => {
  cls.set("ism", "cron");
  const ism = String(cls.get("ism"));
  console.log("cls.run ichida:", cls.isActive(), ism);
});
text
so'rovdan tashqarida isActive: false
cls.run ichida: true cron

So'rovdan tashqarida kontekst yo'q — isActive() yolg'on. Bu holat real: rejalashtirilgan vazifa, navbat ishchisi, ishga tushishdagi kod. U yerda cls.get(...) kutilmagan natija beradi. Yechim — cls.run: o'zingiz kontekst ochasiz. 24-qismdagi kontekst.run(qiymat, fn) ning aynan o'zi.

Diqqat: cls.get ni chaqirishdan oldin isActive() ni tekshiring yoki sukut qiymat bering. Kanondagi joriyLog shunday qiladi: kontekst bo'lmasa — asosiy logger.

3.4 DI'siz kirish: ClsServiceManager

Ba'zan ClsService ni constructor'da olib bo'lmaydi — masalan, oddiy funksiya ichida. ClsServiceManager.getClsService() — statik kirish:

ts
const cls = ClsServiceManager.getClsService();

Kanon joriyLog funksiyasida shuni ishlatadi: u klass emas, 25-qismdan qolgan funksiya. Bu DI qoidasidan chekinish, lekin ongli: joriyLog o'nlab joyda chaqiriladi va uning imzosi o'zgarmasligi kerak edi.

4. Kontekst qayerda ochilsin?

4.1 Tartib muammosi

O'tgan darsda muhim saboqni oldik: Nest modul middleware'i statik fayl va yaroqsiz JSON so'rovlarigacha yetib bormaydi. CLS ga ham shu qoida tegadi. mount: true bilan kontekst Nest marshrutlariga yaqin ochiladi — app.use middleware'lari undan oldin ishlaydi va kontekstsiz qoladi.

vazifalar-api da app.use middleware'lari ko'p: sorovId, pino-http, helmet, cors, statik fayllar, express.json, tanaXatosi. Masalan, yaroqsiz JSON xatosi — tanaXatosi — Problem Details javobida sorovId ni yozadi va logga chiqaradi. Bunga kontekst kerak.

flowchart LR
  A["sorovId()"] --> B["pino-http: req.log"]
  B --> C["ClsMiddleware: log → CLS"]
  C --> D["helmet, cors, statik"]
  D --> E["express.json, tanaXatosi"]
  E --> F["Nest: guard, pipe, controller, xizmat"]

Tartibga qarang: ClsMiddleware pino-http dan keyin turadi — unga tayyor req.log kerak. Qolgan hamma narsa undan keyin, kontekst ichida.

4.2 Kanon yechimi

Kanon mount: false qo'yadi va middleware'ni o'zi, app.use bilan, kerakli joyga ulaydi:

ts
  app.use(sorovLogi(l));
  app.use(
    new ClsMiddleware({
      generateId: true,
      idGenerator: (req: express.Request) => req.sorovId,
      setup: (cls, req: express.Request) => {
        cls.set(CLS_LOG, req.log);
      },
    }).use,
  );
  • idGenerator — tasodifiy ID emas, 25-qismdagi sorovId (u X-Request-Id sarlavhasidan yoki yangi UUID). Bitta so'rovning ID si hamma joyda bir xil.
  • setup — kontekstga so'rov logger'ini qo'yadi. Bu pino-http yaratgan bola logger: unda sorovId allaqachon bog'langan.
  • .use — klass nusxasining metodi; Express middleware'i funksiya kutadi.

Maslahat: 28-qismdagi auth Nest'ga ko'chganda shu kontekstga so'rov egasi ham qo'shiladi: auth middleware'i egasining ID'sini cls.set(CLS_EGASI, …) bilan yozadi, xizmatlar uni cls.get bilan oladi (Passport NestJS'da). Ko'p ijarachili (multi-tenant) tizimlarda — tenantId: so'rovning sarlavhasi yoki subdomeni bo'yicha setup da qo'yiladi va ombor har so'rovni shu ijarachiga cheklaydi. Kalit qancha bo'lmasin, naqsh bitta.

5. Express'da qanday edi, Nest'da qanday

25–27 (o'z ALS) Nest (nestjs-cls)
Kontekst ochish kontekst.run(req.log, next) ClsMiddleware
O'qish kontekst.getStore() cls.get("log")
Qiymatlar bitta (logger) istalgancha kalit
So'rov ID o'zimiz generateId + getId()
Tranzaksiya — @nestjs-cls/transactional

Oxirgi qator — eng katta yutuq. nestjs-cls ning plagini tranzaksiyani kontekst orqali uzatadi: bir nechta xizmat chaqiruvi bitta tranzaksiyada, tranzaksiya obyektini parametr qilmasdan. Buni Tranzaksiyalar NestJS'da darsida o'lchab ko'ramiz.

Nima to'ladik: o'zimizning 10 qatorimiz o'rniga bitta prod bog'liqlik. Xulq o'zgarmadi — log.test.ts dagi correlation testlari (xizmat logi, parallel so'rovlar, fon eksport) bir harf o'zgartirilmasdan o'tdi (kanon tekshiruvi).

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
cls.get — undefined cron'da so'rovdan tashqarida kontekst yo'q cls.run(...) yoki isActive() tekshiruvi
400 javobida sorovId yo'q mount: true — kontekst kech ochilgan mount: false + app.use(new ClsMiddleware(...).use)
req.log CLS da undefined ClsMiddleware pino-http dan oldin tartib: pino-http → ClsMiddleware
Controller har so'rovda yaratiladi zanjirda Scope.REQUEST provider CLS bilan singleton'ga qaytaring
Nest can't resolve dependencies … ClsService global yo'q, ClsModule import qilinmagan global: true

7. Mashqlar

Mashqlar kurs/mashqlar/30/30-nestjs-cls/ da.

1-mashq (oson): metodni toping

To'ldiring:

  • Kontekst ID si: cls. ()
  • Cron ichida kontekst ochish: cls. (async () => { ... })
  • Kanonda ClsMiddleware qaysi middleware'dan keyin ulanadi?
  • Uch so'rovda CLS'li singleton xizmat nechta nusxada?
Yechim
  • getId — generateId: true bo'lsa.
  • run — 24-qismdagi AsyncLocalStorage.run kabi.
  • pino-http — setup ga tayyor req.log kerak.
  • 1 — tajribadagi SalomXizmati nusxalari: 1.

2-mashq (o'rta): ClsService modeli

Node'da (node:async_hooks) kichik Cls klassini yozing: run(fn) — yangi bo'sh Map bilan kontekst ochadi va ID beradi (#keyingi hisoblagichdan: k-1, k-2, …); set, get, getId, isActive. Sinov: uchta parallel "so'rov" — har biri set("ism", …), tasodifiy bo'lmagan kechikish (setTimeout), keyin get. Kontekstdan tashqarida isActive() ni chiqaring. Ishora: AsyncLocalStorage (AsyncLocalStorage), getStore() kontekstdan tashqarida undefined.

Yechim
js
import { AsyncLocalStorage } from "node:async_hooks";

class Cls {
  #als = new AsyncLocalStorage();
  #keyingi = 0;
  run(fn) {
    const store = new Map([["id", `k-${++this.#keyingi}`]]);
    return this.#als.run(store, fn);
  }
  isActive() { return this.#als.getStore() !== undefined; }
  getId() { return this.#als.getStore()?.get("id"); }
  get(kalit) { return this.#als.getStore()?.get(kalit); }
  set(kalit, qiymat) {
    const store = this.#als.getStore();
    if (store === undefined) throw new Error("kontekst yo'q");
    store.set(kalit, qiymat);
  }
}

const cls = new Cls();
const kut = (ms) => new Promise((r) => setTimeout(r, ms));

async function salom(ism, ms) {
  cls.set("ism", ism);
  await kut(ms);
  return `${cls.getId()} → salom, ${cls.get("ism")}`;
}

const natija = await Promise.all([
  cls.run(() => salom("Ali", 30)),
  cls.run(() => salom("Malika", 10)),
  cls.run(() => salom("Jasur", 20)),
]);
console.log(natija.join("\n"));
console.log("tashqarida:", cls.isActive(), cls.get("ism"));

Konsolda:

text
k-1 → salom, Ali
k-2 → salom, Malika
k-3 → salom, Jasur
tashqarida: false undefined

Kechikishlar ataylab har xil: Malika'ning "so'rovi" birinchi tugadi, Ali'niki oxirgi. Shunda ham har biri o'z ismini oldi — await dan keyin AsyncLocalStorage kontekstni tiklaydi. nestjs-cls ichida ham AsyncLocalStorage; u ustiga DI, middleware va plaginlarni qo'shadi.

3-mashq (qiyin): scope ko'piklanishini hisoblash

Provider'lar grafigi berilgan: { nom: [bog'liqliklar] } va REQUEST scope'li provider'lar to'plami. requestScope(graf, asl) funksiyasini yozing: qaysi provider'lar natijada REQUEST scope'li bo'lishini qaytarsin (bog'liqligi REQUEST bo'lsa — o'zi ham). Keyin nusxalar(graf, asl, sorovlar) — har provider nechta nusxada yaratilishini: REQUEST bo'lsa sorovlar ta, aks holda 1. Sinov: Controller → Xizmat → Ombor → Log va Xizmat → Soat; avval Log REQUEST, keyin hech biri, keyin Soat. Ishora: o'zgarish bo'lmaguncha takrorlanadigan sikl (while) va Set.

Yechim
js
const graf = {
  Controller: ["Xizmat"],
  Xizmat: ["Ombor", "Soat"],
  Ombor: ["Log"],
  Log: [],
  Soat: [],
};

function requestScope(graf, asl) {
  const req = new Set(asl);
  let ozgardi = true;
  while (ozgardi) {
    ozgardi = false;
    for (const [nom, deps] of Object.entries(graf)) {
      if (!req.has(nom) && deps.some((d) => req.has(d))) {
        req.add(nom);
        ozgardi = true;
      }
    }
  }
  return req;
}

function nusxalar(graf, asl, sorovlar) {
  const req = requestScope(graf, asl);
  return Object.keys(graf)
    .map((nom) => `${nom}: ${req.has(nom) ? sorovlar : 1}`)
    .join(", ");
}

console.log(nusxalar(graf, ["Log"], 100));
console.log(nusxalar(graf, [], 100));
console.log(nusxalar(graf, ["Soat"], 100));

Konsolda:

text
Controller: 100, Xizmat: 100, Ombor: 100, Log: 100, Soat: 1
Controller: 1, Xizmat: 1, Ombor: 1, Log: 1, Soat: 1
Controller: 100, Xizmat: 100, Ombor: 1, Log: 1, Soat: 100

Birinchi qator — «REQUEST scope'ning narxi» bo'limidagi savolning javobi: bitta Log butun zanjirni 100 nusxaga aylantirdi, faqat unga tayanmagan Soat bitta qoldi. Uchinchi qator qiziqroq: Soat REQUEST bo'lsa, Ombor va Log zararsiz — ular Soat ga tayanmaydi. Ko'piklanish faqat yuqoriga, iste'molchilar tomon ketadi. Sikl o'zgarish bo'lmaguncha aylanadi: har aylanishda bir qavat yuqoriga ko'tariladi.

8. Loyiha qadami: nestjs-cls va commit

Config, log va so'rov konteksti qadamining oxirgi qismi. Bu darsda — kontekst, keyin butun qadam commit qilinadi.

8.1 Paket

bash
npm install --save-exact nestjs-cls@7.0.1

8.2 log.ts — o'z AsyncLocalStorage imiz ketadi

diff
-import { AsyncLocalStorage } from "node:async_hooks";
+import { ClsServiceManager } from "nestjs-cls";
diff
-const kontekst = new AsyncLocalStorage<Log>();
-
-export function joriyLog(asosiy: Log): Log {
-  return kontekst.getStore() ?? asosiy;
-}
+/** CLS kalitlari (nestjs-cls): joriy so'rov logger'i */
+export const CLS_LOG = "log";
+
+/** Joriy so'rovning logger'i (sorovId bilan); so'rovdan tashqarida
+ *  (bootstrap, cron) — asosiy logger */
+export function joriyLog(asosiy: Log): Log {
+  const cls = ClsServiceManager.getClsService();
+  const joriy = cls.isActive()
+    ? cls.get<Log | undefined>(CLS_LOG)
+    : undefined;
+  return joriy ?? asosiy;
+}

sorovLogi endi bitta middleware qaytaradi — kontekstni u ochmaydi:

diff
-export function sorovLogi(log: Log): RequestHandler[] {
-  const http = pinoHttp({
+export function sorovLogi(log: Log): RequestHandler {
+  return pinoHttp({
     logger: log,

Funksiya oxiridagi massiv o'chadi:

diff
-  return [
-    http,
-    (req, _res, next) => {
-      kontekst.run(req.log, next);
-    },
-  ];
 }

joriyLog imzosi o'zgarmadi — uni chaqiradigan o'nlab joy (xizmatlar, filtr, eksport) tegilmaydi. Yana ikki qatorda (customSuccessMessage, customErrorMessage) String(req.method) → req.method — kanondagi kichik tozalash, ma'nosi bir xil.

8.3 app.module.ts — yakuniy ko'rinish

ClsModule.forRoot({ global: true, middleware: { mount: false } }) qo'shiladi. Uch darsdagi o'zgarishlardan keyin fayl to'liq (kanon, telefon uchun formatlangan, izohlar qisqartirildi):

ts
// app.module.ts — ildiz modul: ilova shu moduldan boshlab yig'iladi.
// forRoot(...) — dinamik modul: ombor tayyor berilsa (testlar) — shu,
// aks holda config bo'yicha OmborModule.forRootAsync() yaratadi.
// Config — BogliqliklarModule ichida; nestjs-pino (Nest logi — JSON,
// bizning pino orqali); nestjs-cls (so'rov konteksti)
import { Module } from "@nestjs/common";
import type {
  DynamicModule, MiddlewareConsumer, NestModule,
} from "@nestjs/common";
import { APP_FILTER, APP_GUARD, APP_PIPE } from "@nestjs/core";
import { LoggerModule } from "nestjs-pino";
import { ClsModule } from "nestjs-cls";
import { BogliqliklarModule } from "./bogliqliklar.module.ts";
import type { TashqiBogliqliklar } from "./bogliqliklar.module.ts";
import { OmborModule } from "./ombor.module.ts";
import { VazifalarModule } from "./vazifalar/vazifalar.module.ts";
import { EksportModule } from "./eksport/eksport.module.ts";
import { SxemaPipe } from "./umumiy/sxema.pipe.ts";
import { JsonTanaGuard } from "./umumiy/json-tana.guard.ts";
import { ProblemDetailsFilter } from "./umumiy/problem.filter.ts";
import { eskirgan, ESKI_YOLLAR } from "./umumiy/eskirgan.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";
import { LOG } from "./tokenlar.ts";
import type { Log } from "./umumiy/log.ts";

export type IlovaBogliqliklari = TashqiBogliqliklar & {
  ombor?: VazifaOmbori | undefined;
};

@Module({})
export class AppModule implements NestModule {
  static forRoot({ ombor, ...b }: IlovaBogliqliklari): DynamicModule {
    return {
      module: AppModule,
      imports: [
        BogliqliklarModule.forRoot(b),
        // Nest tizim loglari (bootstrap, ExceptionHandler) — bizning
        // pino orqali JSON. useExisting: pino-http'ni o'zimiz app.use
        // bilan ulaganmiz (ilova.ts) — Nest marshrutlaridan OLDIN
        LoggerModule.forRootAsync({
          inject: [LOG],
          useFactory: (log: Log) => ({
            pinoHttp: { logger: log },
            useExisting: true,
          }),
        }),
        // so'rov konteksti (AsyncLocalStorage) — middleware'ni
        // ilova.ts da app.use bilan erta ulaymiz (mount: false)
        ClsModule.forRoot({
          global: true,
          middleware: { mount: false },
        }),
        ombor === undefined
          ? OmborModule.forRootAsync()
          : OmborModule.forRoot(ombor),
        VazifalarModule,
        EksportModule,
      ],
      // global enhancer'lar DI bilan: Standard Schema tekshiruvi,
      // 415 guard'i, Problem Details filtri
      providers: [
        { provide: APP_PIPE, useClass: SxemaPipe },
        { provide: APP_GUARD, useClass: JsonTanaGuard },
        { provide: APP_FILTER, useClass: ProblemDetailsFilter },
      ],
    };
  }

  // eski versiyasiz yo'llar: Deprecation + Sunset sarlavhalari bilan
  configure(consumer: MiddlewareConsumer): void {
    consumer.apply(eskirgan(ESKI_YOLLAR)).forRoutes("*splat");
  }
}

8.4 ilova.ts — yakuniy ko'rinish

ClsMiddleware sorovLogi dan keyin qo'shiladi, import — CLS_LOG. Fayl to'liq:

ts
// ilova.ts — Nest ilovasi (25-qism ilova.ts ning o'rni): Express
// middleware'lari → Nest marshrutlari → Problem Details filtri.
// listen() YO'Q — server ishga-tushir.ts da (testlar — supertest).
// Sozlamalar DI'dagi Config'dan (ConfigModule) — parametrlar faqat
// testlarda ustidan yozish uchun
import { NestFactory } from "@nestjs/core";
import { VERSION_NEUTRAL, VersioningType } from "@nestjs/common";
import type {
  NestExpressApplication,
} from "@nestjs/platform-express";
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { join } from "node:path";
import { Logger } from "nestjs-pino";
import { ClsMiddleware } from "nestjs-cls";
import { AppModule } from "./app.module.ts";
import { sorovId } from "./umumiy/sorov-id.ts";
import { XotiraIdempotentlikOmbori } from "./umumiy/idempotentlik.ts";
import type { IdempotentlikOmbori } from "./umumiy/idempotentlik.ts";
import { JSON_TURLARI } from "./umumiy/json-kerak.ts";
import { tanaXatosi } from "./umumiy/tana-xatosi.ts";
import { CLS_LOG, sorovLogi } from "./umumiy/log.ts";
import type { Log } from "./umumiy/log.ts";
import type { Xabarchi } from "./umumiy/webhook.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";
import type { Config } from "./config.ts";
import { CONFIG, LOG } from "./tokenlar.ts";

export type Bogliqliklar = {
  /** tayyor ombor (testlar); berilmasa — config bo'yicha
   *  OmborModule.forRootAsync() yaratadi va yopadi */
  ombor?: VazifaOmbori;
  /** tayyor config; berilmasa — process.env dan */
  config?: Config;
  /** quyidagilar berilsa — config'dagi qiymat o'rniga (testlar) */
  corsOriginlar?: readonly string[];
  idempotentlikOmbori?: IdempotentlikOmbori;
  log?: Log;
  xabarchi?: Xabarchi;
  tz?: string;
  trustProxy?: boolean | number | string;
  hsts?: boolean;
};

export async function ilovaYarat({
  ombor, config, corsOriginlar,
  idempotentlikOmbori = new XotiraIdempotentlikOmbori(),
  log, xabarchi, tz, trustProxy, hsts,
}: Bogliqliklar): Promise<NestExpressApplication> {
  // bodyParser: false — Nest o'z JSON parser'ini qo'ymasin.
  // abortOnError: false — fabrika yiqilsa (config xato, baza yo'q)
  // Nest process.exit(1) QILMASIN, xato Promise orqali chiqsin.
  // bufferLogs — Nest loglari pino Logger ulangunicha yig'iladi
  const app = await NestFactory.create<NestExpressApplication>(
    AppModule.forRoot({
      ombor, config, log, xabarchi, tz, idempotentlikOmbori,
    }),
    { bodyParser: false, bufferLogs: true, abortOnError: false },
  );
  app.useLogger(app.get(Logger));
  const c = app.get<Config>(CONFIG);
  const l = app.get<Log>(LOG);
  app.set("trust proxy", trustProxy ?? c.TRUST_PROXY);
  app.disable("x-powered-by"); // "Express" ekanini oshkor qilmaymiz
  // app.use() — Express middleware'lari, Nest marshrutlaridan OLDIN
  app.use(sorovId());
  // so'rov logi (pino-http), keyin CLS konteksti: req.log "log"
  // kalitiga — xizmat, filtr, fon ishi joriyLog() bilan oladi
  app.use(sorovLogi(l));
  app.use(
    new ClsMiddleware({
      generateId: true,
      idGenerator: (req: express.Request) => req.sorovId,
      setup: (cls, req: express.Request) => {
        cls.set(CLS_LOG, req.log);
      },
    }).use,
  );
  app.use(helmet({ strictTransportSecurity: hsts ?? c.HSTS }));
  app.use(
    cors({
      origin: [...(corsOriginlar ?? c.CORS_ORIGINS)],
      allowedHeaders: [
        "Content-Type", "Idempotency-Key", "X-Request-Id",
        "If-Match", "If-None-Match",
      ],
      exposedHeaders: [
        "Location", "X-Request-Id", "Idempotency-Replayed", "ETag",
        "Link", "Deprecation", "Sunset",
      ],
      maxAge: 600,
    }),
  );
  app.useStaticAssets(join(import.meta.dirname, "..", "public"));
  app.use(express.json({ limit: "16kb", type: JSON_TURLARI }));
  app.use(tanaXatosi);
  // Versiya URI'da: /v1/... ; VERSION_NEUTRAL — versiyasiz eski yo'l
  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: ["1", VERSION_NEUTRAL],
  });
  await app.init();
  return app;
}

8.5 Test timeout'lari

O'tgan darsda jarayon.test.ts band mashinada bir marta yiqilganini ko'rdik. Vitest'ning sukut timeout'i — 5 s; 25–27 dagi node:test da esa timeout yo'q edi, testlar bunga moslanmagan. vitest.config.ts yakuniy:

ts
// vitest.config.ts — Nest 12 ESM loyihalarining sukut test vositasi.
// Vite 8 (oxc) dekorator metama'lumotini tsconfig'dan oladi
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    // 25–27 dagi nomlar (test/*.test.ts) + Nest odati (*.spec.ts)
    include: ["test/**/*.test.ts", "src/**/*.spec.ts"],
    // config process.env dan — testlar jim bo'lsin (o'z logini
    // bergan testlar — xotiraLogi — bundan mustasno)
    env: { LOG_LEVEL: "silent" },
    // Vitest sukuti 5 s (node:test — cheksiz): baza bilan ishlaydigan
    // testlar yuklangan mashinada 5–7 s oldi — 20 s
    testTimeout: 20_000,
    hookTimeout: 30_000,
  },
});

test/jarayon.test.ts da { timeout: 20_000 } → { timeout: 60_000 }. Kanon o'lchovi: bola jarayon yolg'iz ~3 s da ko'tariladi, parallel testlar bilan yuklangan mashinada 20 s dan oshdi (IlmHamroh, 2026-10). Bizda ham yolg'iz 3 s edi.

8.6 Natija: xizmat logi so'rov ID si bilan

npm run build, keyin SAQLASH=xotira node dist/main.js va X-Request-Id: aziz-0001 sarlavhali POST /v1/vazifalar. Biz yurgizdik (biz ishga tushirdik; time maydoni olib tashlandi):

text
{"daraja":"info","ilova":"vazifalar-api","context":"InstanceLoader","msg":"ClsModule dependencies initialized"}
{"daraja":"info","ilova":"vazifalar-api","sorovId":"aziz-0001","vazifaId":1,"msg":"vazifa yaratildi"}
{"daraja":"info","ilova":"vazifalar-api","sorovId":"aziz-0001","req":{"method":"POST","url":"/v1/vazifalar","ip":"127.0.0.1","userAgent":"node"},"res":{"status":201},"ms":20,"msg":"POST /v1/vazifalar 201"}
{"daraja":"warn","ilova":"vazifalar-api","sorovId":"aziz-0002","req":{"method":"POST","url":"/v1/vazifalar","ip":"127.0.0.1","userAgent":"node"},"res":{"status":400},"ms":4,"msg":"POST /v1/vazifalar 400"}

Ikkinchi qator — VazifalarService ichidagi joriyLog(this.log).info(...). Xizmat req ni bilmaydi, lekin qatorda sorovId: "aziz-0001" — CLS orqali keldi. Uchinchi qator — shu so'rovning pino-http qatori, ID bir xil. Ishga tushish logida yangi modullar ham ko'rindi: ClsModule, ClsCommonModule, ClsRootModule, ClsPluginsModule.

8.7 Tekshiruv va commit

bash
npm run check

Biz holat nusxasida yurgizdik (Postgres'siz): tip 0, lint 0, Vitest — 75 passed | 6 skipped, build o'tdi. Kanon bazali to'liq muhitda (kanon o'lchovi, IlmHamroh, 2026-10):

text
--- npm run tip
exit 0 (5698 ms)
--- npm run lint
exit 0 (10588 ms)
--- npm run test
 ↓ test/konteyner.test.ts (1 test | 1 skipped)
 Test Files  22 passed | 1 skipped (23)
      Tests  117 passed | 1 skipped (118)
exit 0 (8988 ms)
--- npm run build
exit 0 (6620 ms)

Testcontainers 10/10, jonli API 9 so'rov uch saqlash rejimida (Postgres, Prisma, Prisma + kesh) — javoblar o'zgarmadi. Prod bog'liqliklar: 15 → 18 (@nestjs/config, nestjs-pino, nestjs-cls).

Commit:

bash
git add -A
M="refactor: config — ConfigModule (registerAs + Zod configYukla),"
M+=" Nest loglari pino orqali (nestjs-pino),"
M+=" so'rov konteksti — nestjs-cls"
git commit -m "$M"

9. Real ishda

  • Correlation ID — prodakshnda xato qidirishning birinchi vositasi: foydalanuvchi "xato chiqdi" deydi, javobdagi sorovId bo'yicha shu so'rovning hamma log qatorlari topiladi.
  • nestjs-cls Nest loyihalarida joriy foydalanuvchi, tenantId, til va tranzaksiya uchun keng ishlatiladi. REQUEST scope esa "bilmasdan ishlatilgan va sekinlashtirgan" narsa sifatida review'da ko'p uchraydi.
  • Navbat ishchilari va cron — cls.run ni unutish klassik xato: fon ishining loglari ID'siz qoladi. 25-qismdan beri bor fon eksporti (/v1/eksportlar) sorovId ni shu sababli o'zi olib yuradi.
  • Intervyu: "Nest'da joriy foydalanuvchini xizmatda qanday olasiz?", "REQUEST scope nega sekin?", "AsyncLocalStorage nima?".

Xulosa

  • REQUEST scope har so'rovda yangi nusxa yaratadi va iste'molchilar tomon ko'piklanadi; CLS bilan xizmatlar singleton qoladi.
  • ClsModule.forRoot — global, middleware (mount, generateId, idGenerator, setup).
  • ClsService: set, get, getId, isActive, run; DI'siz — ClsServiceManager.getClsService().
  • So'rovdan tashqarida kontekst yo'q — cls.run bilan oching.
  • Kanon: mount: false + app.use(new ClsMiddleware(...).use) — pino-http dan keyin, qolgan hamma middleware'dan oldin.
  • Config, log va kontekst qadami tugadi: 25-qism xulqi o'zgarmadi, Nest standart vositalari ichiga o'tdi.

Keyingi dars: Prisma bilan NestJS — PrismaService naqshi, onModuleInit va Prisma xatolari filtri, hamda kanon nega omborni boshqacha ulaganini ko'ramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS so'rov konteksti: nestjs-cls, ClsService va AsyncLocalStorage — IlmHamroh