@andrey4emk/npm-app-back-b24 3.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/logs/logs.ts +98 -2
- package/package.json +1 -1
- package/utils/fetchRetry.ts +1 -1
package/logs/logs.ts
CHANGED
|
@@ -62,8 +62,20 @@ class LogsAPI {
|
|
|
62
62
|
|
|
63
63
|
private readonly resetColor = "\x1b[0m";
|
|
64
64
|
|
|
65
|
+
/** Токен Telegram-бота для отправки error-логов */
|
|
66
|
+
private readonly tgBotToken: string | undefined;
|
|
67
|
+
|
|
68
|
+
/** ID чата Telegram для отправки error-логов */
|
|
69
|
+
private readonly tgChatId: string | undefined;
|
|
70
|
+
|
|
71
|
+
/** Имя приложения для заголовка сообщения */
|
|
72
|
+
private readonly appName: string;
|
|
73
|
+
|
|
65
74
|
constructor() {
|
|
66
75
|
this.initConfig();
|
|
76
|
+
this.tgBotToken = process.env.LOG_TG_BOT_TOKEN;
|
|
77
|
+
this.tgChatId = process.env.LOG_TG_CHAT_ID;
|
|
78
|
+
this.appName = process.env.APP_NAME || "Unknown App";
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
/** Проверяем и заполняем конфиг дефолтными значениями если данных нет */
|
|
@@ -83,6 +95,12 @@ class LogsAPI {
|
|
|
83
95
|
*/
|
|
84
96
|
add(message: string | object, level?: LogLevel, jsonData?: unknown): void {
|
|
85
97
|
const levelStr: LogLevel = level || "info";
|
|
98
|
+
const messageText = typeof message === "string" ? message : JSON.stringify(message);
|
|
99
|
+
|
|
100
|
+
// Отправляем error-логи в Telegram независимо от настроек консольного вывода
|
|
101
|
+
if (levelStr === "error") {
|
|
102
|
+
this.sendToTelegram(messageText, jsonData);
|
|
103
|
+
}
|
|
86
104
|
|
|
87
105
|
// Проверяем конфиг, и если логирование отключено, то выходим
|
|
88
106
|
const isLoggingEnabled = confLog.get(`${levelStr}.enabled`) as boolean;
|
|
@@ -90,8 +108,6 @@ class LogsAPI {
|
|
|
90
108
|
return;
|
|
91
109
|
}
|
|
92
110
|
|
|
93
|
-
const messageText = typeof message === "string" ? message : JSON.stringify(message);
|
|
94
|
-
|
|
95
111
|
// Получаем цвет для уровня
|
|
96
112
|
const color = (confLog.get(`${levelStr}.color`) as string) || "\x1b[37m";
|
|
97
113
|
|
|
@@ -106,6 +122,86 @@ class LogsAPI {
|
|
|
106
122
|
console.dir(jsonData, { depth: null, colors: true });
|
|
107
123
|
}
|
|
108
124
|
}
|
|
125
|
+
|
|
126
|
+
/** Лимит длины текста для Telegram (с запасом на HTML-обёртку) */
|
|
127
|
+
private readonly TG_TEXT_LIMIT = 4000;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Отправляет сообщение в Telegram-чат (fire-and-forget)
|
|
131
|
+
* Использует нативный fetch, а не fetchRetry — во избежание циклической зависимости
|
|
132
|
+
* Ошибки не пробрасываются, только console.error
|
|
133
|
+
*/
|
|
134
|
+
private sendToTelegram(message: string, jsonData?: unknown): void {
|
|
135
|
+
if (!this.tgBotToken || !this.tgChatId) return;
|
|
136
|
+
|
|
137
|
+
const url = `https://api.telegram.org/bot${this.tgBotToken}/sendMessage`;
|
|
138
|
+
|
|
139
|
+
// Экранируем HTML-спецсимволы в тексте сообщения
|
|
140
|
+
const escapedMessage = this.escapeHtml(message);
|
|
141
|
+
let text = `<b>ERROR</b> | ${this.escapeHtml(this.appName)}\n${escapedMessage}`;
|
|
142
|
+
|
|
143
|
+
// Добавляем jsonData если есть
|
|
144
|
+
if (jsonData !== undefined) {
|
|
145
|
+
// Безопасная сериализация — логгер не должен бросать исключения
|
|
146
|
+
let jsonStr: string;
|
|
147
|
+
try {
|
|
148
|
+
jsonStr = JSON.stringify(jsonData, null, 2);
|
|
149
|
+
} catch {
|
|
150
|
+
jsonStr = "[Не удалось сериализовать jsonData]";
|
|
151
|
+
}
|
|
152
|
+
text += `\n<pre>${this.escapeHtml(jsonStr)}</pre>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Обрезаем до лимита Telegram
|
|
156
|
+
let isTruncated = false;
|
|
157
|
+
if (text.length > this.TG_TEXT_LIMIT) {
|
|
158
|
+
const truncatedSuffix = "\n...(обрезано)";
|
|
159
|
+
text = text.slice(0, this.TG_TEXT_LIMIT - truncatedSuffix.length) + truncatedSuffix;
|
|
160
|
+
isTruncated = true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Если текст обрезан — отправляем как plain text, чтобы не разорвать HTML-теги
|
|
164
|
+
if (isTruncated) {
|
|
165
|
+
text = this.stripHtml(text);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fetch(url, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/json" },
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
chat_id: this.tgChatId,
|
|
173
|
+
text,
|
|
174
|
+
...(isTruncated ? {} : { parse_mode: "HTML" }),
|
|
175
|
+
}),
|
|
176
|
+
})
|
|
177
|
+
.then((response) => {
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
console.error(`Logs Telegram: HTTP ${response.status}`);
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
.catch((error: unknown) => {
|
|
183
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
184
|
+
console.error(`Logs Telegram: ошибка отправки — ${errMsg}`);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Экранирует HTML-спецсимволы для Telegram parse_mode HTML */
|
|
189
|
+
private escapeHtml(text: string): string {
|
|
190
|
+
return text
|
|
191
|
+
.replace(/&/g, "&")
|
|
192
|
+
.replace(/</g, "<")
|
|
193
|
+
.replace(/>/g, ">");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Убирает HTML-теги и декодирует HTML-entities для plain text отправки */
|
|
197
|
+
private stripHtml(text: string): string {
|
|
198
|
+
return text
|
|
199
|
+
.replace(/<\/?b>/g, "")
|
|
200
|
+
.replace(/<\/?pre>/g, "")
|
|
201
|
+
.replace(/</g, "<")
|
|
202
|
+
.replace(/>/g, ">")
|
|
203
|
+
.replace(/&/g, "&");
|
|
204
|
+
}
|
|
109
205
|
}
|
|
110
206
|
|
|
111
207
|
export const logs = new LogsAPI();
|
package/package.json
CHANGED
package/utils/fetchRetry.ts
CHANGED
|
@@ -64,7 +64,7 @@ export async function fetchRetry(
|
|
|
64
64
|
|
|
65
65
|
logs.add(
|
|
66
66
|
`fetchRetry: попытка ${attempt}/${retries} не удалась (${lastError.message}), повтор через ${delay}мс — ${String(url)}`,
|
|
67
|
-
"
|
|
67
|
+
"warn"
|
|
68
68
|
);
|
|
69
69
|
|
|
70
70
|
await new Promise((resolve) => setTimeout(resolve, delay));
|