From 6b6515795daa7939ce04bd171ff09083704cd31f Mon Sep 17 00:00:00 2001 From: boa604 Date: Mon, 10 Aug 2026 13:24:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A6=D0=98=D0=A2=D0=9A-1008.=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B4?= =?UTF-8?q?=D1=83=D0=B1=D0=BB=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=BB=D0=B0?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D0=B2=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.js | 4 +- core/app.js | 28 ++- core/constants.js | 6 + core/log_file_writer.js | 364 +++++++++++++++++++++++++++++++++ core/logger.js | 192 ++++++++++++++--- core/out_queue.js | 5 + core/out_queue_processor.js | 7 +- index.js | 61 +++--- models/obj_config.js | 8 + models/prms_log_file_writer.js | 27 +++ 10 files changed, 642 insertions(+), 60 deletions(-) create mode 100644 core/log_file_writer.js create mode 100644 models/prms_log_file_writer.js diff --git a/config.js b/config.js index 5d60980..71a951d 100644 --- a/config.js +++ b/config.js @@ -18,7 +18,9 @@ let common = { //Таймаут останова сервера (мс) nTerminateTimeout: 60000, //Контролировать версию Системы - bControlSystemVersion: true + bControlSystemVersion: true, + //Директория размещения логов (не указано / пусто - протоколирование в файл не ведётся) + sLogPath: "" }; //Параметры подключения к БД diff --git a/core/app.js b/core/app.js index 0ed2969..594db21 100644 --- a/core/app.js +++ b/core/app.js @@ -42,6 +42,8 @@ class ParusAppServer { this.bStopping = false; //Таймаут останова сервера this.terminateTimeout = null; + //Код завершения процесса при завершении каскада останова (0 - естественное завершение) + this.nStopExitCode = 0; //Список обслуживаемых сервисов this.services = []; //Привяжем методы к указателю на себя для использования в обработчиках событий @@ -180,20 +182,32 @@ class ParusAppServer { } catch (e) { await this.logger.error(`Ошибка отключения от БД: ${e.sCode}: ${e.sMessage}`); } - //Мы закончили останов - сброс таймера аварийного останова, процесс завершится самостоятельно - if (this.terminateTimeout) { - clearTimeout(this.terminateTimeout); - } } } + //Сообщим об останове (попадёт в файл до закрытия) + await this.logger.notifyServerStopped(this.nStopExitCode); + //Закрываем файл лога + await this.logger.closeLogFile(); + //Мы закончили останов - сброс таймера аварийного останова + if (this.terminateTimeout) { + clearTimeout(this.terminateTimeout); + this.terminateTimeout = null; + } + //Принудительный выход только при ненулевом коде (ошибка старта и т.п.) + if (this.nStopExitCode) process.exit(this.nStopExitCode); } //Инициализация сервера async init(prms) { - await this.logger.info("Инициализация сервера приложений..."); //Проверяем структуру переданного объекта конфигурации let sCheckResult = validateObject(prms, prmsAppSchema.init, "Параметры инициализации"); //Если настройки верны - будем стартовать if (!sCheckResult) { + //Открываем файл лога при необходимости + this.logger.openLogFile(prms.config.common.sLogPath); + //Если файл лога открыт - отразим это только в консоли + this.logger.notifyFileLogConsole(); + //Сообщим о начале инициализации + await this.logger.info("Инициализация сервера приложений..."); //Протоколируем версию и релиз await this.logger.info(`Версия сервера приложений: ${prms.config.common.sVersion}, релиз: ${prms.config.common.sRelease}`); //Создаём подключение к БД @@ -278,12 +292,14 @@ class ParusAppServer { await this.dbConn.connect(); } //Останов сервера - async stop(terminateTimeout) { + async stop(terminateTimeout, nExitCode) { if (!this.bStopping) { //Установим флаг - остановка в процессе this.bStopping = true; //Запомним таймер аварийного останова this.terminateTimeout = terminateTimeout; + //Код завершения после каскада останова (по умолчанию - естественное завершение) + this.nStopExitCode = typeof nExitCode === "number" ? nExitCode : 0; //Сообщаем, что начался останов сервера await this.logger.warn("Останов сервера приложений..."); //Останов обслуживания очереди исходящих diff --git a/core/constants.js b/core/constants.js index 02e7e0e..e5e3655 100644 --- a/core/constants.js +++ b/core/constants.js @@ -52,3 +52,9 @@ exports.SINFO_MQTT = "INFO_MQTT"; //Предупреждение exports.SCONSOLE_LOG_COLOR_PATTERN_ERR = "\x1b[31m%s\x1b[0m%s"; //Цвет для ошибок exports.SCONSOLE_LOG_COLOR_PATTERN_WRN = "\x1b[33m%s\x1b[0m%s"; //Цвет для предупреждений exports.SCONSOLE_LOG_COLOR_PATTERN_INF = "\x1b[32m%s\x1b[0m%s"; //Цвет для информации + +//Префиксы строк протокола +exports.SLOG_PREFIX_INF = "ИНФОРМАЦИЯ"; +exports.SLOG_PREFIX_WRN = "ПРЕДУПРЕЖДЕНИЕ"; +exports.SLOG_PREFIX_ERR = "ОШИБКА"; +exports.SLOG_PREFIX_PROTOCOL_ERR = "ОШИБКА ПРОТОКОЛИРОВАНИЯ"; diff --git a/core/log_file_writer.js b/core/log_file_writer.js new file mode 100644 index 0000000..1b0b107 --- /dev/null +++ b/core/log_file_writer.js @@ -0,0 +1,364 @@ +/* + Сервис интеграции ПП Парус 8 с WEB API + Модуль ядра: запись протокола работы в файл +*/ + +//---------------------- +// Подключение библиотек +//---------------------- + +const fs = require("fs"); //Файловая система +const path = require("path"); //Пути файловой системы +const { validateObject, getNowString, makeErrorText } = require("./utils"); //Вспомогательные функции +const { ServerError } = require("./server_errors"); //Типовая ошибка +const { + SCONSOLE_LOG_COLOR_PATTERN_ERR, + SCONSOLE_LOG_COLOR_PATTERN_INF, + SERR_COMMON, + SLOG_PREFIX_INF, + SLOG_PREFIX_ERR, + SLOG_PREFIX_PROTOCOL_ERR +} = require("./constants"); //Общие константы +const prmsLogFileWriterSchema = require("../models/prms_log_file_writer"); //Схемы валидации параметров функций модуля + +//-------------------------- +// Глобальные идентификаторы +//-------------------------- + +//Таймаут ожидания опустошения очереди записи в файл при закрытии (мс) +const NLOG_FILE_CLOSE_TIMEOUT = 10000; + +//Интервал повторного сообщения об ошибке записи в файл (мс) +const NLOG_FILE_ERROR_REPORT_INTERVAL = 10000; + +//Интервал повторного открытия файла лога после ошибки (мс) +const NLOG_FILE_REOPEN_INTERVAL = 10000; + +//Максимум строк в очереди записи (защита памяти при высокой нагрузке / медленном диске) +const NLOG_FILE_MAX_PENDING = 2000; + +//------------ +// Тело модуля +//------------ + +//Дополнение числа ведущим нулём до двух знаков +const pad2 = nVal => String(nVal).padStart(2, "0"); + +//Формирование имени файла лога: log_yyyymmdd_hh24miss.log +const buildLogFileName = () => { + const dNow = new Date(); + const sDate = `${dNow.getFullYear()}${pad2(dNow.getMonth() + 1)}${pad2(dNow.getDate())}`; + const sTime = `${pad2(dNow.getHours())}${pad2(dNow.getMinutes())}${pad2(dNow.getSeconds())}`; + return `log_${sDate}_${sTime}.log`; +}; + +//Проверка, что каталог существует и доступен для записи +const checkLogDirWritable = sPath => { + try { + //Каталог должен существовать + if (!fs.existsSync(sPath) || !fs.statSync(sPath).isDirectory()) return false; + //Права на запись + fs.accessSync(sPath, fs.constants.W_OK); + //Пробная запись + const sProbe = path.join(sPath, `.exs_log_write_probe_${process.pid}`); + fs.writeFileSync(sProbe, ""); + fs.unlinkSync(sProbe); + return true; + } catch { + return false; + } +}; + +//Признак, что поток записи в файл непригоден +const isLogStreamBroken = stream => !stream || stream.destroyed || !stream.writable; + +//Гарантия перевода строки в конце текста +const ensureLogLineEnd = sLine => (sLine.endsWith("\n") ? sLine : `${sLine}\n`); + +//Безопасное уничтожение потока записи (removeAllListeners + destroy) +const destroyLogStream = stream => { + if (!stream) return null; + try { + stream.removeAllListeners("error"); + if (!stream.destroyed) stream.destroy(); + return null; + } catch (e) { + return e; + } +}; + +//Класс записи протокола в файл +class LogFileWriter { + //Конструктор класса + constructor() { + this.logStream = null; + this.sLogFile = null; + this.bFileLog = false; + this.bClosing = false; + this.writeChain = Promise.resolve(); + this.nQueued = 0; + this.nLastFileWriteErrorAt = 0; + this.nLastLogFileReopenAt = 0; + this.sPendingLogLine = null; + } + //Признак активного протоколирования в файл + isEnabled() { + return this.bFileLog === true; + } + //Сброс состояния писателя (без уничтожения потока) + clearState() { + this.bFileLog = false; + this.bClosing = false; + this.logStream = null; + this.sLogFile = null; + this.writeChain = Promise.resolve(); + this.nQueued = 0; + this.nLastFileWriteErrorAt = 0; + this.nLastLogFileReopenAt = 0; + this.sPendingLogLine = null; + } + //Создание потока записи в файл + createWriteStream(sFilePath) { + const stream = fs.createWriteStream(sFilePath, { flags: "a", encoding: "utf8" }); + stream.on("error", e => this.handleWriteError(e)); + return stream; + } + //Уничтожение текущего потока записи без сброса пути файла + destroyStream() { + destroyLogStream(this.logStream); + this.logStream = null; + } + //Уведомление в консоль о протоколировании в файл (без записи в файл) + notifyOpenedConsole() { + if (!this.isEnabled() || !this.sLogFile) return; + const sNow = getNowString(); + console.log(SCONSOLE_LOG_COLOR_PATTERN_INF, `${sNow} ${SLOG_PREFIX_INF}: `, `Протоколирование в файл: ${this.sLogFile}`); + } + //Уведомление в консоль о результате закрытия файла лога (без записи в файл) + notifyClosedConsole(sLogFile, e) { + const sNow = getNowString(); + if (e) { + console.log( + SCONSOLE_LOG_COLOR_PATTERN_ERR, + `${sNow} ${SLOG_PREFIX_ERR}: `, + `Ошибка закрытия файла лога${sLogFile ? ` (${sLogFile})` : ""}: ${makeErrorText(e)}` + ); + return; + } + if (!sLogFile) return; + console.log(SCONSOLE_LOG_COLOR_PATTERN_INF, `${sNow} ${SLOG_PREFIX_INF}: `, `Файл лога закрыт: ${sLogFile}`); + } + //Сообщение об ошибке записи в файл с ограничением частоты + reportWriteError(e) { + const nNow = Date.now(); + //Ограничиваем спам повторных сообщений + if (nNow - this.nLastFileWriteErrorAt < NLOG_FILE_ERROR_REPORT_INTERVAL) return; + this.nLastFileWriteErrorAt = nNow; + const sNow = getNowString(); + console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ${SLOG_PREFIX_PROTOCOL_ERR} В ФАЙЛ: `, makeErrorText(e)); + } + //Обработка ошибки записи в файл лога + handleWriteError(e, sFailedLine) { + //Если протоколирование в файл не активно - выходим + if (!this.bFileLog && !this.sLogFile) return; + //Сообщим в консоль (с ограничением спама) + this.reportWriteError(e); + //Запомним неудачную строку для однократного повтора после переоткрытия + if (sFailedLine) this.sPendingLogLine = sFailedLine; + //При закрытии не пытаемся восстановить поток - close/release завершат освобождение + if (this.bClosing) return; + //Повтор только после переоткрытия непригодного потока (без цикла на "живом" потоке) + if (isLogStreamBroken(this.logStream)) { + if (this.reopen()) this.retryPendingLine(); + } + } + //Открытие файла лога + open(sLogPath) { + //Если путь не задан - протоколирование в файл не ведётся + if (!sLogPath || !String(sLogPath).trim()) return; + //Нормализуем путь + const sPath = path.resolve(String(sLogPath).trim()); + //Проверяем параметры + let sCheckResult = validateObject({ sLogPath: sPath }, prmsLogFileWriterSchema.open, "Параметры функции открытия файла лога"); + if (sCheckResult) throw new ServerError(SERR_COMMON, sCheckResult); + //Каталог должен существовать и быть доступен для записи + if (!checkLogDirWritable(sPath)) + throw new ServerError(SERR_COMMON, `Каталог размещения логов не существует или недоступен для записи: ${sPath}`); + //Если уже открыт - сначала освободим без сообщения в console + if (this.logStream || this.bFileLog) this.release(undefined, true); + //Формируем полный путь файла сессии + const sFullPath = path.join(sPath, buildLogFileName()); + //Открываем поток записи + this.logStream = this.createWriteStream(sFullPath); + this.sLogFile = sFullPath; + this.bFileLog = true; + } + //Повторное открытие текущего файла лога + reopen() { + //Нет пути файла или идёт закрытие - не переоткрываем + if (!this.sLogFile || this.bClosing) return false; + //Ограничиваем частоту переоткрытия + const nNow = Date.now(); + if (this.nLastLogFileReopenAt > 0 && nNow - this.nLastLogFileReopenAt < NLOG_FILE_REOPEN_INTERVAL) return false; + this.nLastLogFileReopenAt = nNow; + try { + //Освободим прежний поток, если он ещё есть + this.destroyStream(); + //Откроем тот же файл на дозапись + this.logStream = this.createWriteStream(this.sLogFile); + this.bFileLog = true; + return true; + } catch (e) { + this.logStream = null; + this.reportWriteError(e); + return false; + } + } + //Освобождение ресурсов файла лога + release(eNotify, bSilentNotify) { + const stream = this.logStream; + const sLogFile = this.sLogFile; + this.clearState(); + const eClose = destroyLogStream(stream); + //Результат закрытия - только в console + if (!bSilentNotify && sLogFile) this.notifyClosedConsole(sLogFile, eNotify !== undefined ? eNotify : eClose); + } + //Запись одной строки в файл + writeLine(sData) { + return new Promise((resolve, reject) => { + const stream = this.logStream; + //Уже поставленные в очередь строки дописываем даже при bClosing (close ждёт writeChain) + if (isLogStreamBroken(stream) || !this.bFileLog) { + resolve(); + return; + } + //Пишем строку целиком + stream.write(sData, err => { + if (err) reject(err); + else resolve(); + }); + }); + } + //Постановка записи в сериализованную очередь (с ограничением глубины) + scheduleWrite(sData) { + //Защита от разрастания очереди при отставании диска + if (this.nQueued >= NLOG_FILE_MAX_PENDING) { + this.reportWriteError(new Error(`Переполнена очередь записи в файл лога (лимит ${NLOG_FILE_MAX_PENDING}), строка пропущена`)); + return; + } + this.nQueued++; + this.writeChain = this.writeChain + .then(() => this.writeLine(sData)) + .catch(e => this.handleWriteError(e, sData)) + .finally(() => { + if (this.nQueued > 0) this.nQueued--; + }); + } + //Повторная запись строки, не попавшей в файл из-за ошибки + retryPendingLine() { + //Нечего повторять или нельзя писать + if (!this.sPendingLogLine || this.bClosing || isLogStreamBroken(this.logStream)) return; + const sData = this.sPendingLogLine; + this.sPendingLogLine = null; + this.scheduleWrite(sData); + } + //Постановка строки в очередь записи в файл (без ожидания записи вызывающей стороной) + enqueue(sLine) { + //Проверяем параметры + if (typeof sLine !== "string") return; + //Если файл не активен или идёт закрытие - ничего не делаем + if (!this.bFileLog || this.bClosing) return; + //Если поток потерян - попробуем переоткрыть + if (isLogStreamBroken(this.logStream)) { + if (!this.reopen()) return; + } + //Однократный повтор отложенной строки (если была ошибка записи) + this.retryPendingLine(); + if (isLogStreamBroken(this.logStream)) return; + //Гарантируем перевод строки и ставим в очередь + this.scheduleWrite(ensureLogLineEnd(sLine)); + } + //Синхронная дозапись строки в файл (авария / process.on("exit")) + appendSync(sLine) { + if (!this.sLogFile || typeof sLine !== "string") return; + try { + if (this.logStream) { + this.destroyStream(); + this.bFileLog = false; + } + fs.appendFileSync(this.sLogFile, ensureLogLineEnd(sLine), "utf8"); + } catch (e) { + this.reportWriteError(e); + } + } + //Корректное закрытие файла лога + async close() { + //Если файла нет - нечего закрывать + if (!this.logStream && !this.bFileLog && !this.sLogFile) return; + //Фиксируем режим закрытия (новые строки и reopen не принимаем) + this.bClosing = true; + this.sPendingLogLine = null; + const sLogFile = this.sLogFile; + let nDrainTimer = null; + try { + //Дождёмся опустошения очереди записи (с ограничением по времени) + const bDrained = await Promise.race([ + this.writeChain.then(() => true).catch(() => true), + new Promise(resolve => { + nDrainTimer = setTimeout(() => resolve(false), NLOG_FILE_CLOSE_TIMEOUT); + }) + ]); + if (nDrainTimer) { + clearTimeout(nDrainTimer); + nDrainTimer = null; + } + //Очередь не успела опустеть - принудительно освободим файл + if (!bDrained) { + this.reportWriteError( + new Error(`Очередь записи в файл лога не опустела за ${NLOG_FILE_CLOSE_TIMEOUT} мс, файл будет закрыт принудительно`) + ); + this.release(); + return; + } + //Закроем поток штатно + await new Promise((resolve, reject) => { + if (!this.logStream || this.logStream.destroyed) { + resolve(); + return; + } + this.logStream.end(err => { + if (err) reject(err); + else resolve(); + }); + }); + } catch (e) { + //Только сообщение - без reopen при закрытии + this.reportWriteError(e); + this.release(e); + return; + } finally { + if (nDrainTimer) clearTimeout(nDrainTimer); + } + //Освобождаем состояние без повторного destroy уже закрытого потока + if (this.logStream) { + try { + this.logStream.removeAllListeners("error"); + } catch {} + } + this.clearState(); + //Результат закрытия - только в console + this.notifyClosedConsole(sLogFile, null); + } + //Аварийное синхронное освобождение файла лога + closeSync() { + if (!this.logStream && !this.bFileLog && !this.sLogFile) return; + this.bClosing = true; + this.release(); + } +} + +//----------------- +// Интерфейс модуля +//----------------- + +exports.LogFileWriter = LogFileWriter; diff --git a/core/logger.js b/core/logger.js index b90c279..350d3b3 100644 --- a/core/logger.js +++ b/core/logger.js @@ -7,22 +7,76 @@ // Подключение библиотек //---------------------- -const { validateObject, getNowString, deepClone } = require("./utils"); //Вспомогательные функции +const { validateObject, getNowString, deepClone, makeErrorText } = require("./utils"); //Вспомогательные функции const db = require("./db_connector"); //Модуль взаимодействия с БД -const { SCONSOLE_LOG_COLOR_PATTERN_ERR, SCONSOLE_LOG_COLOR_PATTERN_WRN, SCONSOLE_LOG_COLOR_PATTERN_INF } = require("./constants"); //Общие константы +const { LogFileWriter } = require("./log_file_writer"); //Запись протокола в файл +const { + SCONSOLE_LOG_COLOR_PATTERN_ERR, + SCONSOLE_LOG_COLOR_PATTERN_WRN, + SCONSOLE_LOG_COLOR_PATTERN_INF, + SLOG_PREFIX_INF, + SLOG_PREFIX_WRN, + SLOG_PREFIX_ERR, + SLOG_PREFIX_PROTOCOL_ERR +} = require("./constants"); //Общие константы const { NLOG_STATE_INF, NLOG_STATE_WRN, NLOG_STATE_ERR } = require("../models/obj_log"); //Схемы валидации записи журнала работы сервиса обмена const prmsLoggerSchema = require("../models/prms_logger"); //Схемы валидации параметров функций модуля +//-------------------------- +// Глобальные идентификаторы +//-------------------------- + +//Интервал повторного сообщения об ошибке передачи строки лога по IPC (мс) +const NLOG_IPC_ERROR_REPORT_INTERVAL = 10000; + +//Сообщение об останове сервера +const SMSG_SERVER_STOPPED = "Сервер приложений остановлен"; + //------------ // Тело модуля //------------ +//Признак работы в дочернем процессе обработчика +const isWorkerProcess = () => typeof process.send === "function" && !!process.connected; + +//Формирование строки лога без ANSI-оформления +const formatLogLine = (sNow, sPrefix, sMsg) => `${sNow} ${sPrefix}: ${sMsg}`; + +//Оформление сообщения по типу записи +const resolveLogStyle = nLogState => { + let sPrefix = SLOG_PREFIX_INF; + let sColorPattern = ""; + switch (nLogState) { + case NLOG_STATE_ERR: { + sPrefix = SLOG_PREFIX_ERR; + sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_ERR; + break; + } + case NLOG_STATE_WRN: { + sPrefix = SLOG_PREFIX_WRN; + sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_WRN; + break; + } + case NLOG_STATE_INF: { + sPrefix = SLOG_PREFIX_INF; + sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_INF; + break; + } + default: + break; + } + return { sPrefix, sColorPattern }; +}; + //Класс управления протоколом class Logger { //Конструктор класса constructor() { this.dbConnector = null; this.bLogDB = false; + this.fileWriter = null; + this.bStopLogged = false; + this.nLastIpcErrorAt = 0; } //Включение/выключение записи протоколов в БД setLogDB(bLogDB) { @@ -41,6 +95,109 @@ class Logger { this.dbConnector = null; this.setLogDB(false); } + //Открытие файла лога (пустой путь - не открываем) + openLogFile(sLogPath) { + //Нет пути - протоколирование в файл не ведётся + if (!sLogPath || !String(sLogPath).trim()) { + this.bStopLogged = false; + return; + } + //Создаём писатель при первом реальном открытии + if (!this.fileWriter) this.fileWriter = new LogFileWriter(); + this.fileWriter.open(sLogPath); + this.bStopLogged = false; + } + //Уведомление в консоль о протоколировании в файл (без записи в файл) + notifyFileLogConsole() { + if (this.fileWriter) this.fileWriter.notifyOpenedConsole(); + } + //Корректное закрытие файла лога + async closeLogFile() { + if (this.fileWriter) await this.fileWriter.close(); + } + //Аварийное синхронное освобождение файла лога + closeLogFileSync() { + if (this.fileWriter) this.fileWriter.closeSync(); + } + //Признак, что сообщение об останове уже выдано + wasStopLogged() { + return this.bStopLogged === true; + } + //Фиксация выдачи сообщения об останове + markStopLogged() { + this.bStopLogged = true; + } + //Подготовка сообщения об останове сервера + prepareServerStoppedMsg(nCode) { + const nExitCode = typeof nCode === "number" ? nCode : 0; + this.markStopLogged(); + return `${SMSG_SERVER_STOPPED} (код: ${nExitCode})`; + } + //Выдача сообщения об останове сервера + async notifyServerStopped(nCode) { + await this.warn(this.prepareServerStoppedMsg(nCode)); + } + //Синхронная выдача сообщения об останове сервера (авария / process.on("exit")) + notifyServerStoppedSync(nCode) { + this.warnSync(this.prepareServerStoppedMsg(nCode)); + } + //Сообщение об ошибке передачи строки лога по IPC + reportIpcLogError(e) { + const nNow = Date.now(); + if (nNow - this.nLastIpcErrorAt < NLOG_IPC_ERROR_REPORT_INTERVAL) return; + this.nLastIpcErrorAt = nNow; + console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${getNowString()} ${SLOG_PREFIX_PROTOCOL_ERR} (IPC): `, makeErrorText(e)); + } + //Отправка строки лога родителю (воркер) + sendLogLineToParent(prms) { + if (typeof process.send !== "function" || !process.connected) return; + if (!prms || typeof prms.sMsg !== "string") return; + try { + process.send({ ...prms, bLogLine: true }, err => { + if (err) this.reportIpcLogError(err); + }); + } catch (e) { + this.reportIpcLogError(e); + } + } + //Приём строки лога от воркера + acceptWorkerLogLine(prms) { + if (!prms || typeof prms.sNow !== "string" || typeof prms.sPrefix !== "string" || typeof prms.sMsg !== "string") return; + this.emitLogLine(prms.nLogState, prms.sMsg, prms.sNow, prms.sPrefix); + } + //Выдача строки протокола: воркер - IPC; main - файл, затем console + emitLogLine(nLogState, sMsg, sNow, sPrefix, bSync) { + const style = resolveLogStyle(nLogState); + const sPrefixUse = sPrefix || style.sPrefix; + //Воркер - только IPC родителю (файл и console пишет main) + if (isWorkerProcess()) { + this.sendLogLineToParent({ nLogState, sNow, sPrefix: sPrefixUse, sMsg }); + return; + } + //Main: сначала файл, затем console + const sLine = formatLogLine(sNow, sPrefixUse, sMsg); + if (this.fileWriter) { + if (bSync) this.fileWriter.appendSync(sLine); + else this.fileWriter.enqueue(sLine); + } + console.log(style.sColorPattern, `${sNow} ${sPrefixUse}: `, sMsg); + } + //Синхронное протоколирование без БД (авария / финал) + logSync(nLogState, sMsg) { + this.emitLogLine(nLogState, sMsg, getNowString(), undefined, true); + } + //Синхронное протоколирование ошибки + errorSync(sMsg) { + this.logSync(NLOG_STATE_ERR, sMsg); + } + //Синхронное протоколирование предупреждения + warnSync(sMsg) { + this.logSync(NLOG_STATE_WRN, sMsg); + } + //Синхронное протоколирование информации + infoSync(sMsg) { + this.logSync(NLOG_STATE_INF, sMsg); + } //Протоколирование async log(prms) { //Фиксируем время @@ -49,30 +206,7 @@ class Logger { let sCheckResult = validateObject(prms, prmsLoggerSchema.log, "Параметры функции протоколирования"); //Если структура объекта в норме if (!sCheckResult) { - //Определим оформление сообщения - let sPrefix = "ИНФОРМАЦИЯ"; - let sColorPattern = ""; - switch (prms.nLogState) { - case NLOG_STATE_ERR: { - sPrefix = "ОШИБКА"; - sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_ERR; - break; - } - case NLOG_STATE_WRN: { - sPrefix = "ПРЕДУПРЕЖДЕНИЕ"; - sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_WRN; - break; - } - case NLOG_STATE_INF: { - sPrefix = "ИНФОРМАЦИЯ"; - sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_INF; - break; - } - default: - break; - } - //Выдаём сообщение - console.log(sColorPattern, `${sNow} ${sPrefix}: `, prms.sMsg); + this.emitLogLine(prms.nLogState, prms.sMsg, sNow); //Протоколируем в БД, если это необходимо if (this.bLogDB) { try { @@ -81,15 +215,15 @@ class Logger { await this.dbConnector.putLog(prms); } } catch (e) { - console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ОШИБКА ПРОТОКОЛИРОВАНИЯ: `, e.sMessage); + this.emitLogLine(NLOG_STATE_ERR, e.sMessage, sNow, SLOG_PREFIX_PROTOCOL_ERR); try { if (this.dbConnector && this.dbConnector.bConnected) - await this.dbConnector.putLog({ nLogState: NLOG_STATE_ERR, sMsg: `ОШИБКА ПРОТОКОЛИРОВАНИЯ: ${e.sMessage}` }); + await this.dbConnector.putLog({ nLogState: NLOG_STATE_ERR, sMsg: `${SLOG_PREFIX_PROTOCOL_ERR}: ${e.sMessage}` }); } catch {} } } } else { - console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ОШИБКА ПРОТОКОЛИРОВАНИЯ: `, sCheckResult); + this.emitLogLine(NLOG_STATE_ERR, sCheckResult, sNow, SLOG_PREFIX_PROTOCOL_ERR); console.log(prms); } } diff --git a/core/out_queue.js b/core/out_queue.js index 4378edd..4c45c87 100644 --- a/core/out_queue.js +++ b/core/out_queue.js @@ -271,6 +271,11 @@ class OutQueue extends EventEmitter { const proc = ChildProcess.fork("core/out_queue_processor", { silent: false }); //Перехват сообщений обработчика proc.on("message", async result => { + //Строка лога от воркера - файл и console родителя (без завершения задачи) + if (result && result.bLogLine === true) { + self.logger.acceptWorkerLogLine(result); + return; + } //Сбросим таймер принудительного останова self.clearWorkerTerminateTimeout(proc); //Перечитывание не требуется, если выполнено успешно diff --git a/core/out_queue_processor.js b/core/out_queue_processor.js index 4c6ac47..e15e8c8 100644 --- a/core/out_queue_processor.js +++ b/core/out_queue_processor.js @@ -753,8 +753,13 @@ process.on("SIGTERM", () => { //Перехват ошибок process.on("uncaughtException", e => { + const sText = makeErrorText(e); + //Синхронно в console и (при необходимости) IPC родителю - до возможной гибели процесса + if (logger) { + logger.errorSync(`Непредвиденная ошибка обработчика исходящего сообщения: ${sText}`); + } //Отправляем ошибку родительскому процессу - sendErrorResult({ sMessage: makeErrorText(e) }); + sendErrorResult({ sMessage: sText }); }); //Приём сообщений diff --git a/index.js b/index.js index ee343bc..016983e 100644 --- a/index.js +++ b/index.js @@ -9,8 +9,7 @@ const cfg = require("./config"); //Настройки сервера приложений const app = require("./core/app"); //Сервер приложений -const { makeErrorText, getNowString } = require("./core/utils"); //Вспомогательные функции -const { SCONSOLE_LOG_COLOR_PATTERN_ERR, SCONSOLE_LOG_COLOR_PATTERN_WRN } = require("./core/constants"); //Общие константы +const { makeErrorText } = require("./core/utils"); //Вспомогательные функции //-------------------------- // Глобальные идентификаторы @@ -31,10 +30,30 @@ process.env.NODE_ORACLE_DB_THIN_MODE = cfg?.dbConnect?.bOraUseThinMode === true //Путь к клиентским библиотекам для Oracle process.env.ORACLE_CLIENT_LIB_DIR = cfg?.dbConnect?.sOraClient || ""; +//Аварийное освобождение файла лога +const releaseLogFileEmergency = () => { + try { + appSrv.logger.closeLogFileSync(); + } catch {} +}; + +//Аварийный выход с протоколированием и освобождением файла лога +const emergencyExit = sErrMsg => { + if (sErrMsg) appSrv.logger.errorSync(sErrMsg); + if (!appSrv.logger.wasStopLogged()) { + appSrv.logger.notifyServerStoppedSync(1); + } + releaseLogFileEmergency(); + process.exit(1); +}; + //Обработка события "выход" жизненного цикла процесса process.on("exit", code => { - //Сообщим о завершении процесса - console.log(SCONSOLE_LOG_COLOR_PATTERN_WRN, `${getNowString()} ПРЕДУПРЕЖДЕНИЕ: `, `Сервер приложений остановлен (код: ${code})`); + //Если сообщение об останове уже было выдано ранее - не дублируем + if (appSrv.logger.wasStopLogged()) return; + //Допишем в console и в файл (если ещё открыт), затем освободим дескриптор + appSrv.logger.notifyServerStoppedSync(code); + releaseLogFileEmergency(); }); //Обработка событий мягкого останова процесса @@ -42,30 +61,22 @@ process.on("exit", code => { process.once(sSig, async () => { await appSrv.logger.warn(`Получен сигнал на останов сервера приложений: ${sSig}`); const terminateTimeout = setTimeout(() => { - console.log( - SCONSOLE_LOG_COLOR_PATTERN_ERR, - `${getNowString()} ОШИБКА: `, - `Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса.` - ); - process.exit(1); + emergencyExit("Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса."); }, cfg.common.nTerminateTimeout); try { await appSrv.stop(terminateTimeout); } catch (e) { - console.log(e); - await appSrv.logger.error(`При останове сервера приложений: ${makeErrorText(e)}`); clearTimeout(terminateTimeout); - process.exit(1); + //Только sync: дальше закрываем файл и выходим + appSrv.logger.errorSync(`При останове сервера приложений: ${makeErrorText(e)}`); + emergencyExit(); } }); }); //Перехват всех неохваченных ошибок process.on("uncaughtException", e => { - //Протоколируем ошибку - console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${getNowString()} НЕПРЕДВИДЕННАЯ ОШИБКА: `, makeErrorText(e)); - //Останов с ошибкой - process.exit(1); + emergencyExit(`НЕПРЕДВИДЕННАЯ ОШИБКА: ${makeErrorText(e)}`); }); //Запуск сервера приложений @@ -78,13 +89,17 @@ const start = async () => { } catch (e) { //Если есть ошибки с которыми сервер не справился - ловим их, показываем... await appSrv.logger.error(makeErrorText(e)); - //...и пытаемся остановить сервер нормально + //...и пытаемся остановить сервер нормально (с таймаутом и кодом 1) + const terminateTimeout = setTimeout(() => { + emergencyExit("Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса."); + }, cfg.common.nTerminateTimeout); try { - await appSrv.stop(); - } catch (e) { - //Могут быть ошибки и при остановке - это аварийный выход - await appSrv.logger.error(makeErrorText(e)); - process.exit(1); + await appSrv.stop(terminateTimeout, 1); + } catch (eStop) { + clearTimeout(terminateTimeout); + //Только sync: дальше закрываем файл и выходим + appSrv.logger.errorSync(makeErrorText(eStop)); + emergencyExit(); } } }; diff --git a/models/obj_config.js b/models/obj_config.js index 4b0a21d..4635937 100644 --- a/models/obj_config.js +++ b/models/obj_config.js @@ -134,6 +134,14 @@ const common = new Schema({ type: path => `Признак контроля версии Системы (${path}) имеет некорректный тип данных (ожидалось - Boolean)`, required: path => `Не указан признак контроля версии Системы (${path})` } + }, + //Директория размещения логов + sLogPath: { + type: String, + required: false, + message: { + type: path => `Директория размещения логов (${path}) имеет некорректный тип данных (ожидалось - String)` + } } }); diff --git a/models/prms_log_file_writer.js b/models/prms_log_file_writer.js new file mode 100644 index 0000000..f487e75 --- /dev/null +++ b/models/prms_log_file_writer.js @@ -0,0 +1,27 @@ +/* + Сервис интеграции ПП Парус 8 с WEB API + Модели данных: описатели параметров функций модуля записи протокола в файл (класс LogFileWriter) +*/ + +//---------------------- +// Подключение библиотек +//---------------------- + +const Schema = require("validate"); //Схемы валидации + +//----------------- +// Интерфейс модуля +//----------------- + +//Схема валидации параметров функции открытия файла лога +exports.open = new Schema({ + //Директория размещения логов + sLogPath: { + type: String, + required: true, + message: { + type: path => `Директория размещения логов (${path}) имеет некорректный тип данных (ожидалось - String)`, + required: path => `Не указана директория размещения логов (${path})` + } + } +});