Перейти к содержимому

14. Логирование

Иллюстрация к уроку

Логирование — запись событий приложения. Без логов ты слепой: не знаешь что происходит, не можешь дебажить production.

❌ Плохо:
console.log('here')
console.log('user', user)
console.log(JSON.stringify(data))
✅ Хорошо:
logger.info('User logged in', { userId: user.id, ip: req.ip })
logger.error('Payment failed', { orderId, error: err.message, stack: err.stack })

Правила:

  • Структурированные логи (JSON) — не строки
  • Уровни важности: error, warn, info, debug
  • Контекст: кто, что, когда, откуда
  • Не логировать секреты
  • Не логировать бесполезный шум
Окно терминала
npm install winston
lib/logger.ts
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
process.env.NODE_ENV === 'production'
? winston.format.json() // JSON в production
: winston.format.combine( // Красивый вывод в dev
winston.format.colorize(),
winston.format.simple()
)
),
transports: [
new winston.transports.Console(),
// В production — пишем в файлы
...(process.env.NODE_ENV === 'production' ? [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5 * 1024 * 1024, // 5MB
maxFiles: 5,
tailable: true,
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
tailable: true,
}),
] : []),
],
// Не крашиться при необработанных исключениях
exceptionHandlers: [
new winston.transports.File({ filename: 'logs/exceptions.log' }),
],
rejectionHandlers: [
new winston.transports.File({ filename: 'logs/rejections.log' }),
],
});
export default logger;
Окно терминала
npm install pino pino-pretty
lib/logger.ts
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV !== 'production'
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss',
ignore: 'pid,hostname',
},
}
: undefined,
redact: {
paths: ['password', 'token', 'authorization', '*.password'],
remove: true,
},
});
export default logger;
import logger from './logger';
// HTTP middleware — добавляем requestId к каждому запросу
app.use((req, res, next) => {
req.log = logger.child({
requestId: crypto.randomUUID(),
method: req.method,
url: req.url,
ip: req.ip,
});
next();
});
// В обработчике
app.post('/api/orders', async (req, res) => {
const log = req.log.child({ userId: req.user?.id });
log.info('Creating order', { items: req.body.items?.length });
try {
const order = await createOrder(req.body);
log.info('Order created', { orderId: order.id, total: order.total });
res.json(order);
} catch (error) {
log.error('Order creation failed', { error: error.message, stack: error.stack });
res.status(500).json({ error: 'Internal server error' });
}
});
// Когда использовать каждый уровень:
logger.error('Database connection failed', { error }); // Критичная ошибка
logger.warn('Rate limit approaching', { current, limit }); // Предупреждение
logger.info('User registered', { userId }); // Важное событие
logger.debug('Cache hit', { key, ttl }); // Детали для дебага
logger.trace('SQL query', { query, params }); // Очень детальные логи
Окно терминала
# Управление уровнем через env
LOG_LEVEL=debug npm start # видим всё
LOG_LEVEL=info npm start # только info и выше (production)
LOG_LEVEL=warn npm start # только warn и error
Окно терминала
npm install morgan # для Express
import morgan from 'morgan';
// Формат для production — JSON
const format = process.env.NODE_ENV === 'production'
? (tokens: any, req: any, res: any) => JSON.stringify({
method: tokens.method(req, res),
url: tokens.url(req, res),
status: Number(tokens.status(req, res)),
duration: Number(tokens['response-time'](req, res)),
size: tokens.res(req, res, 'content-length'),
})
: 'dev'; // красивый вывод для разработки
app.use(morgan(format, {
stream: {
write: (message: string) => logger.http(message.trim()),
},
skip: (req) => req.url === '/health', // не логировать health checks
}));
// middleware.ts — логируем каждый запрос
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const start = Date.now();
const response = NextResponse.next();
response.headers.set('x-request-id', crypto.randomUUID());
// Логируем после обработки (через waitUntil в edge)
console.log(JSON.stringify({
level: 'info',
method: request.method,
url: request.url,
duration: Date.now() - start,
timestamp: new Date().toISOString(),
}));
return response;
}
# docker-compose.yml — ELK Stack для production
services:
elasticsearch:
image: elasticsearch:8.12.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
volumes:
- es_data:/usr/share/elasticsearch/data
logstash:
image: logstash:8.12.0
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
kibana:
image: kibana:8.12.0
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
Окно терминала
# Journalctl (systemd)
sudo journalctl -u myapp -f # follow
sudo journalctl -u myapp -n 100 # последние 100 строк
sudo journalctl -u myapp --since "1 hour ago"
# Docker логи
docker logs myapp -f
docker logs myapp --tail 100
docker logs myapp --since 2h
# Docker Compose
docker compose logs app -f
docker compose logs --since 30m
# PM2
pm2 logs myapp
pm2 logs myapp --lines 200
# Поиск в логах
sudo journalctl -u myapp | grep ERROR
sudo journalctl -u myapp | jq 'select(.level == "error")'
  • Структурированные JSON логи — лучше строк (поиск и анализ)
  • Pino — быстрый, Winston — популярный, оба хороши
  • Child loggers с requestId — для трассировки запроса через систему
  • Не логируй секреты — используй redact в pino или фильтры
  • Уровни: error > warn > info > debug > trace
  • В production — info уровень, в dev — debug
  • Централизованные логи (ELK, Loki+Grafana) — для серьёзных проектов

Уровни логирования и фильтрация: