Zod выступает как слой строгой валидации данных в рантайме и особенно эффективно проявляет себя в задачах унификации и контроля структур логов, где важны предсказуемость схемы, совместимость между сервисами и отсутствие «грязных» полей.
Zod позволяет описывать контракт логируемых событий как формальную схему и проверять соответствие структуры непосредственно в момент формирования лог-записи или перед её отправкой в систему агрегации.
Логирование в современных приложениях перестало быть набором строк и превратилось в структурированный поток событий. Каждое событие логирования представляет собой объект с фиксированными полями:
Отсутствие строгой схемы приводит к проблемам:
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;
}
В распределённых архитектурах лог-сообщение должно включать дополнительные поля:
Расширение схемы через композицию 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 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 изначально ориентирован на структурированные 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), а не внутри каждого лог-сообщения.
В архитектурах с промежуточными слоями 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:
Это снижает вероятность расхождений между сервисами, пишущими в общую систему агрегации.
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()
});
}
Это позволяет варьировать детализацию логов без изменения бизнес-кода.