fock-logger
Version:
Simple logger for your pet-project
453 lines (452 loc) • 21.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = exports.DEFAULT_EXECUTE_DATA = exports.loggersNames = void 0;
const configurator_1 = __importDefault(require("../config/configurator"));
const { config } = new configurator_1.default();
const color_1 = require("../utils/color");
const loggers_names_1 = __importDefault(require("../data/loggers.names"));
const file_logger_1 = __importDefault(require("./file.logger"));
exports.loggersNames = new loggers_names_1.default(config.logging);
var Keys;
(function (Keys) {
Keys["ctrl_backspace"] = "\u0017";
Keys["ctrl_c"] = "\u0003";
Keys["backspace"] = "";
Keys["backspaceTwo"] = "\b";
Keys["enter"] = "\r";
})(Keys || (Keys = {}));
const CLEAR_KEYS = [
Keys.backspace,
Keys.backspaceTwo
];
exports.DEFAULT_EXECUTE_DATA = {
level: "info",
end: "\n",
join: " ",
sign: true,
hideSymbol: "*",
hideInput: false,
overwriteListeners: false,
};
class Logger {
name;
out;
input;
_file_logger;
_config;
_execute_data;
constructor(name, data = {
...exports.DEFAULT_EXECUTE_DATA,
}, out = process.stdout, input = process.stdin) {
this.name = name;
this.out = out;
this.input = input;
this._config = {
...config,
...exports.loggersNames.GetNames()[name],
...data,
name,
};
this._execute_data = {
...exports.DEFAULT_EXECUTE_DATA,
color: this._config.colors[1],
write: this._config.logging,
level: (data.level || this._config.defaultLevel),
};
this._file_logger = new file_logger_1.default(this._config.dir, this._config, config.logging);
exports.loggersNames.SetNames({
[this._config.name]: {
name: this._config.name,
colors: this._config.colors,
},
});
}
/**
* Главный метод, который используется для вывода информации в терминал
* и последующей её записи в файл
* @param text Ваш текст или другая информация, которая будет переведена в `JSON` формат (если Вы закинули массив)
* или отобразится как `Error` если элемент будет соответствовать `Error`
*
* @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите
*
* ## Значения
*
* - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию)
* - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию)
* - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию)
* - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию)
* - `end` — Конец строки для логгера (`\n` по умолчанию)
* - `join` — Соединение для значения массиов (`" "` по умолчанию)
*
* @returns значения `colored` и `base`, где первый — цветное отображение Ваших данных, а `base` — обычное, в Вашем виде данных
*/
execute(text, data = {}) {
return this.log(text, data, "execute");
}
/**
* Выводит ошибки в терминал и обрабатывает их, также запись в `.log`-файл
*
* @param text Ваши ошибки
*
* @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите
*
* ## Значения
*
* - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию)
* - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию)
* - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию)
* - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию)
* - `end` — Конец строки для логгера (`\n` по умолчанию)
* - `join` — Соединение для значения массиов (`" "` по умолчанию)
*
* @returns значения `colored` и `base`, где первый — цветное отображение Ваших данных, а `base` — обычное, в Вашем виде данных
*/
error(text, data = {}) {
return this.log(text, data, "error");
}
cleanupInput(listeners) {
if (listeners) {
listeners.forEach((listener) => this.input.removeAllListeners(listener));
}
else {
this.input.removeAllListeners();
}
this.input.setRawMode(false);
this.input.pause();
}
/**
* Читает текст, который введёт пользователь в терминал. Читает все символы последовательно
*
* @param text То, что Вы хотите вывести перед тем, как пользователь начнёт ввод
*
* @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите
*
* ## Значения
*
* - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию)
* - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию)
* - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию)
* - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию)
* - `end` — Конец строки для логгера (`\n` по умолчанию)
* - `join` — Соединение для значения массиов (`" "` по умолчанию)
* - `listeners` — Ваши прослушиватели, если хотите больше гибкости (`{}`)
* - `overwriteListeners` — Булево значение, говорит, нужно ли перезаписывать дефолтные прослушиватели логгера (`false`)
* - `hideInput` — Булево значение, говорит, нужно ли прятать текст в терминале (ввод превратится в `****`) (`false`)
*
* @returns текст, что ввёл пользователь
*/
readRaw(text, data = {}) {
const configuration = this.resolveData(data, this._execute_data);
return new Promise((resolve, reject) => {
this.input.setRawMode(true);
this.input.resume();
this.input.setEncoding("utf8");
const listeners = (configuration?.listeners || (() => undefined))(this.input);
listeners?.onStart?.();
this.execute(text, configuration);
let globalData = "";
const onData = (buffer) => {
listeners?.onData?.(buffer);
const key = buffer.toString("utf8");
if (key === Keys.ctrl_c) {
cleanup();
return reject(new Error("User interrupted with Ctrl+C"));
}
if (key === Keys.ctrl_backspace) {
this.clearChars(globalData.length, this.out);
return (globalData = "");
}
if (CLEAR_KEYS.includes(key)) {
if (globalData.length === 0) {
return;
}
this.clearChars(1, this.out);
return (globalData = globalData.slice(0, -1));
}
if (Keys.enter === key) {
cleanup();
this.out.write("\n");
return resolve(globalData);
}
globalData += key;
this.out.write(data.hideInput ? configuration.hideSymbol : key);
};
const onError = (err) => {
listeners?.onError?.(err);
cleanup();
reject(err);
};
const onEnd = () => {
cleanup();
reject(new Error("Stream ended without data"));
};
const cleanup = () => {
this.input.removeListener("data", onData);
this.input.removeListener("error", onError);
this.input.removeListener("end", onEnd);
this.input.setRawMode(false);
this.input.pause();
if (listeners?.onData) {
this.input.removeListener("data", listeners.onData);
}
if (listeners?.onError) {
this.input.removeListener("error", listeners.onError);
}
if (listeners?.onEnd) {
this.input.removeListener("end", listeners.onEnd);
}
};
if (data.overwriteListeners) {
this.input.on("data", listeners?.onData || onData);
this.input.on("error", listeners?.onError || onError);
this.input.on("end", listeners?.onEnd || onEnd);
}
else {
this.input.on("data", onData);
this.input.on("error", onError);
this.input.on("end", onEnd);
}
});
}
/**
* Читает текст, который введёт пользователь в терминал. Читает всё целиком
*
* @param text То, что Вы хотите вывести перед тем, как пользователь начнёт ввод
*
* @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите
*
* ## Значения
*
* - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию)
* - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию)
* - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию)
* - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию)
* - `end` — Конец строки для логгера (`\n` по умолчанию)
* - `join` — Соединение для значения массиов (`" "` по умолчанию)
* - `listeners` — Ваши прослушиватели, если хотите больше гибкости (`{}`)
*
* @returns текст, что ввёл пользователь
*/
read(text, data = {}) {
const configuration = this.resolveData(data, this._execute_data);
return new Promise((resolve, reject) => {
this.input.resume();
this.input.setEncoding("utf8");
const listeners = (configuration?.listeners || (() => undefined))(this.input);
listeners?.onStart?.();
const cleanup = () => {
this.input.removeListener("readable", onReadable);
this.input.removeListener("error", onError);
this.input.removeListener("end", onEnd);
if (listeners?.onData) {
this.input.removeListener("data", listeners.onData);
}
this.input.pause();
};
this.execute(text, configuration);
const onReadable = () => {
try {
listeners?.onReadable?.();
const userInput = this.input.read();
if (!userInput) {
return reject(new Error("No user input resolved"));
}
cleanup();
const input = userInput.replace(/\r?\n$/, "");
this._file_logger.execute("User: " + input);
return resolve(input);
}
catch (error) {
cleanup();
return reject(error);
}
};
const onError = (err) => {
listeners?.onError?.(err);
cleanup();
return reject(err);
};
const onEnd = () => {
listeners?.onEnd?.();
cleanup();
return reject(new Error("Stream ended without data"));
};
this.input.on("readable", onReadable);
this.input.on("error", onError);
this.input.on("end", onEnd);
if (listeners?.onData) {
this.input.on("data", listeners.onData);
}
});
}
/**
* Изменяет последнюю линию в терминале
*
* @param text То, что Вы хотите вывести перед тем, как пользователь начнёт ввод
* @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите
*
* ## Значения
*
* - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию)
* - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию)
* - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию)
* - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию)
* - `end` — Конец строки для логгера (`\n` по умолчанию)
* - `join` — Соединение для значения массиов (`" "` по умолчанию)
* - `ignoreLineBreakerError` — Булево значение, говорит, игнорировать ли ошибки о `\n` (`false` по умолчанию)
*
* @returns тот же метод для замены предыдущего текста
*
* @example
* ```ts
* const logger = new Logger();
*
* const datas = [
* "|",
* "/",
* "—",
* "\\",
* ];
*
* let i = 1;
*
* const changeLine = logger.changeLine(datas[i-1], { end: "" });
*
* setInterval(() => {
* if (i>datas.length-1) i = 0;
*
* changeLine(datas[i++]);
* }, 200);
* ```
*/
changeLine(text, data = {}) {
const configuration = this.resolveData(data, this._execute_data);
const errorCaptched = !configuration.ignoreLineBreakerError && configuration.end.includes("\n");
if (errorCaptched) {
throw new Error("Can not resolve line breaker");
}
this.execute(text, configuration);
return (t, d = {}) => {
this.out.cursorTo(-1);
this.out.clearLine(0);
return this.changeLine(t, { ...data, ...d });
};
}
executeLogFile(text, colored) {
if (typeof text === "string") {
this._file_logger.execute(`${this.name}: ${text}`);
}
else {
this._file_logger.execute(`${this.name}:`);
for (const msg of colored) {
this._file_logger.execute(msg[1]);
}
}
}
errorLogFile(text) {
this._file_logger.error(text);
}
log(text, data, type) {
const configuration = this.resolveData(data, this._execute_data);
const { colored, base } = this.resolveText(Array.isArray(text) ? text : [text], configuration.color);
const name = (0, color_1.color)(this.name, this._config.colors[0]) + ":";
const date = `[${new Date().toISOString()}]`;
const { prefix, join, suffix } = this.resolveAffix({
...configuration,
date,
name,
});
const configLevel = this._config.levels[config.level];
const messageLevel = this._config.levels[configuration.level];
if (configLevel === undefined || messageLevel === undefined) {
throw new Error(`Invalid log level. Config level: ${config.level}, Message level: ${configuration.level}`);
}
const isLevelEqualsOrLess = configLevel <= messageLevel;
if (isLevelEqualsOrLess) {
// Используем уже окрашенный текст без дополнительного окрашивания
this.out.write(prefix + colored.join(join) + suffix);
}
const logEnabled = (config.logging && this._file_logger) || configuration.write;
if (logEnabled) {
this.logFileService({ type, text, colored });
}
return {
colored,
base,
};
}
clearChars(length, stdout) {
const clear = new Array(length).fill("\b").join("");
const space = new Array(length).fill(" ").join("");
stdout.write(clear + space + clear);
}
logFileService({ type, text, colored, }) {
const suffix = "LogFile";
const prefix = type;
const name = `${prefix}${suffix}`;
this[name](text, colored);
}
get write() {
return this._file_logger.execute;
}
get colors() {
return this._config.colors;
}
resolveData(data, defaultData) {
return { ...defaultData, ...data };
}
resolveText(text, color) {
const out = typeof text === "string" ? [text] : text;
const finalColor = color || this._config.colors[1];
const seen = new WeakSet();
const output = out.map((text) => {
const processedText = typeof text !== "string"
? text instanceof Error
? text.stack || text.message
: JSON.stringify(text, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
}, 4)
: text;
// Добавляем цвет только если он определен
return finalColor ? (0, color_1.color)(processedText, finalColor) : processedText;
});
return {
colored: output,
base: out,
};
}
resolveAffix({ date, name, end, join, sign, }) {
const prefix = this.resolveLogPrefix({
date,
dateEnabled: this._config.date,
sign: name,
signEnabled: sign !== false,
});
const resolvedJoin = this.resolveExecuteData("join", join);
const suffix = this.resolveExecuteData("end", end);
return {
prefix,
suffix,
join: resolvedJoin,
};
}
resolveExecuteData(key, data) {
return data !== undefined ? data : exports.DEFAULT_EXECUTE_DATA[key];
}
resolveLogPrefix({ date, dateEnabled, sign, signEnabled, }) {
const datePrefix = dateEnabled ? date + " " : "";
const signPrefix = signEnabled ? sign + " " : "";
return datePrefix + signPrefix;
}
}
exports.Logger = Logger;
exports.default = Logger;