TypeScript Деплой и наблюдаемость TypeScript-сервиса
0%

Деплой и наблюдаемость TypeScript-сервиса

Деплой и наблюдаемость TypeScript-сервиса

Код написан и протестирован. Теперь его нужно превратить в артефакт, доставить в прод и сделать так, чтобы в проде было видно, что происходит. Наблюдаемость — не роскошь: без логов, метрик и трейсинга инцидент превращается в гадание. Разберём путь от сборки до трёх столпов наблюдаемости.

Сборка артефакта

Напомним ключевое: типы стираются, в рантайм едет JavaScript. Продакшн-сборка = проверка типов (гейт) + быстрый эмит.

// package.json (фрагмент)
{
  "type": "module",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsup src/index.ts --format esm --sourcemap --clean",
    "start": "node dist/index.js"
  }
}

Для приложения (не библиотеки) часто бандлят весь код в один-два файла — это уменьшает размер, ускоряет старт и упрощает образ. Для библиотеки — эмитят с .d.ts и обоими форматами модулей (см. раздел о публикации). Всегда включайте source maps (--sourcemap): без них стектрейсы в проде будут указывать на номера строк в собранном/минифицированном JS, а не в вашем .ts, — отладка превращается в мучение.

Source maps и отладка

Source map — файл .js.map, сопоставляющий позиции в собранном JS с исходным .ts. С ним стектрейсы, дебаггер и профайлер показывают ваш настоящий код.

// включите поддержку source maps в рантайме, чтобы стектрейсы указывали на .ts
// начиная с Node 20 это встроено:  node --enable-source-maps dist/index.js

Отлаживать TypeScript можно прямо в исходниках: node --inspect открывает порт для Chrome DevTools / VS Code, а source maps связывают точки останова с .ts. Для локальной разработки удобен tsx (или node --watch --experimental-strip-types) — он исполняет .ts напрямую без отдельной сборки. Важно: решение о том, куда выкладывать source maps в проде — вопрос безопасности: публичные maps раскрывают исходники. Для бэкенда держите их приватными (или загружайте в систему трекинга ошибок вроде Sentry, а из образа удаляйте).

Контейнеризация: многоступенчатый Docker

Docker упаковывает приложение вместе с рантаймом в неизменяемый образ. Ключевой приём для TypeScript — многоступенчатая сборка (multi-stage build): собираем в одном слое с полными devDependencies, а в финальный образ кладём только рантайм и production-зависимости. Это кратно уменьшает образ и поверхность атаки.

# ---------- Стадия 1: сборка ----------
FROM node:22-slim AS build
WORKDIR /app
RUN corepack enable                         # активируем pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile          # все зависимости, включая dev
COPY . .
RUN pnpm typecheck && pnpm build            # гейт типов + сборка в dist/

# ---------- Стадия 2: только прод-зависимости ----------
FROM node:22-slim AS deps
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod   # БЕЗ devDependencies

# ---------- Стадия 3: финальный образ ----------
FROM node:22-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
USER node                                   # не root — принцип наименьших привилегий
COPY --from=deps  /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
EXPOSE 3000
CMD ["node", "--enable-source-maps", "dist/index.js"]

Практики продакшн-образов:

  • Base image. node:22-slim — разумный баланс. distroless (без shell и пакетного менеджера) ещё безопаснее. alpine меньше, но musl libc иногда даёт сюрпризы с нативными модулями.
  • .dockerignore. Исключите node_modules, .git, dist, тесты — не тащите мусор в контекст сборки.
  • Не root. Запускайте под непривилегированным пользователем (USER node).
  • Пиньте версии. И базовый образ, и зависимости (через lock-файл) — ради воспроизводимости.
  • Кеш слоёв. Копируйте package.json и ставьте зависимости до копирования исходников — тогда слой с node_modules переиспользуется, пока не изменились зависимости.

Три столпа наблюдаемости

  • Логи отвечают «что произошло» — дискретные события.
  • Метрики — «сколько и как быстро» — агрегированные числа во времени (RPS, латентность, ошибки, использование памяти).
  • Трейсы — «где именно потратилось время» — путь одного запроса через все сервисы и слои.

Логи: структурированные, через pino

Никогда не логируйте в прод через console.log со строками. В проде нужны структурированные логи (JSON): их можно фильтровать, искать и агрегировать. Стандарт для Node — pino: он очень быстрый (пишет JSON эффективно) и типизирован.

import { pino } from "pino";

export const logger = pino({
  level: config.LOG_LEVEL,
  // редактируем секреты, чтобы не утекли в логи
  redact: ["req.headers.authorization", "*.password", "*.token"],
});

// логируйте объектом (структурно), а не конкатенацией строк
logger.info({ userId: user.id, action: "login" }, "пользователь вошёл");
logger.error({ err, orderId }, "не удалось создать заказ"); // err сериализуется целиком

Ключевые практики логирования:

  • Уровни осмысленно. error — требует внимания человека; warn — подозрительно, но обработано; info — важные бизнес-события; debug — детали для разбора, выключены в проде.
  • Correlation ID. Присваивайте каждому запросу идентификатор и протаскивайте его во все логи запроса (pino + AsyncLocalStorage или интеграция с трейсингом). Без него нельзя собрать историю одного запроса из потока логов.
  • Не логируйте PII и секреты. Используйте redact.
  • JSON в проде, красивый вывод локально. pino-pretty — только для разработки.

Метрики: Prometheus

Метрики собирают через prom-client и отдают на эндпоинте /metrics, который скрейпит Prometheus, а визуализирует Grafana. Минимальный набор для сервиса — «золотые сигналы» (латентность, трафик, ошибки, насыщение):

import { Counter, Histogram, register } from "prom-client";

const httpDuration = new Histogram({
  name: "http_request_duration_seconds",
  help: "Длительность HTTP-запросов",
  labelNames: ["method", "route", "status"] as const,
});

// на каждый запрос замеряем длительность и метим по маршруту/статусу
app.addHook("onResponse", (req, reply, done) => {
  httpDuration.observe(
    { method: req.method, route: req.routeOptions.url ?? "unknown", status: reply.statusCode },
    reply.elapsedTime / 1000,
  );
  done();
});

app.get("/metrics", async (_req, reply) => {
  reply.header("Content-Type", register.contentType);
  return register.metrics();
});

Трейсинг: OpenTelemetry

OpenTelemetry (OTel) — вендор-нейтральный стандарт телеметрии. Его главная суперсила — автоматическая инструментация: подключив SDK, вы получаете трейсы HTTP, БД, gRPC без ручной разметки. Трейс показывает путь запроса как дерево спанов (span — отрезок работы) со временем каждого шага — это незаменимо для распределённых систем и поиска «где тормозит».

// tracing.ts — подключается ПЕРВЫМ, до импорта приложения
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

const sdk = new NodeSDK({
  serviceName: "orders-api",
  traceExporter: new OTLPTraceExporter(), // шлёт в OTel Collector -> Jaeger/Tempo
  instrumentations: [getNodeAutoInstrumentations()], // авто-трейсинг http, pg, fetch...
});

sdk.start();

// корректно завершаем экспорт при остановке
process.on("SIGTERM", () => void sdk.shutdown());
# запуск с предзагрузкой инструментации до кода приложения
node --enable-source-maps --import ./dist/tracing.js dist/index.js

Свяжите три столпа: прокидывайте trace_id в структурные логи (pino) — тогда из метрики-аномалии можно перейти к трейсу медленного запроса, а из спана — к его логам. Это и есть настоящая наблюдаемость.

Производительность

  • Профилируйте, не гадайте. node --prof, --cpu-prof, инспектор в Chrome DevTools, clinic.js. Оптимизируйте по данным.
  • Следите за event loop lag. Главный признак проблем в Node — задержка event loop (см. файл про конкурентность). perf_hooks.monitorEventLoopDelay или метрика из prom-client. Растёт lag — где-то блокирующий синхронный код.
  • Кешируйте разумно. In-memory (LRU) для горячих данных, Redis для общего кеша. Всегда думайте про инвалидацию и TTL.
  • Пулы соединений. К БД и внешним сервисам — через пул, а не соединение на запрос.
  • Не оптимизируйте вслепую. Сначала корректность и наблюдаемость, потом — измеренные узкие места.

Публикация пакета с типами (для библиотек)

Если вы публикуете библиотеку, потребители должны получить типы. Настройте package.json так, чтобы отдать и код, и .d.ts, и оба формата модулей:

{
  "name": "@acme/utils",
  "version": "1.2.0",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "sideEffects": false
}

Проверьте корректность экспортов и типов инструментом publint и @arethetypeswrong/cli перед публикацией — они ловят типичные ошибки конфигурации ESM/CJS/типов. Версионируйте по semver, автоматизируйте релиз через changesets.

Источники

Что дальше

Соберём всё в целостный взгляд: как TypeScript живёт в SDLC — от требований до эксплуатации, — плюс кураторский список лучших ресурсов для дальнейшего роста.

SDLC и лучшие источники

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов