Интеграция с системами логирования

Zod выступает как слой строгой валидации данных в рантайме и особенно эффективно проявляет себя в задачах унификации и контроля структур логов, где важны предсказуемость схемы, совместимость между сервисами и отсутствие «грязных» полей.

Zod позволяет описывать контракт логируемых событий как формальную схему и проверять соответствие структуры непосредственно в момент формирования лог-записи или перед её отправкой в систему агрегации.


Логирование в современных приложениях перестало быть набором строк и превратилось в структурированный поток событий. Каждое событие логирования представляет собой объект с фиксированными полями:

  • уровень (info, warn, error)
  • временная метка
  • идентификатор запроса
  • сообщение
  • контекст (payload)
  • технические метаданные

Отсутствие строгой схемы приводит к проблемам:

  • разнородные поля между сервисами
  • невозможность корректной агрегации в системах вроде ELK или Loki
  • ошибки сериализации
  • потеря контекста при трассировке

Zod вводит формальный контракт, который делает логирование детерминированным процессом.


Описание базовой схемы лог-события

Типичная схема лог-записи строится как объект с фиксированными полями и строгими типами:

import { z } from "zod";

const LogLevelSchema = z.enum(["debug", "info", "warn", "error"]);

const LogSchema = z.object({
  timestamp: z.string().datetime(),
  level: LogLevelSchema,
  message: z.string(),
  requestId: z.string().uuid().optional(),
  service: z.string(),
  context: z.record(z.unknown()).optional()
});

Такая модель задаёт единый формат логов для всего приложения.

Ключевой эффект — исключение ситуаций, когда разные части системы пишут несовместимые структуры.


Валидация логов на этапе создания

Одним из практических паттернов является валидация лог-записи до её отправки в транспорт:

function createLog(input: unknown) {
  const parsed = LogSchema.parse(input);
  return parsed;
}

При нарушении схемы происходит немедленное выявление ошибки, что предотвращает попадание некорректных данных в лог-систему.

В более мягком режиме используется safeParse:

function tryCreateLog(input: unknown) {
  const result = LogSchema.safeParse(input);

  if (!result.success) {
    return {
      level: "error",
      message: "Invalid log structure",
      context: result.error.flatten()
    };
  }

  return result.data;
}

Структурирование логов для распределённых систем

В распределённых архитектурах лог-сообщение должно включать дополнительные поля:

  • traceId
  • spanId
  • serviceName
  • environment

Расширение схемы через композицию Zod позволяет избежать дублирования:

const BaseLogSchema = z.object({
  timestamp: z.string().datetime(),
  level: LogLevelSchema,
  message: z.string()
});

const TracingSchema = z.object({
  traceId: z.string(),
  spanId: z.string().optional(),
  service: z.string(),
  environment: z.string()
});

const FullLogSchema = BaseLogSchema.merge(TracingSchema).extend({
  context: z.record(z.unknown()).optional()
});

Такой подход формирует единый стандарт логов между микросервисами.


Интеграция с системами логирования

Winston

При использовании Winston Zod часто применяется как фильтр перед транспортом:

import winston from "winston";

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console({
      format: winston.format.printf((info) => {
        const parsed = FullLogSchema.safeParse(info);
        if (!parsed.success) return "INVALID_LOG";
        return JSON.stringify(parsed.data);
      })
    })
  ]
});

Zod выступает как слой санитарной проверки перед сериализацией.


Pino

Pino изначально ориентирован на структурированные JSON-логи, поэтому Zod используется для нормализации входящих данных:

import pino from "pino";

const logger = pino({
  base: null,
  timestamp: pino.stdTimeFunctions.isoTime,
  hooks: {
    logMethod(args, method) {
      const candidate = args[0];
      const result = FullLogSchema.safeParse(candidate);

      if (!result.success) {
        method.call(this, { level: "error", message: "invalid log" });
        return;
      }

      method.apply(this, [result.data]);
    }
  }
});

Нормализация произвольных данных

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

const ExternalPayloadSchema = z.object({
  status: z.number(),
  data: z.unknown(),
  headers: z.record(z.string()).optional()
});

После нормализации лог становится предсказуемым независимо от источника данных.


Обработка ошибок через схемы

Ошибки являются важной частью логирования и требуют отдельного подхода.

const ErrorSchema = z.object({
  name: z.string(),
  message: z.string(),
  stack: z.string().optional()
});

Интеграция с логами:

const ErrorLogSchema = FullLogSchema.extend({
  error: ErrorSchema.optional()
});

Это позволяет унифицировать формат ошибок между синхронными и асинхронными процессами.


Контекстные поля и трассировка

Контекстные поля используются для связывания логов между собой:

const ContextSchema = z.object({
  requestId: z.string().uuid(),
  userId: z.string().optional(),
  sessionId: z.string().optional()
});

Композиция с основной схемой логов позволяет формировать трассируемые цепочки событий.


Версионирование лог-схем

Системы логирования со временем эволюционируют, и структура логов изменяется. Zod позволяет явно управлять версиями:

const LogV1 = z.object({
  message: z.string(),
  level: LogLevelSchema
});

const LogV2 = LogV1.extend({
  timestamp: z.string().datetime(),
  service: z.string()
});

Далее возможна маршрутизация по версии:

const LogSchema = z.discriminatedUnion("version", [
  z.object({ version: z.literal("v1"), data: LogV1 }),
  z.object({ version: z.literal("v2"), data: LogV2 })
]);

Производительность и оптимизация

Использование Zod в логировании требует учёта нагрузки:

  • минимизация количества parse в горячих путях
  • использование safeParse вместо исключений
  • предкомпиляция схем
  • кеширование валидированных структур

В высоконагруженных системах Zod чаще применяется на границах системы (ingress/egress), а не внутри каждого лог-сообщения.


Паттерн логирующего middleware

В архитектурах с промежуточными слоями Zod используется как middleware:

function logMiddleware(schema: z.ZodSchema) {
  return (data: unknown) => {
    const result = schema.safeParse(data);
    if (result.success) {
      return result.data;
    }
    return null;
  };
}

Такой подход централизует контроль формата логов.


Стандартизация логов между сервисами

При микросервисной архитектуре Zod используется для описания контракта логов как API:

  • единые схемы
  • общие пакеты типов
  • синхронизация изменений через CI

Это снижает вероятность расхождений между сервисами, пишущими в общую систему агрегации.


Обогащение логов

Zod-схемы часто используются не только для проверки, но и для расширения данных:

const EnrichedLogSchema = FullLogSchema.transform((log) => ({
  ...log,
  hostname: process.env.HOSTNAME
}));

Так формируется единый слой enrichment перед отправкой в storage.


Динамические схемы и конфигурации

В некоторых системах лог-схема зависит от окружения:

function createSchema(env: string) {
  return env === "production"
    ? FullLogSchema
    : FullLogSchema.extend({
        debugInfo: z.any().optional()
      });
}

Это позволяет варьировать детализацию логов без изменения бизнес-кода.