Трассировка вызовов

Трассировка вызовов в контексте Validator.js — это процесс отслеживания последовательности выполнения проверок, преобразований и пользовательских валидаторов. Такая техника используется для:

  • поиска ошибок в логике валидации;
  • анализа цепочек вызовов;
  • диагностики некорректных данных;
  • профилирования производительности;
  • построения систем аудита и логирования;
  • отладки middleware и API.

При использовании Validator.js трассировка особенно важна в крупных backend-проектах, где множество валидаторов вызываются каскадно через:

  • HTTP middleware;
  • ORM-модели;
  • DTO-объекты;
  • формы;
  • сервисы сериализации;
  • GraphQL-резолверы.

Архитектура вызовов Validator.js

Библиотека Validator.js предоставляет набор чистых функций:

validator.isEmail(value)
validator.isURL(value)
validator.isUUID(value)

Каждая функция:

  1. принимает входное значение;
  2. нормализует тип;
  3. выполняет внутренние проверки;
  4. возвращает true или false.

Простейшая цепочка вызова:

const validator = require('validator')

const result = validator.isEmail('admin@example.com')

console.log(result)

Трассировка здесь выглядит так:

main()
 └── validator.isEmail()
      ├── assertString()
      ├── normalizeEmail()
      └── regexp.test()

Перехват вызовов валидаторов

Обёртка над функциями

Самый распространённый способ трассировки — создание proxy-обёртки.

Пример:

const validator = require('validator')

function trace(fn, name) {
    return function (...args) {
        console.log(`[TRACE] ${name}`)
        console.log(`[ARGS]`, args)

        const result = fn(...args)

        console.log(`[RESULT]`, result)

        return result
    }
}

const tracedIsEmail = trace(
    validator.isEmail,
    'validator.isEmail'
)

tracedIsEmail('admin@example.com')

Результат:

[TRACE] validator.isEmail
[ARGS] [ 'admin@example.com' ]
[RESULT] true

Глобальная трассировка Validator.js

Автоматическое оборачивание всех методов

В больших приложениях вручную трассировать каждую функцию неудобно.

Подход с динамическим обходом:

const validator = require('validator')

const tracedValidator = {}

for (const key in validator) {
    if (typeof validator[key] === 'function') {

        tracedValidator[key] = (...args) => {
            console.log(`[CALL] ${key}`)
            console.log(`[INPUT]`, args)

            const result = validator[key](...args)

            console.log(`[OUTPUT]`, result)

            return result
        }

    } else {
        tracedValidator[key] = validator[key]
    }
}

Использование:

tracedValidator.isEmail('root@example.com')
tracedValidator.isUUID('123')

Трассировка через Proxy

Использование Proxy API

Proxy позволяет динамически перехватывать обращения к объекту.

Пример:

const validator = require('validator')

const tracedValidator = new Proxy(validator, {
    get(target, prop) {

        const original = target[prop]

        if (typeof original !== 'function') {
            return original
        }

        return (...args) => {
            console.log(`[TRACE] ${prop}`)
            console.log(`[ARGS]`, args)

            const result = original(...args)

            console.log(`[RETURN]`, result)

            return result
        }
    }
})

Использование:

tracedValidator.isURL('https://google.com')

Преимущества:

  • отсутствие ручного перечисления методов;
  • автоматическая поддержка новых функций;
  • единая точка трассировки;
  • совместимость с middleware.

Стек вызовов

Получение call stack

Иногда требуется узнать, откуда был вызван валидатор.

Node.js позволяет получить стек:

function getStack() {
    const err = new Error()

    return err.stack
}

Интеграция:

const validator = require('validator')

function traced(fn, name) {

    return (...args) => {

        console.log(`[CALL] ${name}`)

        console.log(getStack())

        return fn(...args)
    }
}

Результат:

Error
    at getStack
    at traced
    at validateUser
    at controller

Трассировка пользовательских валидаторов

Кастомные функции

Часто поверх Validator.js создаются собственные проверки.

Пример:

const validator = require('validator')

function validateUserEmail(email) {

    console.log('[VALIDATE USER EMAIL]')

    if (!validator.isEmail(email)) {
        throw new Error('Invalid email')
    }

    return true
}

Трассировка:

validateUserEmail()
 └── validator.isEmail()

Построение дерева вызовов

Вложенные валидаторы

Реальные системы используют каскадную валидацию.

Пример:

function validatePassword(password) {

    if (!validator.isLength(password, { min: 8 })) {
        return false
    }

    if (!validator.matches(password, /[A-Z]/)) {
        return false
    }

    if (!validator.matches(password, /\d/)) {
        return false
    }

    return true
}

Трассировка:

validatePassword()
 ├── validator.isLength()
 ├── validator.matches()
 └── validator.matches()

Автоматическое логирование

Использование middleware

В Express можно централизовать трассировку.

Пример:

function validationLogger(req, res, next) {

    console.log(`[REQUEST] ${req.method} ${req.url}`)

    next()
}

Интеграция:

app.use(validationLogger)

Логирование ошибок валидации

Перехват исключений

Некоторые пользовательские валидаторы выбрасывают ошибки.

Пример:

function validateUUID(id) {

    if (!validator.isUUID(id)) {
        throw new Error('Invalid UUID')
    }

    return true
}

Трассировка:

try {
    validateUUID('123')
} catch (err) {

    console.error('[VALIDATION ERROR]')
    console.error(err.message)
}

Асинхронная трассировка

Проверки в async-функциях

Validator.js синхронен, однако пользовательские валидаторы часто асинхронны.

Пример:

async function validateUser(data) {

    console.log('[START VALIDATION]')

    const emailValid = validator.isEmail(data.email)

    console.log('[EMAIL]', emailValid)

    const exists = await database.findUser(data.email)

    console.log('[USER EXISTS]', exists)

    return !exists
}

Performance tracing

Измерение времени выполнения

Для оценки производительности используется performance.now().

Пример:

const { performance } = require('perf_hooks')

function tracePerformance(fn, name) {

    return (...args) => {

        const start = performance.now()

        const result = fn(...args)

        const end = performance.now()

        console.log(`${name}: ${end - start}ms`)

        return result
    }
}

Использование:

const tracedEmail = tracePerformance(
    validator.isEmail,
    'isEmail'
)

tracedEmail('admin@example.com')

Глубокая трассировка параметров

Анализ входящих данных

Полезно логировать не только значения, но и типы.

Пример:

function debugArgs(args) {

    args.forEach((arg, index) => {

        console.log({
            index,
            value: arg,
            type: typeof arg
        })

    })
}

Интеграция:

function traced(fn, name) {

    return (...args) => {

        console.log(name)

        debugArgs(args)

        return fn(...args)
    }
}

Трассировка нормализации данных

Validator.js Sanitizers

Библиотека содержит санитайзеры:

validator.trim()
validator.escape()
validator.normalizeEmail()

Их также полезно трассировать.

Пример:

function traceSanitizer(fn, name) {

    return (value) => {

        console.log(`[BEFORE] ${value}`)

        const result = fn(value)

        console.log(`[AFTER] ${result}`)

        return result
    }
}

Диагностика цепочек преобразования

Последовательная обработка

Пример:

const email = validator.normalizeEmail(
    validator.trim(input)
)

Трассировка:

trim()
 └── normalizeEmail()

Расширенный вариант:

function pipeline(value, ...fns) {

    return fns.reduce((acc, fn) => {

        console.log(`[PIPE] ${fn.name}`)
        console.log(`[VALUE]`, acc)

        return fn(acc)

    }, value)
}

Использование:

pipeline(
    ' ADMIN@EXAMPLE.COM ',
    validator.trim,
    validator.normalizeEmail
)

Интеграция с Winston

Централизованное логирование

В production-среде вместо console.log используют логгеры.

Пример:

const winston = require('winston')

const logger = winston.createLogger({
    transports: [
        new winston.transports.Console()
    ]
})

Трассировка:

logger.info('Validation started')

Интеграция с Pino

Высокопроизводительное логирование

Пример:

const pino = require('pino')

const logger = pino()

Использование:

logger.info({
    validator: 'isEmail',
    value: 'admin@example.com'
})

Trace ID

Идентификаторы запросов

При обработке множества запросов важно связывать логи.

Пример:

const traceId = crypto.randomUUID()

Использование:

logger.info({
    traceId,
    validator: 'isEmail'
})

Трассировка в Express Validator

Связь с Validator.js

express-validator использует Validator.js внутри себя.

Пример:

body('email')
    .isEmail()

Трассировка middleware:

app.use((req, res, next) => {

    console.log(`[BODY]`, req.body)

    next()
})

Monkey patching

Переопределение методов

Иногда используется динамическая замена функций.

Пример:

const original = validator.isEmail

validator.isEmail = function (...args) {

    console.log('[PATCHED isEmail]')

    return original(...args)
}

Недостатки:

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

Debug mode

Режим отладки

Полезно включать трассировку только в development-режиме.

Пример:

const DEBUG = process.env.DEBUG === 'true'

Использование:

if (DEBUG) {
    console.log('[TRACE]', args)
}

Уровни трассировки

Разделение уровней логов

Типичная схема:

Уровень Назначение
error ошибки
warn подозрительные данные
info обычные проверки
debug подробная трассировка
trace полный стек вызовов

Пример:

logger.debug('Validation started')

Трассировка больших объектов

Безопасная сериализация

Прямая сериализация может приводить к ошибкам циклических ссылок.

Небезопасный код:

JSON.stringify(obj)

Безопасный вариант:

const util = require('util')

console.log(
    util.inspect(obj, {
        depth: 5,
        colors: true
    })
)

Трассировка схем валидации

Централизованные схемы

Пример:

const schema = {
    email: validator.isEmail,
    uuid: validator.isUUID
}

Трассировка:

for (const field in schema) {

    console.log(`[FIELD] ${field}`)

    const valid = schema[field](data[field])

    console.log(`[VALID] ${valid}`)
}

Инструменты Node.js для трассировки

console.trace

Встроенная функция:

console.trace('Validation trace')

Результат:

Trace: Validation trace
    at validateUser
    at controller

Error.captureStackTrace

Управление стеком

Пример:

const obj = {}

Error.captureStackTrace(obj)

console.log(obj.stack)

OpenTelemetry

Production tracing

В распределённых системах применяется OpenTelemetry.

Пример концепции:

const span = tracer.startSpan('validator.isEmail')

Завершение:

span.end()

Такая интеграция позволяет:

  • строить distributed tracing;
  • анализировать latency;
  • отслеживать bottleneck;
  • связывать API и БД.

Типичные проблемы трассировки

Избыточное логирование

Чрезмерное количество логов:

  • замедляет приложение;
  • увеличивает размер файлов;
  • усложняет анализ.

Утечка чувствительных данных

Опасно логировать:

  • пароли;
  • токены;
  • номера карт;
  • cookies;
  • JWT.

Небезопасно:

console.log(password)

Безопаснее:

console.log('[PASSWORD REDACTED]')

Маскирование данных

Redaction

Пример:

function maskEmail(email) {

    return email.replace(/(.{2}).+(@.+)/, '$1***$2')
}

Результат:

ad***@example.com

Практический пример полной трассировки

Комплексная система

const validator = require('validator')

function trace(fn, name) {

    return (...args) => {

        console.log('================')
        console.log(`[CALL] ${name}`)
        console.log(`[ARGS]`, args)

        const start = performance.now()

        try {

            const result = fn(...args)

            const end = performance.now()

            console.log(`[RESULT]`, result)
            console.log(`[TIME] ${end - start}ms`)

            return result

        } catch (err) {

            console.error(`[ERROR]`, err.message)

            throw err
        }
    }
}

const isEmail = trace(
    validator.isEmail,
    'validator.isEmail'
)

const isUUID = trace(
    validator.isUUID,
    'validator.isUUID'
)

isEmail('admin@example.com')
isUUID('123')

Лучшие практики

Централизация трассировки

Предпочтительно иметь:

  • единый logger;
  • общий middleware;
  • централизованный trace manager.

Изоляция debug-кода

Трассировка не должна смешиваться с бизнес-логикой.

Плохой подход:

function validate(data) {

    console.log(data)

    return validator.isEmail(data.email)
}

Лучше:

const validate = tracedValidator.isEmail

Использование структурированных логов

Лучше логировать JSON-структуры:

logger.info({
    validator: 'isEmail',
    success: true
})

Это упрощает:

  • фильтрацию;
  • индексацию;
  • поиск;
  • агрегацию;
  • интеграцию с ELK и Grafana.

Отладка сложных цепочек

Каскадные схемы

Пример:

function validateUser(user) {

    return (
        validator.isEmail(user.email) &&
        validator.isUUID(user.id) &&
        validator.isLength(user.password, {
            min: 8
        })
    )
}

Трассировка позволяет быстро определить:

  • какая проверка упала;
  • какие данные вызвали ошибку;
  • сколько времени заняла операция;
  • где возникло исключение;
  • какая цепочка вызовов была выполнена.