fix lint issues; use polling instead of fs.watchFile for keywords reloading; add error handling in dialogs collector

This commit is contained in:
soffee 2026-01-27 01:41:53 +03:00
parent f6af162ce6
commit 10f430592d
6 changed files with 51 additions and 23 deletions

View file

@ -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": {

View file

@ -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<RawKeywordLike[]>
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<RawKeywordLike[]>
global: true,
multi_line: false,
insensitive: true,
}
},
};
if (typeof item.flags === "object") {

View file

@ -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);
}
}

View file

@ -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();
try {
for await (const d of this.tg.iterDialogs()) {
if (!peersConfigBoolFilter(config, d.peer.id)) {
continue;
}
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;

View file

@ -15,7 +15,7 @@ export interface RawKeywordPattern {
global: boolean;
multi_line: boolean;
insensitive: boolean;
}
};
}
export type RawKeywordLike = string | RawKeywordPattern;

View file

@ -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 {