Ваше приложение — чёрный ящик? Пора это исправить
Вы когда-нибудь ловили себя на мысли: «Почему мой сервис на Node.js тормозит, хотя код идеален?» Или получали жалобу от пользователя, но не могли понять, что пошло не так? Знакомо? Добро пожаловать в мир, где без observability в JavaScript вы работаете вслепую.
Observability (наблюдаемость) — это не просто модное слово. Это подход, который превращает ваш код из чёрного ящика в прозрачную систему. Мы научимся задавать ей вопросы: «Кто виноват?» (трейсинг), «Что происходит прямо сейчас?» (метрики) и «Что случилось вчера в 3:15?» (логи).
В этой статье мы разберём три столпа observability в Node.js: трейсинг, метрики и логи. Вы получите готовые примеры кода и поймёте, как внедрить эти практики уже сегодня. Поехали!
Трейсинг в JavaScript: путешествие запроса
Представьте, что пользователь нажимает кнопку «Оформить заказ». Этот запрос проходит через API Gateway, сервис авторизации, корзину, платежный шлюз и базу данных. Если что-то сломалось — где искать? Трейсинг (или distributed tracing) даёт вам карту этого путешествия.
Что такое трейсинг простыми словами
Трейсинг — это способ отследить путь конкретного запроса через все микросервисы и функции. Каждый шаг — это span, а вся цепочка — trace. Вы видите, сколько времени занял каждый этап и где произошла ошибка.
OpenTelemetry: стандарт для observability в JavaScript
OpenTelemetry (OTel) — это индустриальный стандарт для сбора трейсов, метрик и логов. Он поддерживается всеми крупными облаками и инструментами (Jaeger, Zipkin, Grafana).
Установка:
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
Базовый пример трейсинга для Express-приложения:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { ConsoleSpanExporter } = require('@opentelemetry/exporter-console');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.register();
const { getTracer } = require('@opentelemetry/api');
const tracer = getTracer('my-app');
// Автоматический трейсинг HTTP и Express
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
registerInstrumentations({
tracerProvider: provider,
instrumentations: [
new ExpressInstrumentation(),
new HttpInstrumentation()
]
});
// Теперь каждый запрос будет создавать trace с spans
const express = require('express');
const app = express();
app.get('/api/order', async (req, res) => {
// Ручной span для бизнес-логики
const span = tracer.startSpan('processPayment');
try {
// ... ваша логика
await new Promise(r => setTimeout(r, 100)); // симуляция
span.setStatus({ code: 1 }); // OK
} catch (err) {
span.setStatus({ code: 2, message: err.message }); // ERROR
throw err;
} finally {
span.end();
}
res.send('Order created');
});
app.listen(3000);
Теперь в консоли вы увидите структурированные данные: какой запрос, сколько времени занял каждый middleware, где была ошибка. Это observability javascript в действии.
Метрики: пульс вашего приложения
Трейсинг хорош для отдельных запросов, но что насчёт общей картины? Сколько запросов в секунду? Какая задержка? Сколько памяти жрёт ваш Node.js? За это отвечают метрики.
Prometheus и клиент для Node.js
Prometheus — стандарт де-факто для сбора метрик. Он «вытягивает» данные из вашего приложения по HTTP.
Установка:
npm install prom-client express
Пример сбора базовых метрик:
const client = require('prom-client');
const express = require('express');
const app = express();
// Включаем сбор метрик по умолчанию (CPU, память и т.д.)
const collectDefaultMetrics = client.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });
// Создаём свою метрику: счётчик запросов
const httpRequestCounter = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'path', 'status']
});
// Гистограмма для времени ответа
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'path'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5] // в секундах
});
// Middleware для сбора метрик
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
httpRequestCounter.inc({ method: req.method, path: req.path, status: res.statusCode });
end({ method: req.method, path: req.path });
});
next();
});
// Эндпоинт для Prometheus
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
app.get('/api/orders', (req, res) => {
res.json({ orders: [] });
});
app.listen(3001, () => console.log('Metrics server running on port 3001'));
Теперь добавьте /metrics в конфигурацию Prometheus, и вы будете видеть графики в Grafana. Метрики приложения javascript — это ваш пульс.
Логирование: запись истории
Трейсинг и метрики отвечают на вопросы «где» и «сколько». Логи отвечают на вопрос «что именно произошло». Без них вы не сможете прочитать сообщение об ошибке.
Структурированное логирование с Winston
Забудьте про console.log. Используйте структурированные логи в JSON — их легко парсить и искать.
npm install winston
Настройка логгера с ротацией и транспортами:
const winston = require('winston');
const { combine, timestamp, json, errors } = winston.format;
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
errors({ stack: true }), // логирование стека ошибок
json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.File({ filename: 'error.log', level: 'error' })
]
});
// Пример использования
logger.info('User request processed', {
userId: 12345,
path: '/api/order',
duration: 150
});
try {
throw new Error('Database connection failed');
} catch (err) {
logger.error('Operation failed', {