|
@@ -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' и выше
|
|
|
}),
|
|
|
],
|
|
|
});
|