Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. ID qayerdan keladi
- 2.1 Kanon: X-Request-Id (holat 17)
- 2.2 Hujumchi nigohi: tashqi ID'ga ishonish
- 3. ID'ni har qatorga yetkazish
- 3.1 Muammo: req chuqurga bormaydi
- 3.2 Kichik server bilan ko'ramiz
- 3.3 Kanon: joriyLog va sorovLogi ning ikkinchi qismi
- 3.4 Javobdan keyin: fon ishi
- 3.5 Kanon tekshirgan tuzoq: so'rov tanasi
- 4. Servislararo uzatish
- 4.1 Bitta ID — bir nechta servis
- 4.2 W3C traceparent
- 5. API qadami: correlation ID va commit (holat 44, yakun)
- 6. Ko'p uchraydigan xatolar
- 6.1 Logger'ni ilova ishga tushganda olib qo'yish
- 6.2 ALS o'ramini zanjir oxiriga qo'yish
- 6.3 Tashqi ID'ni tekshirmasdan olish
- 6.4 Ichki so'rovda ID'ni unutish
- 6.5 ID'ni ruxsat uchun ishlatish
- 7. Mashqlar
- 1-mashq (oson): qaysi ID?
- 2-mashq (o'rta): ALS'siz va ALS bilan
- 3-mashq (qiyin): traceparent bilan ichki so'rov
- 4-mashq: API qadami
- 8. Real ishda
- Xulosa
- Manbalar
Correlation ID: X-Request-Id, AsyncLocalStorage va so'rov kuzatuvi
Qisqacha: Correlation ID — bitta so'rovga beriladigan noyob belgi. U so'rovning har log qatorida, javob sarlavhasida va ichki so'rovlarda bo'ladi.
vazifalar-apida uX-Request-Idsarlavhasidan olinadi (yoki yangi UUID),pino-httpdan keyinAsyncLocalStorage.run(req.log, next)bilan butun zanjirga "yopishtiriladi". Natija: xizmat qatlami, xato ishlovchi va hatto javobdan keyin ishlaydigan fon ishi hamreqni olmasdan o'shasorovIdbilan yozadi. Servislar orasida standart format — W3Ctraceparent.
Bu darsda
- So'rov ID'si qayerdan kelishi va unga qachon ishonish mumkinligini tushuntirasiz.
- AsyncLocalStorage bilan logger'ni chuqur funksiyalarga argumentsiz yetkazasiz.
- Ichki so'rovlarda ID'ni keyingi servisga uzatasiz.
traceparentsarlavhasini o'qiy olasiz va bola qiymatini yasaysiz.- Holat 44 ni yakunlab, loglar korrelyatsiyasini test bilan tekshirasiz.
Oldin bilishingiz kerak: AsyncLocalStorage, Pino chuqur, Express + TypeScript, Bulk va uzoq davom etuvchi operatsiyalar.
1. Nega bu kerak?
Malika qo'llab-quvvatlash xizmatiga yozadi: "Vazifa qo'shmoqchi edim, 'Kutilmagan xato' chiqdi. Pastida sorovId: 3e5b6c8b-… deb yozilgan." Bu ID — Problem Details javobidagi maydon. Endi siz loglardan shu ID bo'yicha qidirasiz va topasiz: so'rov qachon keldi, qaysi xizmat funksiyasi ishladi, qayerda xato chiqdi, qancha vaqt ketdi.
Buning uchun so'rovning har qatorida shu ID bo'lishi kerak. Faqat pino-http ning yakuniy qatorida emas — xizmat qatlamida ham, xato ishlovchida ham, javob ketgandan keyin ishlaydigan fon ishida ham.
Correlation ID (correlation ID) — "bog'lovchi belgi": turli joylarda yozilgan qatorlarni bitta so'rovga bog'laydi. Hayotiy o'xshatish: pochta posilkasining trek raqami. Posilka Toshkentdan Samarqandga bir nechta ombor orqali o'tadi. Har ombor o'z jurnaliga yozadi, lekin hammasi bitta trek raqami bilan. Raqamni kiritsangiz — butun yo'l ko'rinadi.
2. ID qayerdan keladi
2.1 Kanon: X-Request-Id (holat 17)
Express + TypeScript darsida sorovId() middleware'ini qo'shgan edik:
const YAROQLI = /^[\w.-]{8,128}$/;
export function sorovId(yangiId: () => string = randomUUID) {
return (req, res, next) => {
const kelgan = req.get("x-request-id");
req.sorovId =
kelgan !== undefined && YAROQLI.test(kelgan)
? kelgan
: yangiId();
res.set("X-Request-Id", req.sorovId);
next();
};
}(Tiplar qisqartirildi.) Qoida uch qadamda:
- Mijoz yoki proxy
X-Request-Idyuborgan bo'lsa va u yaroqli bo'lsa — o'shani olamiz. - Bo'lmasa — yangi UUID (crypto.randomUUID).
- Javobga qaytaramiz — mijoz uni ko'rsatishi yoki xato xabariga qo'shishi mumkin.
X-Request-Id — standart emas, lekin keng tarqalgan nom (nginx, Heroku, bulut balanserlari ishlatadi).
2.2 Hujumchi nigohi: tashqi ID'ga ishonish
Mijozdan kelgan ID — ishonchsiz ma'lumot. Hujumchi nima qila oladi?
| Hujum | Natija | Kanon himoyasi |
|---|---|---|
\n bilan soxta log qatori |
log injection | regex: faqat \w . - |
| 1 MB uzunlikdagi ID | log va xotira shishadi | 8–128 belgi |
| boshqa foydalanuvchining ID'si | tergovni chalkashtirish | ID — faqat kuzatish uchun, ruxsat uchun emas |
Uchinchi qator muhim: correlation ID hech qachon autentifikatsiya yoki ruxsat uchun ishlatilmaydi. U — faqat trek raqami. Ishlab chiqarishda ko'pincha ID'ni reverse proxy yasaydi va mijozdan kelganini almashtiradi. Bunda ichki servislar faqat proxy'ga ishonadi.
3. ID'ni har qatorga yetkazish
3.1 Muammo: req chuqurga bormaydi
pino-http har so'rovga req.log beradi — sorovId bog'langan bola logger (Pino chuqur). Marshrut ichida req.log.info(...) ishlaydi. Lekin xizmat qatlami (xizmat.ts) — HTTP'ni bilmaydi va req ni olmaydi (qatlamli tuzilma qarori). Uni qanday qilib req.log bilan yozdiramiz?
Ikki yo'l bor:
| Yo'l | Afzalligi | Kamchiligi |
|---|---|---|
har funksiyaga log argumenti |
aniq, testga qulay | o'nlab imzolar o'zgaradi |
| AsyncLocalStorage | imzolar toza | "yashirin" bog'liqlik |
Log — kesib o'tuvchi narsa (har joyda kerak, lekin biznes mantiqqa tegishli emas). AsyncLocalStorage darsidagi qoida aynan shunday holatlar uchun: ALS'ga faqat so'rov ID, joriy foydalanuvchi, til, trace kabilar qo'yiladi.
3.2 Kichik server bilan ko'ramiz
node:http, ALS va o'zimizning kichik log bilan. Ikki parallel so'rov, har biri o'z ID'si bilan. Xizmat funksiyasi vazifaYarat ID haqida hech narsa bilmaydi:
import { createServer } from "node:http";
import { AsyncLocalStorage } from "node:async_hooks";
import { setTimeout as kut } from "node:timers/promises";
const kontekst = new AsyncLocalStorage();
function log(xabar, maydonlar = {}) {
const sorovId = kontekst.getStore() ?? "-";
const qator = { sorovId, ...maydonlar, msg: xabar };
console.log(JSON.stringify(qator));
}
// xizmat qatlami: req'ni ham, ID'ni ham bilmaydi
async function vazifaYarat(matn) {
await kut(matn.length * 10); // baza o'rnida
log("vazifa yaratildi", { matn });
}
const server = createServer((req, res) => {
const id = req.headers["x-request-id"] ?? "yangi-id";
res.setHeader("X-Request-Id", id);
kontekst.run(id, async () => {
const matn = decodeURIComponent(req.url.slice(1));
await vazifaYarat(matn);
res.end("ok");
log("javob ketdi");
});
});
server.listen(0, "127.0.0.1");
await new Promise((ok) => server.once("listening", ok));
const asos = `http://127.0.0.1:${server.address().port}`;
const sora = (id, matn) =>
fetch(`${asos}/${encodeURIComponent(matn)}`, {
headers: { "X-Request-Id": id },
});
await Promise.all([
sora("aaa-0001", "Hisobot yozish"),
sora("bbb-0002", "Non"),
]);
log("hammasi tugadi");
server.closeAllConnections();
server.close();Konsolda:
{"sorovId":"bbb-0002","matn":"Non","msg":"vazifa yaratildi"}
{"sorovId":"bbb-0002","msg":"javob ketdi"}
{"sorovId":"aaa-0001","matn":"Hisobot yozish","msg":"vazifa yaratildi"}
{"sorovId":"aaa-0001","msg":"javob ketdi"}
{"sorovId":"-","msg":"hammasi tugadi"}Birinchi so'rov (aaa) birinchi yuborildi, lekin sekinroq bajarildi — bbb uni quvib o'tdi. Shunga qaramay har qatorda to'g'ri ID. vazifaYarat ning imzosida ID yo'q. So'rovdan tashqarida esa — -.
sequenceDiagram
participant M as Mijoz
participant S as sorovId()
participant H as pino-http + ALS
participant X as xizmat.ts
M->>S: POST, X-Request-Id: kor-0001
S->>H: req.sorovId = kor-0001
H->>X: als.run(req.log, next)
X-->>X: joriyLog().info("vazifa yaratildi")
H-->>M: 201, X-Request-Id: kor-0001Diagrammaga qarang: ID bir marta qo'yiladi (sorovId()), bir marta ALS'ga joylanadi, keyin xizmat uni "havodan" oladi.
3.3 Kanon: joriyLog va sorovLogi ning ikkinchi qismi
Kanon ALS'ga ID satrini emas, butun bola logger'ni (req.log) qo'yadi. Shunda pastdagi kod oddiy Pino logger bilan ishlaydi (src/umumiy/log.ts, holat 44):
const kontekst = new AsyncLocalStorage<Log>();
/** Joriy so'rovning logger'i; so'rovdan tashqarida — asosiy */
export function joriyLog(asosiy: Log): Log {
return kontekst.getStore() ?? asosiy;
}
export function sorovLogi(log: Log): RequestHandler[] {
const http = pinoHttp({ /* … Pino chuqur darsida */ });
return [
http,
(req, _res, next) => {
kontekst.run(req.log, next);
},
];
}sorovLogi endi ikkita middleware qaytaradi: pino-http va ALS o'rami. kontekst.run(req.log, next) — next() va undan keyingi hamma middleware, marshrut va ular boshlagan async ish shu kontekstda ishlaydi.
Bitta POST /v1/vazifalar (X-Request-Id: kor-0001) kanon zanjiridan qanday o'tadi:
| Qadam | Middleware | Natija |
|---|---|---|
| 1 | sorovId() |
req.sorovId = "kor-0001"; javobga X-Request-Id sarlavhasi |
| 2 | pino-http |
req.log — sorovId bog'langan bola logger; vaqt o'lchash boshlandi |
| 3 | ALS o'rami | kontekst.run(req.log, next) — bundan keyingi hamma narsa shu kontekstda |
| 4 | helmet, cors, static |
xavfsizlik va CORS sarlavhalari; logga tegmaydi |
| 5 | express.json() |
tana o'qiladi — kontekst saqlanib qoladi |
| 6 | idempotentlik, router |
marshrut xizmatni chaqiradi (req uzatilmaydi) |
| 7 | xizmat.yarat |
joriyLog() → req.log: "vazifa yaratildi" qatori sorovId bilan |
| 8 | javob 201 → finish |
pino-http yakuniy qatori: "POST /v1/vazifalar 201", ms bilan |
7-qadamdagi qator 8-qadamdagidan oldin yoziladi: pino-http o'z qatorini javob tugagandagina yozadi.
Xizmatga logger to'g'ridan emas, funksiya sifatida beriladi (ilova.ts):
const xizmat = vazifaXizmati(ombor, () => joriyLog(log));Nega funksiya? vazifaXizmati ilova ishga tushganda bir marta yasaladi — o'sha paytda hech qanday so'rov yo'q. Funksiya esa har chaqiruvda joriy so'rovning logger'ini oladi. Xizmat ichida (xizmat.ts):
const v = await ombor.yarat(yangi);
log().info({ vazifaId: v.id }, "vazifa yaratildi");Testda esa log sifatida soxta funksiya berish oson — ALS'siz (Mock, stub, spy va fake).
3.4 Javobdan keyin: fon ishi
Bulk va uzoq operatsiyalar darsidagi eksport 202 qaytaradi va ishni fonda davom ettiradi. Javob ketdi — so'rov "tugadi". Fon ishi qaysi ID bilan yozadi?
// eksport.ts (holat 44): logger javobdan OLDIN olinadi
const l = log();
bajar(ish).then(
() => {
l.info({ eksportId: ish.id, soni: ish.tayyor }, "eksport tayyor");
},
(x: unknown) => {
ish.holat = "xato";
ish.xato = "Eksport bajarilmadi";
l.error({ err: x, eksportId: ish.id }, "eksport bajarilmadi");
},
);Kanon jonli chiqishi (tekshiruv-44.txt) — 202 javobi va 1 ms keyingi fon qatori, ikkalasida bir xil sorovId:
{"daraja":"info","time":"2026-10-24T06:04:49.109Z","ilova":"vazifalar-api","sorovId":"kanon-44-0003","req":{"method":"POST","url":"/v1/eksportlar","ip":"127.0.0.1","userAgent":"curl/8.19.0"},"res":{"status":202},"ms":1,"msg":"POST /v1/eksportlar 202"}
{"daraja":"info","time":"2026-10-24T06:04:49.110Z","ilova":"vazifalar-api","sorovId":"kanon-44-0003","eksportId":"deb4d671-f6e4-484d-87d4-a8cfa1d92697","soni":1,"msg":"eksport tayyor"}Aslida bajar() ALS kontekstida boshlangani uchun ichidagi log() ham to'g'ri ID'ni olardi. Lekin kanon logger'ni oldindan l ga oldi — o'qiganga aniq ko'rinsin.
3.5 Kanon tekshirgan tuzoq: so'rov tanasi
ALS haqida eski maqolalarda ogohlantirish bor: "body-parser oqim hodisalari orqali o'qiydi, kontekst yo'qolishi mumkin". AsyncLocalStorage darsida ko'rgansiz: EventEmitter tinglovchisi emit qilganning kontekstida ishlaydi. Kanon buni tekshirdi: express.json() tanani o'qigandan keyin ham xizmat qatori to'g'ri sorovId bilan chiqdi — test bor (log.test.ts, "POST tanasi bilan ham"). Sabab tartibda: ALS o'rami express.json() dan oldin turadi, oqim o'qish shu kontekst ichida boshlanadi.
Tekshirib ko'ring: Kimdir
app.use(sorovLogi(log))niexpress.json()dan keyin ko'chirdi. Endi nima o'zgaradi?
Javob
Ikki narsa. Birinchisi — JSON xatosi (400) va 413 kabi javoblar express.json() da, ya'ni pino-http ga yetmasdan chiqadi: ular uchun so'rov qatori yozilmaydi (yoki ID'siz yoziladi). Ikkinchisi — tanani o'qish ALS'dan tashqarida boshlanadi. Qoida: ID va log middleware'i — zanjirning eng boshida, sorovId() dan keyin darhol. Kanondagi tartib: sorovId → sorovLogi → helmet → cors → static → json.
4. Servislararo uzatish
4.1 Bitta ID — bir nechta servis
vazifalar-api testlar qismida webhook yuboradi — boshqa servisga HTTP so'rov. Kelajakda (29-qism) Telegram bot servisi bo'ladi. Malikaning so'rovi ikki servisdan o'tsa, ikkalasining logida bir xil ID bo'lishi kerak. Buning uchun ichki so'rovga sarlavhani qo'shamiz:
import { createServer } from "node:http";
import { once } from "node:events";
import { AsyncLocalStorage } from "node:async_hooks";
const kontekst = new AsyncLocalStorage();
const log = (servis, xabar) => {
const id = kontekst.getStore()?.id ?? "-";
console.log(`[${servis}] [${id}] ${xabar}`);
};
// Ichki so'rov: joriy so'rov ID'sini sarlavhaga qo'shadi
function ichkiFetch(url) {
const id = kontekst.getStore()?.id;
const headers = id ? { "X-Request-Id": id } : {};
return fetch(url, { headers });
}
function servis(nom, ishlov) {
return createServer((req, res) => {
const id = req.headers["x-request-id"] ?? `${nom}-yangi`;
kontekst.run({ id }, () => ishlov(req, res));
}).listen(0, "127.0.0.1");
}
const manzil = (s) => `http://127.0.0.1:${s.address().port}`;
const xabar = servis("xabar", (req, res) => {
log("xabar", "bildirishnoma yuborildi");
res.end();
});
const api = servis("api", async (req, res) => {
log("api", "vazifa bajarildi");
await ichkiFetch(manzil(xabar));
log("api", "javob ketdi");
res.end();
});
await Promise.all([once(api, "listening"), once(xabar, "listening")]);
const sarlavha = { "X-Request-Id": "mijoz-0007" };
await fetch(manzil(api), { headers: sarlavha });
for (const s of [api, xabar]) {
s.closeAllConnections();
s.close();
}Konsolda:
[api] [mijoz-0007] vazifa bajarildi
[xabar] [mijoz-0007] bildirishnoma yuborildi
[api] [mijoz-0007] javob ketdiIkkala servisda bir ID. ichkiFetch o'rniga oddiy fetch ishlatsangiz — xabar servisi xabar-yangi deb yozardi va zanjir uzilardi. O'zingiz almashtirib ko'ring.
Kanonda webhookXabarchi ID'ni sarlavhaga qo'shmaydi — webhook tashqi, begona servisga ketadi va bizning ichki ID'miz unga kerak emas. Lekin javob logida sorovId bor (tekshiruv-51.txt, «webhook yuborildi» qatori). Ichki servislar bo'lganda esa uzatish shart.
4.2 W3C traceparent
Bitta X-Request-Id oddiy holatlar uchun yetadi. Katta tizimlarda ko'proq kerak: so'rov qaysi servisdan qaysi biriga o'tgani, har qadam qancha vaqt olgani — daraxt ko'rinishida. Bunga taqsimlangan kuzatuv (distributed tracing) deyiladi. Uning standart sarlavhasi — W3C Trace Context tavsiyasidagi traceparent:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01| Qism | Uzunlik | Ma'nosi |
|---|---|---|
00 |
2 | versiya |
4bf9…4736 |
32 hex | trace-id — butun so'rov zanjiri, hamma servisda bir xil |
00f0…02b7 |
16 hex | parent-id (span) — shu sarlavhani yuborgan qadam |
01 |
2 | bayroqlar: 01 — bu trace yozib olinadi (sampled) |
Har servis trace-id'ni saqlaydi, parent-id o'rniga esa o'z qadamining yangi ID'sini qo'yib keyingisiga yuboradi. Natijada qadamlar daraxti tiklanadi. Brauzerda sinab ko'ring:
const NAQSH = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
function traceparentOch(qiymat) {
const m = NAQSH.exec(qiymat);
if (!m || /^0+$/.test(m[1]) || /^0+$/.test(m[2])) return null;
const namuna = (parseInt(m[3], 16) & 1) === 1;
return { traceId: m[1], spanId: m[2], namuna };
}
function keyingiTraceparent(ota, yangiSpanId) {
const bayroq = ota.namuna ? "01" : "00";
return `00-${ota.traceId}-${yangiSpanId}-${bayroq}`;
}
const kelgan =
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
const ota = traceparentOch(kelgan);
console.log(ota.traceId);
console.log(ota.spanId, ota.namuna);
console.log(keyingiTraceparent(ota, "b7ad6b7169203331"));
console.log(traceparentOch("salom"));
const nollar = "00-" + "0".repeat(32) + "-00f067aa0ba902b7-01";
console.log(traceparentOch(nollar));Konsolda:
4bf92f3577b34da6a3ce929d0e0e4736
00f067aa0ba902b7 true
00-4bf92f3577b34da6a3ce929d0e0e4736-b7ad6b7169203331-01
null
nullStandart bo'yicha hammasi nol bo'lgan trace-id yaroqsiz — shuning uchun oxirgi qator null. Yaroqsiz sarlavha kelsa, servis uni tashlab, yangi trace boshlaydi.
Qo'lda yozish shart emas: OpenTelemetry kutubxonasi traceparent ni o'zi o'qiydi, yaratadi, fetch ga qo'shadi va ichida xuddi shu ALS'dan foydalanadi. Kanon uni qo'shmadi — servis bitta. OpenTelemetry — 34-qismda.
5. API qadami: correlation ID va commit (holat 44, yakun)
Holat 44 ning oxirgi qismi:
| Fayl | O'zgarish |
|---|---|
src/umumiy/log.ts |
AsyncLocalStorage<Log>, joriyLog(), sorovLogi → 2 middleware |
src/ilova.ts |
vazifaXizmati(ombor, () => joriyLog(log)), eksport va xato ishlovchisi ham joriyLog; xatoLogi olib tashlanadi |
src/vazifalar/xizmat.ts |
"vazifa yaratildi", "vazifa o'chirildi" qatorlari |
src/eksport/eksport.ts |
"eksport tayyor" / "eksport bajarilmadi" |
src/umumiy/idempotentlik.ts |
console.error o'rniga req.log.error |
test/xato.test.ts |
500 testi console.error ni emas, xotiradagi log qatorini tekshiradi |
test/log.test.ts |
4 test (yangi) |
Yakuniy src/umumiy/log.ts — to'liq (holat 44; uzun qatorlar va izohlar telefon uchun bo'lindi, kod aynan):
// log.ts — strukturali (JSON) log: Pino. stdout'ga yoziladi
// (12-factor) — faylga yozish, rotatsiya, yig'ish — muhitning
// ishi (Docker, Loki — 34-qism).
// Correlation ID: har so'rov AsyncLocalStorage ichida — xizmat
// va fon ishlari ham joriy so'rov logger'ini oladi
// (req'ni uzatmasdan)
import { AsyncLocalStorage } from "node:async_hooks";
import { pino } from "pino";
import type { DestinationStream, Logger, LoggerOptions } from "pino";
import { pinoHttp } from "pino-http";
import type { RequestHandler } from "express";
export type Log = Logger;
/** Hech qachon logga tushmasligi kerak bo'lgan joylar —
* qiymat "[yashirildi]" ga almashadi */
export const YASHIRIN = [
"req.headers.authorization",
"req.headers.cookie",
'req.headers["x-api-key"]',
"res.headers['set-cookie']",
"*.parol",
"*.token",
];
export function logYarat(
daraja: string,
manzil?: DestinationStream,
): Log {
const sozlama: LoggerOptions = {
level: daraja,
// vaqt ISO ko'rinishda (epoch ms emas) — odam ham o'qisin;
// Toshkentga aylantirish — ko'rsatuvchi vositada
timestamp: pino.stdTimeFunctions.isoTime,
redact: { paths: YASHIRIN, censor: "[yashirildi]" },
base: { ilova: "vazifalar-api" },
formatters: { level: (label) => ({ daraja: label }) },
};
return manzil === undefined ? pino(sozlama) : pino(sozlama, manzil);
}
const kontekst = new AsyncLocalStorage<Log>();
/** Joriy so'rovning logger'i (sorovId bilan);
* so'rovdan tashqarida — asosiy logger */
export function joriyLog(asosiy: Log): Log {
return kontekst.getStore() ?? asosiy;
}
/**
* pino-http: har so'rov oxirida bitta qator
* (metod, yo'l, status, vaqt),
* sorovId — bizning X-Request-Id; keyin qolgan zanjir
* AsyncLocalStorage
* ichida ishlaydi. sorovId() middleware'idan KEYIN qo'yiladi
*/
function asliYol(req: { url?: string | undefined }): string {
const r = req as { originalUrl?: string; url?: string };
return (r.originalUrl ?? r.url ?? "").split("?")[0] ?? "";
}
export function sorovLogi(log: Log): RequestHandler[] {
const http = pinoHttp({
logger: log,
genReqId: (req) =>
(req as unknown as { sorovId: string }).sorovId,
customAttributeKeys: { reqId: "sorovId", responseTime: "ms" },
// req.log (va AsyncLocalStorage'dagi) child logger'ga
// faqat sorovId
// bog'lanadi — har qatorga butun req yozilmasin
quietReqLogger: true,
// 4xx — warn, 5xx — error (xato ishlovchi o'zi ham yozadi),
// qolgani info
customLogLevel: (_req, res, xato) =>
xato !== undefined || res.statusCode >= 500
? "error"
: res.statusCode >= 400
? "warn"
: "info",
// req.url — router ichida qisqaradi ("/"),
// originalUrl — to'liq yo'l
customSuccessMessage: (req, res) =>
`${String(req.method)} ${asliYol(req)} ` +
String(res.statusCode),
customErrorMessage: (req, res) =>
`${String(req.method)} ${asliYol(req)} ` +
String(res.statusCode),
serializers: {
// to'liq sarlavhalar o'rniga keraklilari (hajm va maxfiylik)
req: (req: {
method: string;
url: string;
raw?: { originalUrl?: string };
headers: Record<string, string | undefined>;
remoteAddress?: string;
}) => ({
method: req.method,
url: req.raw?.originalUrl ?? req.url,
ip: req.remoteAddress,
userAgent: req.headers["user-agent"],
}),
res: (res: { statusCode: number }) => ({
status: res.statusCode,
}),
},
});
return [
http,
(req, _res, next) => {
kontekst.run(req.log, next);
},
];
}Qolgan fayllar — Pino chuqur darsi oxiridagi kesimdan farq ko'rinishida (uzun qatorlar bo'lindi, kod aynan). src/ilova.ts — xatoLogi o'rniga hamma joyda joriyLog(log):
--- a/ilova.ts
+++ b/ilova.ts
-import type { XatoLogi } from "./umumiy/xato-ishlovchi.ts";
-import { logYarat, sorovLogi } from "./umumiy/log.ts";
+import { joriyLog, logYarat, sorovLogi } from "./umumiy/log.ts";
@@ Bogliqliklar @@
- /** kutilmagan xatolarni yozish (sukut — console.error) */
- xatoLogi?: XatoLogi;
@@ ilovaYarat parametrlari @@
- xatoLogi,
log = logYarat("silent"),
@@ middleware tartibi @@
app.use(sorovId());
+ // so'rov logi + AsyncLocalStorage konteksti (sorovId) —
+ // qolgan hamma middleware va handler shu kontekst ichida
app.use(sorovLogi(log));
@@ routerlar @@
- const xizmat = vazifaXizmati(ombor);
+ const xizmat = vazifaXizmati(ombor, () => joriyLog(log));
- v1.use("/eksportlar", eksportRouteri(ombor));
+ v1.use(
+ "/eksportlar",
+ eksportRouteri(ombor, { log: () => joriyLog(log) }),
+ );
@@ oxiri @@
- app.use(xatoIshlovchi(xatoLogi));
+ app.use(
+ xatoIshlovchi((_id, xato) => {
+ joriyLog(log).error({ err: xato }, "kutilmagan xato");
+ }),
+ );src/vazifalar/xizmat.ts — logger funksiya sifatida, sukut — jim:
--- a/xizmat.ts
+++ b/xizmat.ts
+import type { Log } from "../umumiy/log.ts";
@@ vazifaXizmati @@
-export function vazifaXizmati(ombor: VazifaOmbori) {
+// log — funksiya: har chaqiruvda JORIY so'rov logger'i
+// (AsyncLocalStorage) — sorovId avtomatik qo'shiladi
+export function vazifaXizmati(
+ ombor: VazifaOmbori,
+ log: () => Pick<Log, "info"> = () => ({ info: () => undefined }),
+) {
@@ yarat @@
- return ombor.yarat(yangi);
+ const v = await ombor.yarat(yangi);
+ log().info({ vazifaId: v.id }, "vazifa yaratildi");
+ return v;
@@ ochir @@
natija(id, await ombor.ochir(id, shart));
+ log().info({ vazifaId: id }, "vazifa o'chirildi");src/eksport/eksport.ts — log parametri; fon ishining yangi ko'rinishi «Javobdan keyin: fon ishi» bo'limida:
--- a/eksport.ts
+++ b/eksport.ts
+import type { Log } from "../umumiy/log.ts";
@@ BOLAK dan keyin @@
+type EksportLogi = Pick<Log, "info" | "error">;
+const jimLog: EksportLogi = {
+ info: () => undefined,
+ error: () => undefined,
+};
@@ eksportRouteri parametrlari @@
bolakdanKeyin = (): Promise<void> => navbat(),
+ log = (): EksportLogi => jimLog,
@@ POST / @@
- bajar(ish).catch((x: unknown) => {
- ish.holat = "xato";
- ish.xato = "Eksport bajarilmadi";
- console.error(`[eksport ${ish.id}]`, x);
- });
+ const l = log();
+ bajar(ish).then( /* «Javobdan keyin: fon ishi» */ );src/umumiy/idempotentlik.ts — req.log Pino chuqur darsidan beri bor:
saqla.catch((x: unknown) => {
- console.error(
- `[${req.sorovId}] idempotentlik omboriga yozilmadi`, x);
+ req.log.error(
+ { err: x }, "idempotentlik omboriga yozilmadi");
});test/xato.test.ts — console.error ni almashtirish o'rniga xotiradagi log:
-import { jsonYubor } from "./yordamchi.ts";
+import { jsonYubor, xotiraLogi } from "./yordamchi.ts";
@@ before/after @@
-const loglar: unknown[][] = [];
-const aslError = console.error;
+const { log, qatorlar } = xotiraLogi();
before(async () => {
- console.error = (...x: unknown[]) => loglar.push(x);
- const { server, manzil } = await ishga({ port: 0,
- bogliqliklar: { ombor: new BuzuqOmbor() } });
+ const { server, manzil } = await ishga({ port: 0,
+ bogliqliklar: { ombor: new BuzuqOmbor(), log } });
asos = manzil;
toxta = () => yop(server, 1000);
});
-after(async () => {
- console.error = aslError;
- await toxta();
-});
+after(() => toxta());
@@ 500 testining oxiri @@
- assert.ok(loglar.some((q) => q[0] === "[test-500-abcdef]"
- && q[1] instanceof Error));
+ // logda — to'liq xato (stack bilan) va o'sha sorovId
+ const q = qatorlar.find((x) => x.msg === "kutilmagan xato");
+ assert.ok(q);
+ assert.equal(q.sorovId, "test-500-abcdef");
+ assert.equal(q.daraja, "error");
+ assert.match(String((q.err as { stack?: string } | undefined)
+ ?.stack), /ulanish uzildi/);Nega bu test aynan bugun o'zgaradi? U log qatorida sorovId ni talab qiladi. Xato ishlovchi joriyLog(log) orqali req.log ni olgandagina shunday bo'ladi — ya'ni ALS o'rami bilan birga.
Testlar — xotiraLogi() bilan (Pino chuqur darsidagi yordamchi). test/log.test.ts — to'liq (holat 44; qatorlar bo'lindi, kod aynan):
import { after, before, test } from "node:test";
import assert from "node:assert/strict";
import { ishgaTushir as ishga } from "../src/ishga-tushir.ts";
import { yop } from "../src/yopish.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
import { xotiraLogi } from "./yordamchi.ts";
const { log, qatorlar } = xotiraLogi();
let asos = "";
let toxta: () => Promise<boolean>;
before(async () => {
const { server, manzil } = await ishga({
port: 0,
bogliqliklar: { ombor: new XotiraOmbori(), log },
});
asos = manzil;
toxta = () => yop(server, 1000);
});
after(() => toxta());
test("correlation ID: xizmat ichidagi log (req'siz) " +
"so'rov sorovId'sini oladi — POST tanasi bilan ham " +
"(AsyncLocalStorage)", async () => {
await fetch(`${asos}/v1/vazifalar`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Request-Id": "kor-0001-abc",
},
body: '{"matn":"Non"}',
});
const xizmat = qatorlar.find((q) => q.msg === "vazifa yaratildi");
assert.ok(xizmat);
assert.equal(xizmat.sorovId, "kor-0001-abc");
assert.equal(xizmat.vazifaId, 1);
const sorov = qatorlar.find(
(q) => q.msg === "POST /v1/vazifalar 201",
);
assert.ok(sorov);
assert.equal(sorov.sorovId, "kor-0001-abc");
assert.equal(sorov.daraja, "info");
assert.equal(typeof sorov.ms, "number");
assert.match(String(sorov.time), /^\d{4}-\d{2}-\d{2}T.*Z$/);
});
test("parallel so'rovlar loglari aralashmaydi", async () => {
await Promise.all(
["par-aaaa-1", "par-bbbb-2", "par-cccc-3"].map((id, i) =>
fetch(`${asos}/v1/vazifalar`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Request-Id": id,
},
body: JSON.stringify({ matn: `p${String(i)}` }),
})),
);
const yaratilgan = qatorlar.filter(
(q) => q.msg === "vazifa yaratildi" &&
String(q.sorovId).startsWith("par-"),
);
assert.equal(yaratilgan.length, 3);
assert.equal(new Set(yaratilgan.map((q) => q.sorovId)).size, 3);
});
test("4xx — warn; Authorization va cookie logga tushmaydi (redact)",
async () => {
await fetch(`${asos}/v1/vazifalar/999`, {
headers: {
Authorization: "Bearer SOXTA-TOKEN",
"X-Request-Id": "red-0001-abc",
},
});
const q = qatorlar.find(
(x) => x.sorovId === "red-0001-abc" &&
x.msg === "GET /v1/vazifalar/999 404",
);
assert.ok(q);
assert.equal(q.daraja, "warn");
assert.doesNotMatch(JSON.stringify(qatorlar), /SOXTA-TOKEN/);
});
test("fon ishi (eksport) logi ham boshlagan so'rov sorovId'si bilan",
async () => {
const r = await fetch(`${asos}/v1/eksportlar`, {
method: "POST",
headers: { "X-Request-Id": "eks-0001-abc" },
});
const joy = String(r.headers.get("location"));
for (let i = 0; i < 50 &&
!qatorlar.some((q) => q.msg === "eksport tayyor"); i++) {
await new Promise((ok) => setTimeout(ok, 10));
}
const q = qatorlar.find((x) => x.msg === "eksport tayyor");
assert.ok(q);
assert.equal(q.sorovId, "eks-0001-abc");
assert.equal(q.eksportId, joy.split("/").at(-1));
});Uchinchi va to'rtinchi testlar assert.doesNotMatch bilan "token yo'q" ni va fon ishini kutish siklini (50 × 10 ms — fon ishi tugaguncha) ko'rsatadi.
Tekshiruv (tekshiruv-44.txt): tip 0, lint 0, test 58/58; prod bog'liqliklar 6 (84 paket); sovuq start 440 ms. Commit (kanon xabari):
git add -A
git commit -m "Log: Pino JSON (stdout), pino-http, redact, X-Request-Id → AsyncLocalStorage orqali hamma qatorlarda; pino-pretty faqat dev"6. Ko'p uchraydigan xatolar
6.1 Logger'ni ilova ishga tushganda olib qo'yish
vazifaXizmati(ombor, joriyLog(log)) — qavslar bilan, bir marta chaqirildi: so'rov yo'q paytda olingan asosiy logger abadiy qoladi, ID hech qachon chiqmaydi. Funksiya uzating: () => joriyLog(log).
6.2 ALS o'ramini zanjir oxiriga qo'yish
«Tekshirib ko'ring» savolidagi holat. sorovId() → sorovLogi() — eng boshida.
6.3 Tashqi ID'ni tekshirmasdan olish
Log injection va shishgan loglar. Regex va uzunlik chegarasi; proxy orqasida — proxy yasagan ID.
6.4 Ichki so'rovda ID'ni unutish
Ikkinchi servis yangi ID yasaydi — zanjir uziladi. Bitta ichkiFetch yordamchisi (yoki OpenTelemetry), oddiy fetch emas.
6.5 ID'ni ruxsat uchun ishlatish
"Bu so'rov ID'si bizniki, demak ichki so'rov" — xato: ID'ni istalgan kishi yuboradi. ID — faqat kuzatish uchun.
7. Mashqlar
1-mashq (oson): qaysi ID?
sorovId() middleware'iga uchta so'rov keldi: (a) X-Request-Id yo'q; (b) X-Request-Id: kor-0001-abc; (c) X-Request-Id: a b. Javobdagi X-Request-Id qanday bo'ladi?
Yechim
(a) yangi UUID. (b) kor-0001-abc — yaroqli (12 belgi, faqat harf, raqam, tire). (c) yangi UUID — bo'shliq [\w.-] ga kirmaydi, uzunligi ham 3 (8 dan kam). Kanon testi (middleware.test.ts) aynan shu uchta holatni tekshiradi.
2-mashq (o'rta): ALS'siz va ALS bilan
«Kichik server bilan ko'ramiz» bo'limidagi kodda kontekst.run(id, async () => {…}) o'rniga oddiy global let joriyId = id yozing va log ichida joriyId ni o'qing. Natija qanday o'zgaradi va nega?
Yechim
Biz ishga tushirdik (ikki marta, natija bir xil):
{"sorovId":"bbb-0002","matn":"Non","msg":"vazifa yaratildi"}
{"sorovId":"bbb-0002","msg":"javob ketdi"}
{"sorovId":"bbb-0002","matn":"Hisobot yozish","msg":"vazifa yaratildi"}
{"sorovId":"bbb-0002","msg":"javob ketdi"}
{"sorovId":"bbb-0002","msg":"hammasi tugadi"}Hamma qator — bbb-0002, hatto "Hisobot yozish" (u aaa niki) va so'rovdan tashqaridagi oxirgi qator. aaa keldi — joriyId = "aaa-0001", await kut(…) da to'xtadi. bbb keldi — joriyId = "bbb-0002". Keyin aaa uyg'onganda global'da bbb-0002 turibdi va u hech qachon tiklanmaydi. Bu — AsyncLocalStorage darsidagi "global o'zgaruvchi" xatosining server ko'rinishi. Qaysi ID "yutishi" so'rovlar kelish tartibiga bog'liq.
3-mashq (qiyin): traceparent bilan ichki so'rov
ichkiFetch ni kengaytiring: kontekstda { id, traceparent } bo'lsin. Kirishda traceparent sarlavhasi yaroqli bo'lsa — uni oling, bo'lmasa yangisini yasang (trace-id — 32, span — 16 tasodifiy hex; crypto.getRandomValues). Ichki so'rovda trace-id saqlansin, span yangi bo'lsin. Natijani tekshirish uchun tasodifni parametr qiling: yangiHex(n) funksiyasini tashqaridan bering.
Yechim
Asosiy qism — toza funksiya, tasodif parametr orqali (bu g'oya Vaqt va tasodifni boshqarish darsida chuqurlashadi):
const NAQSH = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
function kiruvchiTrace(sarlavha, yangiHex) {
const m = NAQSH.exec(sarlavha ?? "");
if (m && !/^0+$/.test(m[1])) return { traceId: m[1], bayroq: m[3] };
return { traceId: yangiHex(32), bayroq: "01" };
}
function chiquvchiTraceparent(trace, yangiHex) {
return `00-${trace.traceId}-${yangiHex(16)}-${trace.bayroq}`;
}
let n = 0;
const soxtaHex = (uzunlik) => String(++n).padStart(uzunlik, "a");
const t1 = kiruvchiTrace(
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
soxtaHex,
);
console.log(chiquvchiTraceparent(t1, soxtaHex));
const t2 = kiruvchiTrace(undefined, soxtaHex);
console.log(chiquvchiTraceparent(t2, soxtaHex));Konsolda:
00-4bf92f3577b34da6a3ce929d0e0e4736-aaaaaaaaaaaaaaa1-01
00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2-aaaaaaaaaaaaaaa3-01Haqiqiy kodda yangiHex — (n) => [...crypto.getRandomValues(new Uint8Array(n / 2))].map((b) => b.toString(16).padStart(2, "0")).join(""). Serverga ulash: kontekst.run({ id, trace: kiruvchiTrace(req.headers.traceparent, yangiHex) }, …) va ichkiFetch ichida traceparent: chiquvchiTraceparent(store.trace, yangiHex).
4-mashq: API qadami
«API qadami» bo'limidagi o'zgarishlarni qiling, test/log.test.ts ni yozing va npm run check ni o'tkazing (58 test). Keyin LOG_LEVEL=info npm start bilan curl -H "X-Request-Id: men-0001-test" -X POST …/v1/vazifalar yuboring va ikkala qatorda (xizmat va so'rov) men-0001-test borligini ko'ring. Kanon xabari bilan commit qiling.
Yechim
Kutiladigan chiqish — «Javobdan keyin: fon ishi» va «Logging asoslari» darsidagi kanon qatorlariga o'xshash: avval "msg":"vazifa yaratildi" (xizmat), keyin "msg":"POST /v1/vazifalar 201" (pino-http), ikkalasida "sorovId":"men-0001-test". Xizmat qatori oldin chiqadi — pino-http qatori javob tugaganda yoziladi. Windows cmd da qo'shtirnoqlar boshqacha ishlaydi; Git Bash yoki PowerShell qulayroq.
8. Real ishda
- Qo'llab-quvvatlash: xato sahifasida yoki Problem Details'da
sorovIdko'rsatish — foydalanuvchi bilan dasturchini bog'laydigan eng arzon usul. - nginx va balanserlar: nginx'da
$request_ido'zgaruvchisi bor — proxy ID yasab,proxy_set_header X-Request-Id $request_idbilan backend'ga beradi (33-qism). - Kuzatuv tizimlari: Jaeger, Grafana Tempo, Datadog APM, Sentry —
traceparentbilan ishlaydi; log qatorigatraceIdqo'shilsa, logdan trace'ga bir bosishda o'tiladi (34-qism). - Intervyu savollari: "Correlation ID nima?", "Uni Node'da chuqur funksiyaga qanday yetkazasiz?", "Mikroservislar orasida-chi?", "
traceparentqanday tuzilgan?".
Xulosa
- Correlation ID — so'rovning trek raqami: har log qatorida, javob sarlavhasida, ichki so'rovlarda.
- Kanon:
X-Request-Id(yaroqli bo'lsa) yoki yangi UUID; tashqi ID — ishonchsiz, faqat kuzatish uchun. kontekst.run(req.log, next)— keyingi hamma kodjoriyLog()bilan so'rov logger'ini oladi; xizmatga logger funksiya sifatida beriladi.- Fon ishi javobdan keyin ham o'sha ID bilan yozadi; tana o'qilgandan keyin ham kontekst saqlanadi (kanon testi).
- Servislar orasida ID sarlavhada uzatiladi; standart — W3C
traceparent(trace-id saqlanadi, span yangilanadi); OpenTelemetry buni avtomatlashtiradi. - Holat 44: 58/58 test, commit.
Keyingi dars: Backend test strategiyasi — unit, integration, e2e va contract testlar; nega backend'da integration test ustun va vazifalar-api ning 67 testi qaysi darajalarga bo'linadi.
Manbalar
- W3C: Trace Context — Recommendation, 2021-11-23 (
traceparentformati va yaroqsiz qiymatlar). - Node.js hujjati: AsyncLocalStorage (v24).
- OpenTelemetry: Context propagation.
- Kanon:
vazifalar-apiholat 44 (src/umumiy/log.ts,src/umumiy/sorov-id.ts,test/log.test.ts),natijalar/tekshiruv-44.txt.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!