Observability в TypeScript: от чёрного ящика к прозрачной системе
Представьте, что ваше TypeScript-приложение — это сложный механизм из тысяч шестерёнок. Всё работает идеально, пока не ломается. И вот вы получаете сообщение: «Сайт тормозит». Где искать проблему? База данных? API? Или просто упал один из микросервисов?
Без observability (наблюдаемости) вы будете гадать, как шаман с бубном. С observability — вы видите всю картину: где упала производительность, какой запрос вызвал ошибку и почему утекла память.
В этой статье мы разберём три столпа observability — трейсинг, метрики и логи — и научимся внедрять их в TypeScript-приложения на реальных примерах. Вы узнаете, как превратить своё приложение из чёрного ящика в прозрачную систему, которую можно диагностировать за минуты.
Почему observability критична для TypeScript-приложений?
TypeScript — это не просто надстройка над JavaScript. Это язык, на котором пишут бэкенд для высоконагруженных систем, микросервисы и даже CLI-инструменты. И чем сложнее система, тем важнее понимать, что в ней происходит.
Традиционное логирование — это хорошо, но недостаточно. Когда у вас 50 микросервисов, логи превращаются в белый шум. Вам нужно:
- Трейсинг — чтобы отследить путь одного запроса через все сервисы.
- Метрики — чтобы увидеть, что RAM растёт как на дрожжах.
- Структурированные логи — чтобы не искать иголку в стоге сена.
Давайте внедрим это в реальное приложение.
Трейсинг в TypeScript: OpenTelemetry на практике
Трейсинг (трассировка) — это история жизни одного запроса. Вы видите, сколько времени занял каждый шаг: от HTTP-запроса до запроса в базу данных. В TypeScript стандартом де-факто стал OpenTelemetry.
Установка и настройка OpenTelemetry
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-jaeger
Создадим файл tracing.ts для инициализации трейсинга:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const jaegerExporter = new JaegerExporter({
endpoint: 'http://localhost:14268/api/traces',
serviceName: 'my-typescript-app',
});
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'my-typescript-app',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
traceExporter: jaegerExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Теперь каждый HTTP-запрос, каждый вызов базы данных будет автоматически трейситься. Вы увидите в Jaeger UI (или Grafana Tempo) полную карту вызовов.
Ручное создание спанов
Автоинструментирование — это круто, но иногда нужно добавить свои метки. Например, измерить время выполнения сложной бизнес-логики:
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('business-logic');
async function processOrder(orderId: string): Promise<void> {
const span = tracer.startSpan('process-order', {
attributes: { 'order.id': orderId },
});
try {
// Ваша логика обработки заказа
await validateOrder(orderId);
await chargePayment(orderId);
await sendConfirmation(orderId);
span.setStatus({ code: 1 }); // OK
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: 2, message: 'Order processing failed' }); // ERROR
throw error;
} finally {
span.end();
}
}
Совет: добавляйте атрибуты — ID пользователя, тип ошибки, версию API. Это сделает трейсинг по-настоящему полезным.
Метрики в TypeScript: Prometheus и клиент для Node.js
Метрики — это цифры здоровья вашего приложения: количество запросов в секунду, latency, использование памяти. Без метрик вы не узнаете, что приложение «умирает», пока не станет слишком поздно.
Для TypeScript отлично подходит prom-client — клиент Prometheus для Node.js.
Установка и базовые метрики
npm install prom-client express
Создадим файл metrics.ts:
import client from 'prom-client';
import express, { Request, Response } from 'express';
// Создаём реестр метрик
const register = new client.Registry();
// Добавляем стандартные метрики Node.js
client.collectDefaultMetrics({ register });
// Кастомная метрика: счётчик запросов
const httpRequestCounter = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status'],
registers: [register],
});
// Кастомная метрика: гистограмма времени ответа
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
registers: [register],
});
// Экспортируем эндпоинт для Prometheus
const app = express();
app.get('/metrics', async (_req: Request, res: Response) => {
res.set('Content-Type', register.contentType);
res.send(await register.metrics());
});
export { app, httpRequestCounter, httpRequestDuration, register };
Использование метрик в приложении
Теперь добавим middleware, который будет собирать метрики с каждого запроса:
import { Request, Response, NextFunction } from 'express';
import { httpRequestCounter, httpRequestDuration } from './metrics';
export function metricsMiddleware(req: Request, res: Response, next: NextFunction): void {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
// Увеличиваем счётчик
httpRequestCounter.inc({
method: req.method,
route: req.route?.path || req.path,
status: res.statusCode.toString(),
});
// Записываем длительность
end({
method: req.method,
route: req.route?.path || req.path,
});
});
next();
}
Теперь в Prometheus вы увидите графики: сколько запросов упало с 500-й ошибкой, какие эндпоинты самые медленные. А если подключить Grafana — получите красивый дашборд.
Логирование в TypeScript: Winston и структурированные логи
Логи — это дневник вашего приложения. Но не просто «Ошибка!», а структурированные записи с контекстом.