IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API42/51-dars20 daqiqa
Mundarija (38)

Pino logger chuqur: pino-http, redact, serializer va transport

Qisqacha: Pino — Node uchun tez, JSON yozadigan logger. U stdout'ga bitta qatorli JSON yozadi. log.child({ sorovId }) maydonlarni bog'laydi. redact ko'rsatilgan yo'llardagi qiymatni (req.headers.authorization) [Redacted] ga almashtiradi. Serializer'lar katta obyektdan (so'rov, xato) keraklisini oladi. pino-http har so'rov oxirida bitta qator yozadi — lekin sukut sozlamasi bilan u Authorization sarlavhasini ochiq yozib qo'ydi (biz ishga tushirdik). Transport'lar log ishini alohida worker thread'ga o'tkazadi; pino-pretty — faqat dev uchun.

Bu darsda

  • Pino'ning sukut qatorini o'qiy olasiz va uni o'z formatingizga sozlaysiz.
  • child, redact, serializer'larni loyihada qo'llaysiz.
  • pino-http ning xavfli sukut sozlamasini ko'rib, kanon sozlamasini tushuntirasiz.
  • Transport va pino.destination farqini, pino-pretty qayerda ishlatilishini bilasiz.
  • Pino, console.log va Winston tezligini halol o'lchov bilan solishtira olasiz.

Oldin bilishingiz kerak: Logging asoslari, worker_threads, Middleware va request lifecycle, Stack trace va error.cause.

1. Nega bu kerak?

Oldingi darsda kichik logger'ni o'zimiz yozdik va g'oyalarni ko'rdik: darajalar, JSON, bola logger, yashirish. Ishlab chiqarish uchun esa bundan ko'pi kerak. Xato obyektini to'g'ri yoyish, so'rov tugaganda vaqtni o'lchash, yuzlab so'rovda serverni sekinlashtirmaslik, sirlarni ishonchli yashirish.

Bularning hammasi tayyor kutubxonada bor. Node dunyosida eng ko'p ishlatiladiganlari — Pino va Winston. Fastify freymvorki Pino'ni ichiga qurgan, NestJS uchun nestjs-pino bor. Kanon vazifalar-api Pino'ni tanladi. Bu darsda u qanday ishlashini ichidan ko'ramiz, Winston — keyingi darsda.

Hayotiy o'xshatish: pochta bo'limining saralash mashinasi. Xatlar (log qatorlari) to'xtovsiz keladi. Mashina ularni bir xil konvertga soladi (JSON), maxfiy qismini qora bo'yoq bilan yopadi (redact), keraksiz ilovalarni olib tashlaydi (serializer) va alohida xonada jo'natadi (transport). Kassadagi navbat (asosiy thread) to'xtamaydi.

2. Pino'ning sukut qatori

2.1 Birinchi qator

js
import { pino } from "pino";

const log = pino();
log.info("salom");
log.info({ vazifaId: 7 }, "vazifa yaratildi");
log.debug("ko'rinmaydi");

Biz ishga tushirdik (Node 24.21.0, pino 10.4.0, Windows 11; hostname qiymati — kompyuter nomi, uni … bilan almashtirdik):

text
{"level":30,"time":1792823686552,"pid":12260,"hostname":"…","msg":"salom"}
{"level":30,"time":1792823686553,"pid":12260,"hostname":"…","vazifaId":7,"msg":"vazifa yaratildi"}

Maydonlarni o'qiymiz:

Maydon Ma'nosi
level daraja soni: 30 — info
time epoch millisekund (1970-yildan beri)
pid, hostname qaysi jarayon va qaysi kompyuter
msg xabar — oxirgi argument

Birinchi argument obyekt bo'lsa — uning maydonlari qatorga qo'shiladi. debug qatori yo'q: sukut chegara — info.

Darajalar Pino'da son bilan saqlanadi (oldingi darsdagi jadval):

js
console.log(pino.levels.values);
text
{ trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60 }

2.2 Kanon formati

Sukut format mashinaga qulay, lekin "time":1792823686552 ni odam o'qiy olmaydi. hostname esa konteynerda tasodifiy satr bo'ladi va shaxsiy kompyuter nomini oshkor qiladi. Kanon uchta sozlama bilan formatni o'zgartirdi:

js
const log = pino({
  base: { ilova: "vazifalar-api" }, // pid, hostname o'rniga
  timestamp: pino.stdTimeFunctions.isoTime,
  formatters: { level: (label) => ({ daraja: label }) },
});
log.info("iso");
text
{"daraja":"info","time":"2026-10-24T06:34:46.558Z","ilova":"vazifalar-api","msg":"iso"}

base — har qatordagi doimiy maydonlar (sukut — pid va hostname). isoTime — UTC ISO satr. formatters.level — daraja nomi bilan, o'zbekcha maydonda.

2.3 Noma'lum daraja — jarayon yiqiladi

LOG_LEVEL=yoq bilan Pino yaratilganda nima bo'ladi? Biz ishga tushirdik:

text
C:\Users\…\node_modules\pino\lib\levels.js:194
    throw Error(`default level:${defaultLevel} must be included in custom levels`)

default level:yoq must be included in custom levels — "sukut daraja yoq darajalar ro'yxatida bo'lishi kerak". Pino tashlaydi, lekin xabari chalkash. Shuning uchun kanon LOG_LEVEL ni Pino'gacha, config'da Zod enum bilan tekshiradi — xato tushunarli va exit 78 (env validatsiyasi).

3. Bola logger: child

js
const log = pino({
  base: { ilova: "vazifalar-api" },
  timestamp: false,
});
const sorovLogi = log.child({ sorovId: "kor-0001" });
sorovLogi.info({ vazifaId: 1 }, "vazifa yaratildi");
const eksportLogi = sorovLogi.child({ bolim: "eksport" });
eksportLogi.warn("sekin");

Biz ishga tushirdik:

text
{"level":30,"ilova":"vazifalar-api","sorovId":"kor-0001","vazifaId":1,"msg":"vazifa yaratildi"}
{"level":40,"ilova":"vazifalar-api","sorovId":"kor-0001","bolim":"eksport","msg":"sekin"}

Bola logger'ning bolasi ham bo'ladi — maydonlar yig'iladi. Pino bola maydonlarini yaratish paytida bir marta JSON'ga aylantirib qo'yadi. Shuning uchun child arzon: har so'rovga bittadan yasash odatiy hol.

Tekshirib ko'ring: sorovLogi.child({ sorovId: "boshqa" }) yasasak, qatorda nechta sorovId bo'ladi?

Javob

Ikkita. Biz ishga tushirdik (pino 10.4.0):

text
{"level":30,"sorovId":"kor-0001","sorovId":"boshqa","msg":"ikki"}

Pino tezlik uchun maydonlarni tekshirmasdan ketma-ket yozadi. JSON.parse oxirgisini oladi, lekin boshqa o'quvchilar birinchisini olishi mumkin. Qoida: bola logger'da ota maydonini qayta yozmang — yangi nom bering (ichkiId).

4. pino-http: har so'rovga bitta qator

4.1 Sukut sozlama — xavfli

pino-http — Node http (va Express) uchun middleware: so'rov tugaganda (res ning finish hodisasi) bitta qator yozadi — metod, yo'l, status, davomiylik. Shuningdek har req ga req.log — shu so'rovning bola logger'ini qo'shadi.

Sukut sozlama bilan oddiy node:http serverda sinab ko'rdik (kod qisqartirilgan, port — listen(0)):

js
const http = pinoHttp({
  logger: log,
  genReqId: (req) => req.headers["x-request-id"] ?? "yangi",
});
const server = createServer((req, res) => {
  http(req, res);
  req.log.info("handler ichida");
  res.end("ok");
});
// fetch: x-request-id: sinov-0001, authorization: Bearer SOXTA

Biz ishga tushirdik (pino-http 11.0.0; bitta qator — ikkinchisida xuddi shu req takrorlanadi):

text
{"level":30,"req":{"id":"sinov-0001","method":"GET","url":"/vazifalar","headers":{"host":"127.0.0.1:56184","connection":"keep-alive","x-request-id":"sinov-0001","authorization":"Bearer SOXTA","accept":"*/*","accept-language":"*","sec-fetch-mode":"cors","user-agent":"node","accept-encoding":"gzip, deflate"},"remoteAddress":"127.0.0.1","remotePort":56185},"msg":"handler ichida"}

Uchta muammo ko'rinadi:

  1. "authorization":"Bearer SOXTA" — token ochiq. Haqiqiy tizimda bu foydalanuvchining kaliti bo'lardi.
  2. Handler ichidagi har req.log.info qatoriga butun req (hamma sarlavhalar) qo'shildi — bitta so'rovda 5 qator bo'lsa, sarlavhalar 5 marta.
  3. Yakuniy qator xabari — umumiy "request completed", qidirish noqulay.

Sukut sozlama — ko'rgazma uchun, ishlab chiqarish uchun emas.

4.2 Kanon sozlamasi: sorovLogi

src/umumiy/log.ts (holat 44) — har qator izohi bilan:

ts
export function sorovLogi(log: Log): RequestHandler[] {
  const http = pinoHttp({
    logger: log,
    // ID — bizning X-Request-Id middleware'imizdan (holat 17)
    genReqId: (req) =>
      (req as unknown as { sorovId: string }).sorovId,
    customAttributeKeys: { reqId: "sorovId", responseTime: "ms" },
    // req.log ga faqat sorovId — har qatorga butun req yozilmasin
    quietReqLogger: true,
    // 4xx — warn, 5xx — error, 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
    customSuccessMessage: (req, res) =>
      `${String(req.method)} ${asliYol(req)} ` +
      String(res.statusCode),
    serializers: {
      // to'liq sarlavhalar o'rniga keraklilari
      req: (req) => ({
        method: req.method,
        url: req.raw?.originalUrl ?? req.url,
        ip: req.remoteAddress,
        userAgent: req.headers["user-agent"],
      }),
      res: (res) => ({ status: res.statusCode }),
    },
  });
  return [http /* , AsyncLocalStorage — Correlation ID darsida */];
}

Kod qisqartirilgan: tiplar va customErrorMessage olib tashlangan. Faylning to'liq yakuniy matni — Correlation ID darsining «API qadami» bo'limida (holat 44 shu darsda tugaydi).

Natija — tekshiruv-44.txt dan haqiqiy qator:

text
{"daraja":"warn","time":"2026-10-24T06:04:49.051Z","ilova":"vazifalar-api","sorovId":"kanon-44-0002","req":{"method":"GET","url":"/v1/vazifalar/99","ip":"127.0.0.1","userAgent":"curl/8.19.0"},"res":{"status":404},"ms":3,"msg":"GET /v1/vazifalar/99 404"}

Endi: daraja status bo'yicha (warn), xabar o'qiladi, sarlavhalardan faqat userAgent, vaqt ms maydonida.

4.3 Kanon topilmasi: req.url va originalUrl

Birinchi variantda xabar req.url dan yasalgan edi. Natija: "POST / 201". Nega /?

express.Router darsini eslang: v1.use("/vazifalar", router) ichida Express req.url dan ulangan qismni kesib tashlaydi — router o'zini ildizda deb biladi. pino-http esa finish paytida, router ichida qisqargan req.url ni oldi. To'liq yo'l req.originalUrl da saqlanadi. Kanon shu sababli asliYol() yordamchisini yozdi (so'rov parametrlarini ham kesadi — ?q=... logga tushmasin).

Tekshirib ko'ring: Nega serializer'da ip va userAgent bor-u, headers butunligicha yo'q?

Javob

Ikki sabab. Hajm: sarlavhalar har qatorni 3–4 barobar uzaytiradi, minglab so'rovda bu disk va pul. Maxfiylik: sarlavhalarda Authorization, Cookie bo'ladi — oq ro'yxat (faqat keraklisini olish) qora ro'yxatdan (redact) ishonchliroq: ertaga yangi maxfiy sarlavha qo'shilsa, u avtomatik tashqarida qoladi.

5. redact: sirlarni yashirish

5.1 Yo'llar va yulduzcha

Redact (redact) — "qora bo'yoq bilan yopish". Pino'ga JSON yo'llar ro'yxati beriladi, shu yo'llardagi qiymat yoziladigan paytda almashtiriladi:

js
const log = pino({
  base: undefined,
  timestamp: false,
  redact: ["req.headers.authorization", "*.parol"],
});
log.info(
  {
    req: { headers: { authorization: "Bearer SOXTA", host: "x" } },
    user: { ism: "Ali", parol: "123" },
  },
  "kirish",
);

Biz ishga tushirdik:

text
{"level":30,"req":{"headers":{"authorization":"[Redacted]","host":"x"}},"user":{"ism":"Ali","parol":"[Redacted]"},"msg":"kirish"}

*.parol — "istalgan bitta darajadagi obyektning parol maydoni". Yulduzcha bitta darajani bildiradi: a.b.parol uchun *.*.parol kerak. Kalit nomida tire bo'lsa — qavs: 'req.headers["x-api-key"]'.

5.2 Kanon ro'yxati va censor

ts
export const YASHIRIN = [
  "req.headers.authorization",
  "req.headers.cookie",
  'req.headers["x-api-key"]',
  "res.headers['set-cookie']",
  "*.parol",
  "*.token",
];
// logYarat ichida:
redact: { paths: YASHIRIN, censor: "[yashirildi]" },

censor — almashtiruvchi matn. remove: true bersangiz — maydon butunlay o'chadi:

text
{"level":30,"user":{"ism":"Ali"},"msg":"remove"}

Kanonda remove emas, censor tanlangan: "maydon bor edi, lekin yashirildi" degan iz qolsin — tergovda foydali.

Serializer req dan sarlavhalarni allaqachon olib tashlagan bo'lsa, redact nega kerak? Bu — ikkinchi qatlam. Kimdir ertaga log.info({ req }, …) deb xom so'rovni yozib yuborsa ham, token yopiq qoladi. Kanon testi: Bearer SOXTA-TOKEN bilan so'rov, keyin barcha log qatorlarida SOXTA-TOKEN qidiriladi — 0 marta.

5.3 Narxi

redact bepul emas. Kanon o'lchovi (t42-log.txt, 100 000 qator faylga): oddiy Pino — 882 ms, ikki yo'lli redact bilan — 1 326 ms. Ya'ni yashirish yozishni ~1.5 barobar sekinlashtirdi. Yo'llar ro'yxatini qisqa tuting va ** kabi chuqur qidiruvdan qoching.

6. Serializer'lar

6.1 Xato: err

Serializer (serializer) — maydon logga yozilishidan oldin uni aylantiradigan funksiya. Pino'da err kalitiga sukut serializer biriktirilgan — u Error ni oddiy obyektga yoyadi:

js
const asl = new Error("ECONNRESET");
const x = new Error("ulanish uzildi", { cause: asl });
log.error({ err: x }, "xato");

Biz ishga tushirdik (stack qatorlari qisqartirildi — …):

text
{"level":50,"err":{"type":"Error","message":"ulanish uzildi: ECONNRESET","stack":"Error: ulanish uzildi\n    at file:///…/e1.mjs:21:11\n    …\ncaused by: Error: ECONNRESET\n    at …"},"msg":"xato"}

Diqqat qiling: message ga cause ning xabari qo'shildi (ulanish uzildi: ECONNRESET), stack oxirida esa caused by: qismi bor. error.cause darsidagi zanjir logda to'liq saqlandi.

Nega bu muhim? Error ning message va stack xususiyatlari sanalmaydigan (non-enumerable). Oddiy JSON.stringify(xato) ularni ko'rmaydi va {} yozadi. Winston'da aynan shu tuzoqni keyingi darsda ko'ramiz.

6.2 O'z serializer'ingiz

js
const log = pino({
  base: undefined,
  timestamp: false,
  serializers: {
    vazifa: (v) => ({ id: v.id, uzunlik: v.matn.length }),
  },
});
const v = { id: 3, matn: "Non olish #uy", teglar: ["uy"] };
log.info({ vazifa: v }, "saqlandi");
text
{"level":30,"vazifa":{"id":3,"uzunlik":13},"msg":"saqlandi"}

Vazifa matnini logga yozmadik — faqat uzunligini. Foydalanuvchi matnida shaxsiy narsa bo'lishi mumkin ("Malikaga 998 90 … ga qo'ng'iroq").

7. Qayerga yozadi: destination va transport

7.1 pino.destination: sinxron va asinxron

Sukut bo'yicha Pino stdout'ga sinxron yozadi: log.info qaytganda qator allaqachon OS'ga berilgan. Jarayon yiqilsa ham oxirgi qatorlar yo'qolmaydi — fatal uchun muhim (kanon jarayon.ts dagi izoh shu haqda).

Asinxron rejim (pino.destination({ sync: false, minLength: 4096 })) qatorlarni bufer'da yig'ib, katta bo'laklarda yozadi. Nazariy jihatdan tezroq — kanon o'lchovida esa boshqacha chiqdi (pastdagi grafik).

7.2 Transport: alohida worker thread

Transport (transport) — log qatorlarini qayta ishlaydigan va jo'natadigan modul. Pino uni alohida worker thread da ishga tushiradi: formatlash, faylga yozish, tarmoqqa jo'natish asosiy thread'ni (so'rovlarni bajaradigan) bloklamaydi.

js
const t = pino.transport({
  targets: [
    {
      target: "pino/file",
      options: { destination: "./ilova.log" },
      level: "info",
    },
    {
      target: "pino-pretty",
      options: { colorize: false },
      level: "warn",
    },
  ],
});
const log = pino({ base: undefined, timestamp: false }, t);
log.info({ vazifaId: 1 }, "vazifa yaratildi");
log.warn({ ms: 1200 }, "sekin so'rov");

Biz ishga tushirdik — terminalda (faqat warn va yuqori, chiroyli):

text
WARN: sekin so'rov
    ms: 1200

ilova.log faylida (info va yuqori, JSON):

text
{"level":30,"vazifaId":1,"msg":"vazifa yaratildi"}
{"level":40,"ms":1200,"msg":"sekin so'rov"}

Bitta logger, ikkita manzil, har biriga o'z chegarasi. Shuningdek process.exit(0) dan oldin yozilgan qator ham transport orqali faylga yetib bordi (sinadik) — Pino chiqishda worker'ni kutadi.

Kanonda transport yo'q: 12-factor bo'yicha ilova faqat stdout'ga yozadi, qolgani muhitda. Transport — muhit stdout'ni yig'a olmaydigan holatlar uchun (masalan, serverda to'g'ridan logni tashqi xizmatga jo'natish).

flowchart LR
  A[log.info] --> B{manzil}
  B -->|sukut| S[stdout, sinxron]
  B -->|transport| W[worker thread]
  W --> F[pino/file]
  W --> P[pino-pretty]
  W --> X[tashqi xizmat]

Rasmga qarang: transport bilan log.info faqat qatorni worker'ga uzatadi; og'ir ish o'sha yerda.

7.3 pino-pretty — faqat dev

pino-pretty JSON'ni odam o'qiydigan ko'rinishga aylantiradi. Kanon uni devDependencies ga qo'ydi va faqat dev skriptida quvur orqali ishlatadi:

json
{
  "scripts": {
    "dev": "node --watch --env-file-if-exists=.env src/server.ts | pino-pretty"
  }
}

Kanon tajribasidagi ko'rinish (t42-log.txt):

text
[11:02:05.059] INFO (9428): vazifa yaratildi
    sorovId: "s-0"
    vazifaId: 0
    ms: 3

Ishlab chiqarishda nega yo'q? U har qatorni qayta parse qilib formatlaydi — CPU sarflaydi. Natija JSON emas — yig'uvchi tizim maydonlarni ajrata olmaydi. Va u prod bog'liqlik bo'lib, hajmni oshiradi (19 paket).

8. O'lchov: Pino haqiqatan tezmi?

Pino o'zini "eng tez Node logger" deb tanishtiradi. Kanon buni tekshirdi: 100 000 qator, bitta jarayon, Windows NTFS (t42-log.txt; band mashina, bitta o'lchov):

100 000 log qatori (kamroq — yaxshi)
  • console.log → quvurbola jarayon stdout'i553 ms
  • pino sinxron fayl882 ms
  • pino asinxron fayl1 056 ms
  • winston File1 246 ms
  • pino + redact1 326 ms

Manba: IlmHamroh kanon o'lchovi, 2026-10-24: Node 24.21.0, Windows 11, i5-12500H (t42-log.txt), bitta o'lchov

Halol o'qiymiz:

  • console.log eng tez chiqdi — lekin u boshqa joyga yozdi (bola jarayon stdout quvuri), Pino esa faylga. Bu o'lchovdan "console tezroq" degan xulosa chiqmaydi — ular teng sharoitda emas.
  • Teng sharoitda (ikkalasi faylga): Pino 882 ms, Winston 1 246 ms — Pino ~1.4 barobar tez.
  • Asinxron rejim bu sinovda sinxrondan sekinroq chiqdi (1 056 ms). Sababini kanon aniqlamagan; bitta o'lchov.
  • redact eng qimmat — ehtiyot bo'ling.

Serverda log narxi qanday? Kanon o'lchovlarida mavjud bo'lmagan vazifaga so'rov holat 40 da (log yo'q) 5 116 req/s, holat 44 da (har 404 — bitta warn qatori) 5 176 req/s chiqdi (tekshiruv-40.txt, tekshiruv-44.txt; autocannon, bitta mashina, bitta o'lchov). Log bilan biroz tezroq — demak farq o'lchov shovqini ichida. Bu yukda bitta log qatorining narxi sezilmadi; 100 000 qatorli sinov esa uni alohida ko'rsatadi (yuqoridagi grafik).

9. API qadami: pino-http, redact, pino-pretty (holat 44, 2-qism)

Holat 44 ning ikkinchi qismi. Hali commit qilmang — commit Correlation ID darsida.

bash
npm i --save-exact pino-http@11.0.0
npm i -D --save-exact pino-pretty@13.2.0

O'zgarishlar:

Fayl O'zgarish
src/umumiy/log.ts YASHIRIN va redact; sorovLogi()
src/ilova.ts log bog'liqligi (sukut logYarat("silent")); app.use(sorovId()) dan keyin app.use(sorovLogi(log))
src/server.ts bogliqliklar ga log
package.json dev — | pino-pretty
test/yordamchi.ts xotiraLogi() — testda loglarni o'qish

Xato ishlovchi, xizmat, eksport va idempotentlik bugun o'zgarmaydi — ular hali console.error bilan yozadi. Ular logger'ga Correlation ID darsida o'tadi: o'sha qatorlarda sorovId bo'lishi kerak, buning uchun AsyncLocalStorage kerak.

Bugungi kesimda src/umumiy/log.ts — to'liq (yakuniy fayldan AsyncLocalStorage qismi olib tashlangan; u Correlation ID darsida qo'shiladi):

ts
// log.ts — strukturali (JSON) log: Pino. stdout'ga yoziladi
// (12-factor) — faylga yozish, rotatsiya, yig'ish — muhitning
// ishi (Docker, Loki — 34-qism).
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);
}

/**
 * pino-http: har so'rov oxirida bitta qator
 * (metod, yo'l, status, vaqt),
 * sorovId — bizning X-Request-Id.
 * 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 child logger'iga 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];
}

src/ilova.ts — holat 40 dan farqi (qolgan qatorlar o'zgarmaydi):

diff
--- a/ilova.ts
+++ b/ilova.ts
 import type { XatoLogi } from "./umumiy/xato-ishlovchi.ts";
+import { logYarat, sorovLogi } from "./umumiy/log.ts";
+import type { Log } from "./umumiy/log.ts";
@@ Bogliqliklar @@
   xatoLogi?: XatoLogi;
+  /** Pino logger (sukut — "silent": testlarda jim) */
+  log?: Log;
@@ ilovaYarat parametrlari @@
   xatoLogi,
+  log = logYarat("silent"),
   tz = "Asia/Tashkent",
@@ middleware tartibi @@
   app.use(sorovId());
+  app.use(sorovLogi(log));

src/server.ts da bogliqliklar obyektiga bitta qator qo'shiladi — log, (hsts: config.HSTS, dan keyin).

Tartib muhim: sorovLogi sorovId() dan keyin turadi — genReqId tayyor req.sorovId ni oladi. Undan oldin qo'ysangiz, sorovId maydoni undefined bo'ladi.

Testlar uchun yordamchi — logni stdout o'rniga xotiradagi massivga yozadi (logYarat ning ikkinchi parametri — «Logging asoslari» darsidagi manzil):

ts
export function xotiraLogi(daraja = "info") {
  const qatorlar: Record<string, unknown>[] = [];
  const log = logYarat(daraja, {
    write: (q: string) => {
      qatorlar.push(JSON.parse(q) as Record<string, unknown>);
    },
  });
  return { log, qatorlar };
}

Pino uchun { write(satr) } obyekti — yetarli manzil. Testda keyin qatorlar.find((q) => q.msg === "…") bilan tekshiramiz. ilovaYarat da esa sukut log = logYarat("silent") — testlar jim ishlaydi.

Diqqat: Oraliq kesim. Biz bugungi kesimni holat 44 nusxasida yig'ib tekshirdik (Node 24.21.0, npm ci): tip 0, lint 0, testlar 54/54. Bitta tuzoqni ham ko'rdik. Xato ishlovchini bugunoq log.error(...) ga o'tkazsak, xato.test.ts dagi 500 testi qizardi (53/54). Sabab — test hali console.error chaqirilishini kutadi. Shuning uchun xato ishlovchi va uning testi keyingi qadamda, birga o'zgaradi.

10. Ko'p uchraydigan xatolar

10.1 pino-http sukut sozlamasi bilan prod'ga chiqish

Authorization va hamma sarlavhalar logda («Sukut sozlama — xavfli» bo'limi). redact + req serializer + quietReqLogger.

10.2 Xatoni error emas, boshqa kalitda berish

log.error({ xato: x }) — xato kalitiga serializer yo'q. Biz sinadik: "xato":{} — xabar ham, stack ham yo'q. err kaliti (yoki serializers ga o'zingiz qo'shing).

10.3 Xabarni birinchi argument qilish

log.info("vazifa", { vazifaId: 1 }) — Pino'da obyekt birinchi turadi. Biz sinadik: natija {"level":30,"msg":"vazifa"} — vazifaId jimgina yo'qoldi (obyekt "format argumenti" deb olinadi). log.info({ vazifaId: 1 }, "vazifa yaratildi").

10.4 pino-pretty ni prod bog'liqlikka qo'yish

devDependencies va faqat dev skriptida quvur.

10.5 redact da yulduzcha chuqurligini noto'g'ri tushunish

*.parol faqat bitta daraja ichida ishlaydi: { a: { b: { parol } } } yopilmaydi. Aniq yo'l yoki *.*.parol; testda tekshiring.

11. Mashqlar

1-mashq (oson): natijani ayting

Logger redact: ["user.parol", "*.token"] bilan yasalgan, base: undefined, timestamp: false. Qator: log.info({ user: { ism: "Sardor", parol: "1", token: "t" }, kalit: { token: "k" } }, "x"). Nima yoziladi?

Yechim
text
{"level":30,"user":{"ism":"Sardor","parol":"[Redacted]","token":"[Redacted]"},"kalit":{"token":"[Redacted]"},"msg":"x"}

user.parol — aniq yo'l. *.token — har birinchi darajadagi obyektning token i: user.token ham, kalit.token ham. Natijani biz ishga tushirib oldik (pino 10.4.0).

2-mashq (o'rta): customLogLevel funksiyasi

Kanondagi daraja tanlovini alohida toza funksiya qiling: darajaTanla(status, xato) — xato bo'lsa yoki status ≥ 500 — "error", 400–499 — "warn", qolgani — "info". Qo'shimcha: 499 (mijoz so'rovni tashlab ketdi) — "info" bo'lsin (bu bizning xato emas va ogohlantirish ham emas). [200, 304, 404, 422, 499, 500] va xatoli 200 uchun chiqaring.

Yechim
js
function darajaTanla(status, xato) {
  if (xato !== undefined || status >= 500) return "error";
  if (status === 499) return "info";
  return status >= 400 ? "warn" : "info";
}

const statuslar = [200, 304, 404, 422, 499, 500];
console.log(statuslar.map((s) => `${s}:${darajaTanla(s)}`).join(" "));
console.log(darajaTanla(200, new Error("finish'dan keyin")));

Konsolda:

text
200:info 304:info 404:warn 422:warn 499:info 500:error
error

Toza funksiya — Express'siz test qilinadi. pinoHttp({ customLogLevel: (_req, res, x) => darajaTanla(res.statusCode, x) }) bilan ulanadi. 499 — standart kod emas (nginx kiritgan), lekin loglarda ko'p uchraydi.

3-mashq (qiyin): asliYol va maxfiy query

Kanondagi asliYol(req) so'rov parametrlarini kesadi. Ba'zan esa ?limit=20&sort=-yaratildi foydali. Funksiya yozing: xavfsizYol(originalUrl, ruxsat) — yo'l + faqat ruxsat to'plamidagi parametrlar (tartibi saqlansin), qolganlari [yashirildi]. Sinang: "/v1/vazifalar?limit=20&token=abc&q=Malika", ruxsat — limit, sort. Eslatma: URL va URLSearchParams — URL darsi.

Yechim
js
function xavfsizYol(asl, ruxsat) {
  const url = new URL(asl, "http://x");
  for (const [k] of url.searchParams) {
    if (!ruxsat.has(k)) url.searchParams.set(k, "[yashirildi]");
  }
  const qism = url.searchParams.toString();
  return url.pathname + (qism ? `?${decodeURIComponent(qism)}` : "");
}

const ruxsat = new Set(["limit", "sort"]);
const yol = "/v1/vazifalar?limit=20&token=abc&q=Malika";
console.log(xavfsizYol(yol, ruxsat));
console.log(xavfsizYol("/v1/vazifalar", ruxsat));

Konsolda:

text
/v1/vazifalar?limit=20&token=[yashirildi]&q=[yashirildi]
/v1/vazifalar

new URL(nisbiy, asos) — nisbiy yo'lni parse qilish uchun soxta asos. set mavjud kalitning qiymatini joyida almashtiradi — tartib saqlanadi. Qidiruv matni (q=Malika) ham shaxsiy bo'lishi mumkin — shuning uchun oq ro'yxat.

4-mashq: API qadami

pino-http va pino-pretty ni o'rnating, log.ts ga YASHIRIN, redact va sorovLogi ni qo'shing, ilova.ts da sorovId() dan keyin ulang. npm run dev bilan bitta POST va bitta yo'q vazifaga GET yuboring: birinchisi INFO, ikkinchisi WARN ko'rinishida chiqsin. Keyin Authorization: Bearer SOXTA sarlavhasi bilan so'rov yuborib, chiqishda SOXTA yo'qligini tekshiring. Commit qilmang.

Yechim

Kod — «Kanon sozlamasi» va «API qadami» bo'limlarida. npm start (pretty'siz) bilan JSON ko'rinishida tekshirish qulayroq: npm start > server.log, so'rovlardan keyin server.log da SOXTA ni qidiring — 0 natija. Kanon jonli tekshiruvida xuddi shunday: SOXTA-TOKEN logda: 0 marta.

12. Real ishda

  • Fastify Pino'ni ichiga qurgan: fastify({ logger: true }) — xuddi shu JSON qatorlar. NestJS'da nestjs-pino (30-qism).
  • Bulut loglari: Google Cloud, AWS va Grafana Loki JSON qatordan level, msg ni avtomatik tanib oladi; ba'zilari severity nomini kutadi — formatters.level aynan shuning uchun bor.
  • Xavfsizlik auditi: "loglarda token bormi?" — tez-tez beriladigan savol; redact va uning testi — tayyor javob.
  • Intervyu savollari: "Pino nega tez?", "Transport nima uchun worker thread'da?", "Logda sirlarni qanday yashirasiz?", "pino-pretty ni prod'da ishlatasizmi?".

Xulosa

  • Pino — JSON yozadigan tez logger; sukut qatori: level, time, pid, hostname, msg — base, timestamp, formatters bilan sozlanadi.
  • child maydonlarni bog'laydi va arzon; pino-http har so'rovga req.log va yakuniy qator beradi.
  • pino-http sukut sozlamasi Authorization ni ochiq yozdi — redact, req serializer va quietReqLogger shart.
  • err serializer message, stack va cause zanjirini saqlaydi; o'z serializer'ingiz — oq ro'yxat.
  • Transport — alohida worker thread; kanon esa faqat stdout (12-factor); pino-pretty — faqat dev.
  • O'lchov: teng sharoitda Pino Winston'dan ~1.4 barobar tez; redact ~1.5 barobar qimmatlashtiradi.

Keyingi dars: Winston va log rotatsiya — Winston'ning format va transport tizimi, xato obyektidagi tuzoq, log fayllari diskni to'ldirmasligi uchun rotatsiya va nega kanon faylga yozmaydi.

Manbalar

  • Pino hujjati: API, Redaction, Transports (pino 10.x, 2026-10-24 da ko'rildi).
  • pino-http: README (11.x) — quietReqLogger, customLogLevel, customAttributeKeys.
  • Kanon: vazifalar-api holat 44 (src/umumiy/log.ts, test/yordamchi.ts), natijalar/tekshiruv-44.txt, natijalar/tajriba/t42-log.txt.
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Pino logger chuqur: pino-http, redact, serializer va transport — IlmHamroh