浏览代码

Улучшено логирование

Vadim 3 月之前
父节点
当前提交
5cb845b06a
共有 2 个文件被更改,包括 168 次插入31 次删除
  1. 68 12
      src/middlewares/error-middleware.ts
  2. 100 19
      src/plugins/logger.ts

+ 68 - 12
src/middlewares/error-middleware.ts

@@ -1,36 +1,92 @@
+import { Request, Response, NextFunction, ErrorRequestHandler } from "express";
 import { SchemaValidationError, SlonikError } from "slonik";
 import { ApiError } from "../exceptions/api-error.js";
 import { logger } from "../plugins/logger.js";
 import { ZodError } from "zod";
+import { UnexpectedError } from "#exceptions/unexpected-errors.js";
 
-export default function (err, req, res, next) {
+const globalErrorHandler: ErrorRequestHandler = (
+  err: unknown, // Тип ошибки здесь может быть широким, далее идут проверки instanceof
+  req: Request,
+  res: Response,
+  next: NextFunction,
+) => {
   if (!err) next();
 
+  // Формируем объект с деталями запроса для логов
+  const requestDetails = {
+    method: req.method,
+    url: req.originalUrl,
+    ip: req.ip,
+    body: req.body, // Осторожно: может содержать чувствительные данные, логируйте по необходимости
+    query: req.query,
+  };
+
   if (err instanceof ApiError) {
-    return res
+    logger.warn({
+      message: `API Error: ${err.message}`,
+      err: err,
+      request: requestDetails,
+    });
+    res.status(err.status).json({ message: err.message, errors: err.errors });
+    return;
+  }
+
+  if (err instanceof UnexpectedError) {
+    logger.error({
+      message: `Unexpected Error: ${err.message}`,
+      err: err,
+      request: requestDetails,
+    });
+    res
       .status(err.status)
-      .json({ message: err.message, err: err.errors });
+      .json({ message: "Внутренняя ошибка сервера (схема БД)" });
+    return;
   }
 
   if (err instanceof ZodError) {
-    logger.error({ message: "Ошибка валидации ZOD", err: err });
-    return res.status(400).json({ message: "Ошибка валидации ZOD", err: err });
+    logger.error({
+      message: "Ошибка валидации ZOD на сервере",
+      err: err,
+      request: requestDetails,
+    });
+    // TODO: Для клиента лучше отправлять обработанные ошибки, а не весь объект ZodError
+    res.status(400).json({
+      message: "Ошибка валидации ZOD",
+      errors: err.flatten().fieldErrors,
+    });
+    return;
   }
 
   if (err instanceof SchemaValidationError) {
     logger.error({
-      message: "Ошибка несоотвествия схемы БД",
+      message: "Ошибка несоответствия схемы БД (Slonik SchemaValidationError)",
       err: err,
+      request: requestDetails,
     });
-    return res.status(500).json({ message: "Ошибка несоотвествия схемы БД" });
+    res.status(500).json({ message: "Внутренняя ошибка сервера (схема БД)" });
+    return;
   }
 
   if (err instanceof SlonikError) {
-    logger.error({ message: "Ошибка запроса БД", err: err });
-    return res.status(500).json({ message: "Ошибка запроса БД" });
+    logger.error({
+      message: "Ошибка запроса БД (SlonikError)",
+      err: err,
+      request: requestDetails,
+    });
+    res.status(500).json({ message: "Внутренняя ошибка сервера (запрос БД)" });
+    return;
   }
 
-  logger.error({ message: "Непредвиденная ошибка", err: err });
+  // Непредвиденная ошибка
+  logger.error({
+    message: "Непредвиденная ошибка сервера",
+    err: err,
+    request: requestDetails,
+  });
+  res.status(500).json({
+    message: "Произошла непредвиденная ошибка. Пожалуйста, попробуйте позже.",
+  });
+};
 
-  return res.status(500).json({ message: "Непредвиденная ошибка", err: err });
-}
+export default globalErrorHandler;

+ 100 - 19
src/plugins/logger.ts

@@ -1,38 +1,120 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
 import { createLogger, format, transports } from "winston";
 import path from "path";
-import "../plugins/dotenv.js";
+import "../plugins/dotenv.js"; // Убедись, что этот путь корректен
+import { ZodError } from "zod"; // Импортируем для instanceof
+import { ApiError } from "../exceptions/api-error.js"; // Импортируем для instanceof
 
 const __dirname = path.resolve();
 
+// Функция для сериализации ошибок в простой объект
+const serializeError = (error: any): Record<string, any> => {
+  if (!(error instanceof Error)) {
+    // Если это не ошибка, но объект, вернем как есть для JSON.stringify
+    if (typeof error === "object" && error !== null) {
+      return error;
+    }
+    // Для примитивов или null
+    return { value: String(error) };
+  }
+
+  const plainError: Record<string, any> = {
+    name: error.name,
+    message: error.message,
+    stack: error.stack,
+    // Дополнительные свойства для конкретных типов ошибок
+  };
+
+  // Копируем все собственные свойства ошибки, включая неперечислимые
+  Object.getOwnPropertyNames(error).forEach((key) => {
+    if (!plainError.hasOwnProperty(key)) {
+      // Не перезаписывать уже установленные (name, message, stack)
+      plainError[key] = (error as any)[key];
+    }
+  });
+
+  // Специальная обработка для ZodError
+  if (error instanceof ZodError) {
+    plainError.issues = error.issues;
+  }
+  // Специальная обработка для ApiError
+  if (error instanceof ApiError) {
+    plainError.status = error.status;
+    if (error.errors) {
+      // 'errors' - это массив в твоем ApiError
+      plainError.originalErrors = error.errors; // Переименуем, чтобы не конфликтовать с plainError.errors
+    }
+  }
+  // Добавь здесь обработку других специфичных типов ошибок, если нужно (SlonikError, SchemaValidationError)
+
+  return plainError;
+};
+
 // Функция для форматирования вложенных объектов
 const formatNestedObjects = (obj: unknown): string => {
   if (typeof obj !== "object" || obj === null) {
     return String(obj);
   }
 
-  // Преобразуем объект в форматированный JSON и заменяем \n на реальные переносы
-  return JSON.stringify(obj, null, 2).replace(/\\n/g, "\n");
+  // Replacer для JSON.stringify, который корректно обработает ошибки
+  const replacer = (key: string, value: unknown) => {
+    if (value instanceof Error) {
+      return serializeError(value);
+    }
+    // JSON.stringify не умеет работать с BigInt по умолчанию
+    if (typeof value === "bigint") {
+      return value.toString() + "n"; // или просто value.toString()
+    }
+    return value;
+  };
+
+  try {
+    // Преобразуем объект в форматированный JSON и заменяем \n на реальные переносы
+    return JSON.stringify(obj, replacer, 2).replace(/\\n/g, "\n");
+  } catch (e) {
+    // В случае ошибки сериализации (например, циклические ссылки, хотя replacer должен помочь с Error)
+    return `[Не удалось сериализовать объект: ${e instanceof Error ? e.message : String(e)}]`;
+  }
 };
 
 // Форматирование логов
 const customFormat = format.combine(
-  format.timestamp({ format: "DD.MM.YYYY HH:mm:ss" }), // Добавление временной метки
-  format.errors({ stack: true }), // Автоматическое добавление стека для ошибок
+  format.timestamp({ format: "DD.MM.YYYY HH:mm:ss" }),
+  format.errors({ stack: true }), // Автоматическое добавление стека для ошибок (если ошибка передана как info или info.message)
+  format.splat(), // Необходимо для работы с метаданными типа logger.info('message', { meta: 'data' })
   format.printf((info) => {
     const { timestamp, level, message, stack, ...rest } = info;
 
-    // Приведение stack к строке или undefined
-    const stackString = typeof stack === "string" ? stack : undefined;
+    // `stack` здесь будет от `format.errors`, если он смог обработать ошибку на верхнем уровне.
+    // Однако, в твоем случае ошибка находится в `rest.err`.
+    // Наша улучшенная `formatNestedObjects` теперь должна корректно сериализовать `rest.err`.
 
-    //  Форматирование вложенных объектов
     const additionalData = Object.keys(rest).length
-      ? `\nAdditional Info:\n${formatNestedObjects(rest)}`
+      ? `\nAdditional Info:\n${formatNestedObjects(rest)}` // Это теперь должно работать правильно
       : "";
 
-    // Извлечение информации о файле и строке из стека
-    const stackInfo = stackString ? stackString.split("\n")[1]?.trim() : "";
+    // Стек из `rest.err` будет уже внутри `additionalData`.
+    // Если `stack` (от format.errors) существует и отличается, можно его тоже добавить,
+    // но обычно стек из `rest.err` будет более полным в твоем случае.
+    // Пока что оставим так, стек будет внутри `Additional Info` для `err`.
+
+    let stackLog = "";
+    // Если `format.errors` извлек стек на верхний уровень (маловероятно с твоей структурой лог-сообщения)
+    if (typeof stack === "string") {
+      stackLog = `\nStack (from info.stack):\n${stack}`;
+    }
+    // Если есть объект ошибки в rest.err и у него есть стек (это более вероятный сценарий),
+    // он будет отформатирован через formatNestedObjects.
+    // Чтобы избежать дублирования, можно не выводить `stackLog` отдельно,
+    // если основной стек уже в `additionalData.err.stack`.
+    // Для простоты, пока оставим `additionalData` как есть.
+
+    // Извлечение информации о файле и строке из стека (если есть основной stack)
+    // Это может быть не так полезно, если главный стек находится внутри `rest.err`
+    const firstStackLineInfo =
+      typeof stack === "string" ? stack.split("\n")[1]?.trim() : "";
 
-    return `[${timestamp}] [${level}]: ${message}${additionalData}${stackInfo ? `\nStack Info: ${stackInfo}` : ""}`;
+    return `[${timestamp}] [${level}]: ${message}${additionalData}${firstStackLineInfo ? `\nStack Info (first line): ${firstStackLineInfo}` : ""} ${stackLog}`;
   }),
 );
 
@@ -40,25 +122,24 @@ const LOGS_LEVEL = process.env.LOGS_LEVEL || "info";
 
 // Логгер Winston
 export const logger = createLogger({
-  format: customFormat, // Формат логов
+  level: LOGS_LEVEL, // Устанавливаем уровень по умолчанию здесь
+  format: customFormat, // Формат логов по умолчанию
   transports: [
     new transports.Console({
       // Логирование в консоль
       format: format.combine(
         format.colorize({ all: true }), // Цветные логи для консоли
-        customFormat,
+        customFormat, // Используем тот же customFormat
       ),
-      level: LOGS_LEVEL,
+      // level для конкретного транспорта переопределит дефолтный, если нужно
     }),
     new transports.File({
-      // Логирование в файл ошибок
       filename: path.join(__dirname, "logs", "all.log"),
-      level: LOGS_LEVEL,
+      // level не указан, значит используется дефолтный 'LOGS_LEVEL'
     }),
     new transports.File({
-      // Логирование в файл ошибок
       filename: path.join(__dirname, "logs", "panic.log"),
-      level: "error",
+      level: "error", // Логирует только 'error' и выше
     }),
   ],
 });