From 10f430592db8bdacacd0dcfe53489ecabf8e8cee Mon Sep 17 00:00:00 2001 From: soffee Date: Tue, 27 Jan 2026 01:41:53 +0300 Subject: [PATCH] fix lint issues; use polling instead of fs.watchFile for keywords reloading; add error handling in dialogs collector --- package.json | 2 +- src/config.ts | 23 +++++++++++++++-------- src/main.ts | 31 ++++++++++++++++++++++++------- src/metrics/dialogs.ts | 14 +++++++++----- src/metrics/keywords.ts | 2 +- src/server.ts | 2 +- 6 files changed, 51 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 0bba223..68c5d49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mtproto_exporter", "type": "module", - "version": "1.5.0", + "version": "1.5.1", "packageManager": "pnpm@10.6.5", "license": "MIT", "scripts": { diff --git a/src/config.ts b/src/config.ts index b76c3d9..7219df4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,7 @@ export interface Configuration { wordsCounter: boolean; keywordsFile: string; watchFile: boolean; + watchFileIntervalSeconds: number; includePeers?: number[]; excludePeers?: number[]; keywords?: RawKeywordLike[]; @@ -17,22 +18,25 @@ export interface Configuration { dialogs: boolean; messages: boolean; reactions: boolean; - } + }; reactionsCollector: { - loadHistory: boolean, - loadHistorySize: number, - }, + loadHistory: boolean; + loadHistorySize: number; + }; messagesCollector: { - includeSender: boolean, - }, + includeSender: boolean; + }; } +/* eslint-disable style/no-multi-spaces, style/key-spacing */ + const optionDefinitions: OptionDefinition[] = [ { name: "bind-host", alias: "b", type: String, defaultValue: "0.0.0.0" }, { name: "port", alias: "p", type: Number, defaultValue: 9669 }, { name: "words-counter", type: Boolean, defaultValue: false }, { name: "keywords-file", alias: "k", type: String }, { name: "watch-file", alias: "w", type: Boolean, defaultValue: false }, + { name: "watch-file-interval-seconds", alias: "W", type: Number, defaultValue: 60 }, { name: "include-peers", alias: "i", type: String, multiple: true }, { name: "exclude-peers", alias: "x", type: String, multiple: true }, { name: "reactions-collector", type: Boolean, defaultValue: false }, @@ -43,6 +47,8 @@ const optionDefinitions: OptionDefinition[] = [ { name: "messages-collector-include-sender", type: Boolean, defaultValue: false }, ]; +/* eslint-enable style/no-multi-spaces, style/key-spacing */ + const cli = cmdline(optionDefinitions); const config: Configuration = { @@ -51,6 +57,7 @@ const config: Configuration = { wordsCounter: cli["words-counter"], keywordsFile: cli["keywords-file"], watchFile: cli["watch-file"], + watchFileIntervalSeconds: cli["watch-file-interval-seconds"], keywords: cli["keywords-file"] ? await readKeywords(cli["keywords-file"]) : undefined, collectors: { dialogs: cli["dialogs-collector"], @@ -94,7 +101,7 @@ export async function readKeywords(filePath: string): Promise keywords.push(item); } else if (typeof item === "object" && typeof item.name === "string") { if (typeof item.pattern === "string") { - let result = { + const result = { name: item.name, pattern: item.pattern, word: Boolean(item.word ?? false), @@ -102,7 +109,7 @@ export async function readKeywords(filePath: string): Promise global: true, multi_line: false, insensitive: true, - } + }, }; if (typeof item.flags === "object") { diff --git a/src/main.ts b/src/main.ts index 4421868..e984f68 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,27 +41,44 @@ if (config.collectors.messages) { } if (config.collectors.reactions) { - console.log("[WARN] reactions-collector is enabled, but it is very experimental and almost does not work. i don't recommend enabling it especially for production use.") + console.log("[WARN] reactions-collector is enabled, but it is very experimental and almost does not work. i don't recommend enabling it especially for production use."); metrics.collectReactionsMetrics(tg, dp, registry); } if (config.keywords) { const counter = new metrics.KeywordsCounter(dp, rawToPatterns(config.keywords)); + console.log("[keywords] Initialized keywords counter with", counter.keywords.length, "keywords/patterns."); + registry.registerMetric(counter); if (config.watchFile) { - fs.watchFile(config.keywordsFile, async (curr, prev) => { - if (curr.mtimeMs === prev.mtimeMs) { - return; - } - console.log("[watch-file] Keywords file was updated. Reloading keywords configuration..."); + const reloadConfig = async () => { try { config.keywords = await readKeywords(config.keywordsFile); counter.setKeywords(rawToPatterns(config.keywords)); + console.log(`Loaded ${counter.keywords.length} keywords/patterns.`); } catch (e) { console.error("Failed to read keywords file", config.keywordsFile, e); } - }); + }; + + let lastMtimeMs = 0; + setInterval(async () => { + const stat = await fs.promises.stat(config.keywordsFile); + if (lastMtimeMs === stat.mtimeMs) { + return; + } + + if (lastMtimeMs === 0 && stat.mtimeMs !== 0) { + lastMtimeMs = stat.mtimeMs; + return; + } + + lastMtimeMs = stat.mtimeMs; + + console.log("[watch-file] Keywords file was updated. Reloading keywords configuration..."); + await reloadConfig(); + }, config.watchFileIntervalSeconds * 1000); } } diff --git a/src/metrics/dialogs.ts b/src/metrics/dialogs.ts index 47f20ea..b73349c 100644 --- a/src/metrics/dialogs.ts +++ b/src/metrics/dialogs.ts @@ -50,7 +50,6 @@ export function collectDialogMetrics(tg: TelegramClient, registry: Registry) { for (const d of await dialogs.get()) { if (d.peer.type !== "chat") continue; if (d.peer.membersCount === null) continue; - members.set({ peerId: d.peer.id, }, d.peer.membersCount); @@ -103,12 +102,17 @@ class DialogsHolder { this.isUpdating = true; this.dialogs = []; const end = this.dialogsIterDurationHistogram.startTimer(); - for await (const d of this.tg.iterDialogs()) { - if (!peersConfigBoolFilter(config, d.peer.id)) { - continue; + try { + for await (const d of this.tg.iterDialogs()) { + if (!peersConfigBoolFilter(config, d.peer.id)) { + continue; + } + this.dialogs.push(d); } - this.dialogs.push(d); + } catch (e) { + console.error("Failed to iterate over telegram dialogs:", e); } + this.dialogsIterDurationSummary.observe(end()); this.lastUpdate = process.hrtime.bigint(); this.isUpdating = false; diff --git a/src/metrics/keywords.ts b/src/metrics/keywords.ts index 6691633..8611802 100644 --- a/src/metrics/keywords.ts +++ b/src/metrics/keywords.ts @@ -15,7 +15,7 @@ export interface RawKeywordPattern { global: boolean; multi_line: boolean; insensitive: boolean; - } + }; } export type RawKeywordLike = string | RawKeywordPattern; diff --git a/src/server.ts b/src/server.ts index be652db..b56e2be 100644 --- a/src/server.ts +++ b/src/server.ts @@ -24,7 +24,7 @@ export default class MetricsServer { private async _requestHandler(req: http.IncomingMessage, res: http.ServerResponse) { const url = new URL(`http://${req.headers.host ?? "localhost"}${req.url}`); - console.log(`[HTTP] ${req.method} - ${url.href} from ${req.socket.remoteAddress}:${req.socket.remotePort}`); + console.log(`[HTTP] ${req.method} - ${req.socket.localAddress}:${req.socket.localPort} (${url.href}) from ${req.socket.remoteAddress}:${req.socket.remotePort}`); if (req.method === "GET" && url.pathname === "/metrics") { try {