Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як реалізувати логування в Express.js (Morgan, Winston)?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Логування в Express.js** поєднує Morgan для відстеження HTTP-запитів і Winston для подій застосунку. Morgan працює як middleware і автоматично логує кожен запит; Winston викликається явно у коді. ```js app.use(morgan('combined')); // GET /users/42 200 5ms - автоматично const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [new winston.transports.File({ filename: 'app.log' })] }); logger.info('User created', { userId: 42 }); // явний виклик ``` **Ключове:** Morgan = автоматичне відстеження HTTP. Winston = явні події застосунку.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Логування (logging) в Express.js** будується на двох бібліотеках з двома різними задачами: Morgan перехоплює кожен HTTP-запит автоматично; Winston логує те, що твій код явно йому передає. ## Теорія ### TL;DR - **Morgan** = логер HTTP-запитів (відеокамера, що фіксує кожного відвідувача) - **Winston** = логер застосунку (блокнот для записів того, що сталося всередині) - Morgan - це middleware: запускається на кожен запит без додаткового коду в роутах - Winston викликається явно: `logger.info()`, `logger.error()` - Правило вибору: Morgan для HTTP-трафіку; Winston для помилок, бізнес-логіки і всього іншого ### Швидкий приклад ```js const express = require('express'); const morgan = require('morgan'); const winston = require('winston'); const app = express(); // Morgan логує кожен HTTP-запит автоматично app.use(morgan('combined')); // Winston логує тільки те, що ти явно скажеш const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [new winston.transports.File({ filename: 'app.log' })] }); app.get('/users/:id', (req, res) => { logger.info(`Fetching user ${req.params.id}`); // Ти вирішуєш, коли це писати res.json({ id: req.params.id }); }); // Вивід Morgan: GET /users/123 200 5ms // Вивід Winston: {"level":"info","message":"Fetching user 123"} ``` Morgan зафіксував HTTP-транзакцію. Winston зафіксував бізнес-подію. Вони нічого не знали одне про одного. ### Головна різниця Morgan - це **middleware**: підключається через `app.use()` і пише рядок логу після того, як відповідь відправлена. Код в роутах для цього не потрібен. Winston - це **бібліотека**, яку викликаєш вручну. Якщо не написав `logger.info(...)`, нічого не залогується. Morgan відповідає на питання "який HTTP-трафік прийшов на сервер?"; Winston - "що зробив код застосунку?" ### Коли що використовувати - **Тільки Morgan:** прості API, середовище розробки, коли достатньо логів HTTP-трафіку - **Тільки Winston:** CLI-інструменти, фонові задачі, сервіси без HTTP - **Обидва разом:** будь-який production-застосунок - більшість випадків - **Morgan + кастомний токен:** коли потрібно логувати тіло запиту, userId або кастомні заголовки - **Winston + кілька транспортів:** коли логи мають іти у файли і до зовнішніх сервісів (Datadog, CloudWatch, Sentry) ### Порівняння | Аспект | Morgan | Winston | |--------|--------|---------| | **Що логує** | HTTP-запити/відповіді | Події застосунку, помилки, кастомні повідомлення | | **Як використовується** | Middleware (`app.use()`) | Прямі виклики (`logger.info()`, `logger.error()`) | | **Автоматично?** | Так, кожен запит | Ні, ти вирішуєш що логувати | | **Формат виводу** | Готові (dev, combined, tiny) або кастомний | Повністю конфігурується (JSON, текст тощо) | | **Вплив на продуктивність** | Мінімальний | Залежить від транспортів (файловий I/O асинхронний за замовчуванням) | | **Коли використовувати** | Відстеження HTTP-трафіку | Помилки, бізнес-логіка, налагодження | ### Як це працює Morgan підключається до циклу відповіді Express. На кожен запит він чекає події `res.on('finish')` і тільки тоді пише рядок логу. Тому в записі є фінальний статус-код і розмір відповіді. Нічого не блокується. Winston влаштований інакше. Коли викликаєш `logger.info()`, Winston форматує повідомлення і передає кожному налаштованому транспорту. Файлові транспорти використовують `fs.createWriteStream()` з буферизованими записами за замовчуванням. Якщо транспорт повільний (наприклад, HTTP-запит до зовнішнього сервісу), Winston ставить повідомлення в чергу замість того, щоб блокувати обробник роуту. ### Типові помилки **Помилка 1: логування чутливих даних** ```js // Неправильно: паролі й токени потраплять у файл логів morgan.token('body', (req) => JSON.stringify(req.body)); app.use(morgan(':method :url :body')); // Правильно: фільтруй чутливі поля перед логуванням morgan.token('body', (req) => { const { password, token, ...safe } = req.body || {}; return JSON.stringify(safe); }); app.use(morgan(':method :url :body')); ``` Хто отримає доступ до файлів логів - отримає паролі. Це не параноя, це реальний вектор атаки. **Помилка 2: синхронні записи у файл** ```js // Неправильно: кожен запис блокує весь event loop const logger = winston.createLogger({ transports: [new winston.transports.File({ filename: 'app.log', sync: true })] }); // Правильно: async - це поведінка за замовчуванням, просто не встановлюй sync: true const logger = winston.createLogger({ transports: [new winston.transports.File({ filename: 'app.log' })] }); ``` Одна повільна операція запису з `sync: true` додає 10-100 мс затримки до кожного запиту. **Помилка 3: неправильні рівні логування** ```js // Неправильно: все на рівні "info" logger.info('Користувач увійшов'); logger.info('Не вдалося підключитися до БД'); // Це ж помилка! logger.info('Cache miss для ключа xyz'); // Це шум для debug // Правильно: рівень відповідає реальній важливості logger.info('Користувач увійшов'); logger.error('Не вдалося підключитися до БД'); logger.debug('Cache miss для ключа xyz'); ``` В production встановлюють `level: 'error'` щоб відфільтрувати шум. Якщо помилки залоговані на рівні `info`, вони зникнуть разом з усім іншим під час фільтрації. **Помилка 4: створення нового логера на кожен запит** ```js // Неправильно: відкриває нові файлові дескриптори на кожному запиті app.get('/api/data', (req, res) => { const logger = winston.createLogger({ /* config */ }); logger.info('Обробка запиту'); res.json({}); }); // Правильно: створити один раз на рівні модуля const logger = winston.createLogger({ /* config */ }); app.get('/api/data', (req, res) => { logger.info('Обробка запиту'); res.json({}); }); ``` При великому трафіку швидко досягаєш ліміту ОС і отримуєш "EMFILE: too many open files" - застосунок падає. **Помилка 5: логування health check запитів** ```js // Неправильно: моніторинг пінгує /health кожні 5 секунд, засмічує логи app.use(morgan('combined')); // Правильно: ігноруй запити, які тебе не цікавлять app.use(morgan('combined', { skip: (req) => req.path === '/health' })); ``` Health check раз на 5 секунд - це 17 000 зайвих рядків у логах за день. ### Де застосовується - **Звичайні Express API:** Morgan для HTTP, Winston для помилок і бізнес-подій - **Next.js:** Morgan в API routes, Winston для серверного логування - **AWS Lambda:** Winston з CloudWatch transport - **Docker/Kubernetes:** вивід у stdout, платформа сама маршрутизує в ELK або Datadog - **NestJS:** команди часто додають Winston поверх вбудованого логера для збереження у файли ### Питання на співбесіді **Q:** Чому Morgan чекає завершення відповіді перед тим як писати лог? **A:** Бо Morgan записує HTTP-статус і розмір відповіді, які невідомі поки відповідь не відправлена. Він підписується на `res.on('finish')` і пише рядок тільки після цього. **Q:** Як виключити запити до /health з логів Morgan? **A:** Через опцію `skip`: `app.use(morgan('combined', { skip: (req) => req.path === '/health' }))`. Без цього інструменти моніторингу, які пінгують endpoint кожні кілька секунд, засмічують логи. **Q:** Яка різниця між `format.json()` і `format.simple()` у Winston? **A:** `json()` виводить структурований лог (один JSON-об'єкт на рядок), який зручно парсити сервісам агрегації. `simple()` виводить читабельний текст. Для локальної розробки підходить `simple()`, для production - `json()`. **Q:** Як переконатися, що логи записані перед завершенням процесу? **A:** Викликати `logger.close()` в обробнику завершення: `process.on('SIGTERM', () => { logger.close(); process.exit(0); })`. Без цього буферизовані повідомлення можуть ніколи не потрапити у файл. **Q:** Як пов'язати логи між мікросервісами? **A:** Генеруєш унікальний ID запиту (UUID) на вхідній точці, передаєш його через HTTP-заголовки у всі сервіси і включаєш у кожен лог Winston. Інструменти на кшталт Datadog або ELK групують записи за цим ID. Цей підхід називається distributed tracing. **Q (рівень senior):** API обробляє 100k запитів на секунду, Morgan дає 15% навантаження на CPU. Як вирішити? **A:** Кілька варіантів у порядку ефективності. Перейти на `morgan('tiny')` щоб зменшити обсяг форматування. Семплювати запити: `skip: () => Math.random() > 0.1` логує тільки 10%. Замінити Winston на Pino - значно швидше для структурованого логування. Перенести логування в окремий процес через Redis або Kafka. Або взагалі вимкнути Morgan і покластися на логи reverse proxy (Nginx, Cloudflare). Правильна відповідь залежить від того, яка точність логів тобі насправді потрібна. ## Приклади ### Базове налаштування: Morgan і Winston разом ```js const express = require('express'); const morgan = require('morgan'); const winston = require('winston'); const app = express(); app.use(express.json()); // HTTP-логування (автоматичне) app.use(morgan('dev')); // Логування застосунку (ручне) const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'logs/app.log' }) ] }); app.get('/users/:id', (req, res) => { logger.info('Fetching user', { userId: req.params.id }); res.json({ id: req.params.id, name: 'Alice' }); }); app.listen(3000, () => logger.info('Server started', { port: 3000 })); // Morgan виводить: GET /users/42 200 3.456 ms - 28 // Winston записує: {"level":"info","timestamp":"...","message":"Fetching user","userId":"42"} ``` Morgan запрацював без єдиного рядка в обробнику роуту. Winston залогував тільки те, що ти йому сказав. ### Production-налаштування: ротація логів і обробка помилок ```js const express = require('express'); const morgan = require('morgan'); const winston = require('winston'); require('winston-daily-rotate-file'); const app = express(); app.use(express.json()); // Щоденна ротація: зберігає 14 днів, максимум 20 МБ на файл const transport = new winston.transports.DailyRotateFile({ filename: 'logs/application-%DATE%.log', datePattern: 'YYYY-MM-DD', maxSize: '20m', maxFiles: '14d' }); const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [transport] }); // Перенаправляємо вивід Morgan через Winston - один файл для всього const morganStream = { write: (msg) => logger.http(msg.trim()) }; app.use(morgan('combined', { stream: morganStream })); app.post('/api/orders', (req, res) => { try { logger.info('Order created', { userId: req.user?.id, orderId: req.body.id }); res.json({ success: true }); } catch (err) { logger.error('Order creation failed', { error: err.message, stack: err.stack }); res.status(500).json({ error: 'Failed' }); } }); // Скидаємо буфер перед завершенням процесу process.on('SIGTERM', () => { logger.close(); process.exit(0); }); ``` Коли Morgan виводить через Winston - всі логи, і HTTP, і застосунку, йдуть в один файл. Одна команда grep покриває все під час розбору production-інциденту. ### Відстеження запиту за унікальним ID ```js const express = require('express'); const morgan = require('morgan'); const winston = require('winston'); const { v4: uuidv4 } = require('uuid'); const app = express(); const logger = winston.createLogger({ format: winston.format.json(), transports: [new winston.transports.Console()] }); // Прикріплюємо унікальний ID до кожного запиту app.use((req, res, next) => { req.id = uuidv4(); res.setHeader('X-Request-ID', req.id); next(); }); // Включаємо ID в логи Morgan morgan.token('request-id', (req) => req.id); app.use(morgan(':request-id :method :url :status :response-time ms')); // Дочірній логер автоматично додає requestId до кожного запису app.use((req, res, next) => { req.logger = logger.child({ requestId: req.id }); next(); }); app.get('/api/data', (req, res) => { req.logger.info('Fetching data'); // {"message":"Fetching data","requestId":"abc-123"} res.json({ ok: true }); }); // Morgan: abc-123 GET /api/data 200 4.5 ms // Winston: {"message":"Fetching data","requestId":"abc-123"} // Обидва мають однаковий ID - один пошук знаходить все ``` Коли запит падає в production - шукаєш за request ID і бачиш усі записи, і від Morgan, і від Winston, які до нього відносяться. Без цього доводиться вгадувати по таймстемпах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.