initial commit or smth
This commit is contained in:
parent
05a1bf047d
commit
98ae9d2e59
11 changed files with 410 additions and 30 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,4 +6,5 @@ dist/
|
|||
.vscode
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
.env
|
||||
.env
|
||||
keywords.yml
|
||||
45
README.md
45
README.md
|
|
@ -1,6 +1,49 @@
|
|||
# mtproto_exporter
|
||||
|
||||
mtcute powered Telegram bot
|
||||
mtcute powered Prometheus metrics exporter
|
||||
|
||||
*this exporter is mostly useful only with userbot*
|
||||
|
||||
## Available Metrics
|
||||
`messenger_dialog_info{peerId, peerType, displayName}`
|
||||
|
||||
Dialog information exposed as labels
|
||||
|
||||
`messenger_dialog_messages_count{peerId}`
|
||||
|
||||
Messages count since exporter startup
|
||||
|
||||
`messenger_dialog_unread_messages_count{peerId}`
|
||||
|
||||
Number of unread messages in dialogs
|
||||
|
||||
`messenger_dialog_keywords_count{peerId}`
|
||||
|
||||
Number of keywords found in messages since exporter startup
|
||||
|
||||
`messenger_dialog_words_count{peerId}`
|
||||
|
||||
Number of words in messages since exporter startup
|
||||
|
||||
This metric is disabled by default because it will produce a lot of unique time series (more info [here](https://prometheus.io/docs/practices/naming/#labels))
|
||||
|
||||
This will expose each **word** in each **message** in each **chat** as unique metric.
|
||||
|
||||
This metric can be enabled with command line flag `--words-counter`
|
||||
|
||||
## CLI Options
|
||||
`--bind-host`, `-b` - ip address where http server will be listening on
|
||||
|
||||
`--port`, `-p` - port where http server will be listening on
|
||||
|
||||
`--words-counter` - enable each word counting metric
|
||||
|
||||
`--keywords-file`, `-k` - path to yaml file with keywords and patterns (see [keywords.yml.example](./keywords.yml.example))
|
||||
|
||||
## Environment Variables
|
||||
|
||||
`API_ID` - Telegram api id used for mtproto connection (see [mtcute.dev](https://mtcute.dev/guide/intro/sign-in.html))
|
||||
`API_HASH` - Telegram api hash used for mtproto connection (see [mtcute.dev](https://mtcute.dev/guide/intro/sign-in.html))
|
||||
|
||||
## Development
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import antfu from '@antfu/eslint-config'
|
||||
import antfu from "@antfu/eslint-config";
|
||||
|
||||
export default antfu({
|
||||
stylistic: {
|
||||
indent: 4,
|
||||
semi: true,
|
||||
quotes: "double",
|
||||
},
|
||||
typescript: true,
|
||||
yaml: false,
|
||||
rules: {
|
||||
'curly': ['error', 'multi-line'],
|
||||
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
'style/quotes': ['error', 'single', { avoidEscape: true }],
|
||||
'import/order': ['error', { 'newlines-between': 'always' }],
|
||||
'antfu/if-newline': 'off',
|
||||
'style/max-statements-per-line': ['error', { max: 2 }],
|
||||
'no-console': 'off',
|
||||
'antfu/no-top-level-await': 'off',
|
||||
"curly": ["error", "multi-line"],
|
||||
"style/brace-style": ["error", "1tbs", { allowSingleLine: true }],
|
||||
// "import/order": ["error", { "newlines-between": "always" }], this shit breaks eslint
|
||||
"antfu/if-newline": "off",
|
||||
"style/max-statements-per-line": ["error", { max: 2 }],
|
||||
"no-console": "off",
|
||||
"antfu/no-top-level-await": "off",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
|||
4
keywords.yml.example
Normal file
4
keywords.yml.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
keywords:
|
||||
- meow
|
||||
- name: woof
|
||||
pattern: 'w[oa]+f'
|
||||
|
|
@ -7,16 +7,21 @@
|
|||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"start": "dotenv tsx ./src/main.ts",
|
||||
"start": "dotenv tsx ./src/main.ts --",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mtcute/dispatcher": "^0.22.2",
|
||||
"@mtcute/node": "^0.22.3",
|
||||
"dotenv-cli": "^8.0.0"
|
||||
"command-line-args": "^6.0.1",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prom-client": "^15.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^4.11.0",
|
||||
"@types/command-line-args": "^5.2.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.14.0",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3"
|
||||
|
|
|
|||
96
pnpm-lock.yaml
generated
96
pnpm-lock.yaml
generated
|
|
@ -14,9 +14,24 @@ importers:
|
|||
'@mtcute/node':
|
||||
specifier: ^0.22.3
|
||||
version: 0.22.3
|
||||
'@types/command-line-args':
|
||||
specifier: ^5.2.3
|
||||
version: 5.2.3
|
||||
'@types/js-yaml':
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9
|
||||
command-line-args:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
dotenv-cli:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
prom-client:
|
||||
specifier: ^15.1.3
|
||||
version: 15.1.3
|
||||
devDependencies:
|
||||
'@antfu/eslint-config':
|
||||
specifier: ^4.11.0
|
||||
|
|
@ -421,6 +436,10 @@ packages:
|
|||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@opentelemetry/api@1.9.0':
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@pkgr/core@0.1.2':
|
||||
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
|
@ -438,6 +457,9 @@ packages:
|
|||
'@tybys/wasm-util@0.9.0':
|
||||
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
|
||||
|
||||
'@types/command-line-args@5.2.3':
|
||||
resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
|
|
@ -453,6 +475,9 @@ packages:
|
|||
'@types/events@3.0.0':
|
||||
resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==}
|
||||
|
||||
'@types/js-yaml@4.0.9':
|
||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
|
|
@ -649,6 +674,10 @@ packages:
|
|||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
array-back@6.2.2:
|
||||
resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==}
|
||||
engines: {node: '>=12.17'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
|
|
@ -661,6 +690,9 @@ packages:
|
|||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bintrees@1.0.2:
|
||||
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
|
|
@ -728,6 +760,15 @@ packages:
|
|||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
command-line-args@6.0.1:
|
||||
resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==}
|
||||
engines: {node: '>=12.20'}
|
||||
peerDependencies:
|
||||
'@75lb/nature': latest
|
||||
peerDependenciesMeta:
|
||||
'@75lb/nature':
|
||||
optional: true
|
||||
|
||||
comment-parser@1.4.1:
|
||||
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
|
@ -1097,6 +1138,15 @@ packages:
|
|||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
find-replace@5.0.2:
|
||||
resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@75lb/nature': latest
|
||||
peerDependenciesMeta:
|
||||
'@75lb/nature':
|
||||
optional: true
|
||||
|
||||
find-up-simple@1.0.1:
|
||||
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1271,6 +1321,9 @@ packages:
|
|||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lodash.camelcase@4.3.0:
|
||||
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
|
|
@ -1566,6 +1619,10 @@ packages:
|
|||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prom-client@15.1.3:
|
||||
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
|
||||
engines: {node: ^16 || ^18 || >=20}
|
||||
|
||||
pump@3.0.2:
|
||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||
|
||||
|
|
@ -1726,6 +1783,9 @@ packages:
|
|||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tdigest@0.1.2:
|
||||
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
|
||||
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
|
|
@ -1771,6 +1831,10 @@ packages:
|
|||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
typical@7.3.0:
|
||||
resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==}
|
||||
engines: {node: '>=12.17'}
|
||||
|
||||
ufo@1.5.4:
|
||||
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
||||
|
||||
|
|
@ -2217,6 +2281,8 @@ snapshots:
|
|||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.19.1
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@pkgr/core@0.1.2': {}
|
||||
|
||||
'@pkgr/core@0.2.0': {}
|
||||
|
|
@ -2238,6 +2304,8 @@ snapshots:
|
|||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/command-line-args@5.2.3': {}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
|
@ -2253,6 +2321,8 @@ snapshots:
|
|||
|
||||
'@types/events@3.0.0': {}
|
||||
|
||||
'@types/js-yaml@4.0.9': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
|
|
@ -2455,6 +2525,8 @@ snapshots:
|
|||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
array-back@6.2.2: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
|
@ -2468,6 +2540,8 @@ snapshots:
|
|||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
bintrees@1.0.2: {}
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
|
|
@ -2532,6 +2606,13 @@ snapshots:
|
|||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
command-line-args@6.0.1:
|
||||
dependencies:
|
||||
array-back: 6.2.2
|
||||
find-replace: 5.0.2
|
||||
lodash.camelcase: 4.3.0
|
||||
typical: 7.3.0
|
||||
|
||||
comment-parser@1.4.1: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
|
@ -2989,6 +3070,8 @@ snapshots:
|
|||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
find-replace@5.0.2: {}
|
||||
|
||||
find-up-simple@1.0.1: {}
|
||||
|
||||
find-up@5.0.0:
|
||||
|
|
@ -3132,6 +3215,8 @@ snapshots:
|
|||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
lodash.camelcase@4.3.0: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
|
@ -3603,6 +3688,11 @@ snapshots:
|
|||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prom-client@15.1.3:
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
tdigest: 0.1.2
|
||||
|
||||
pump@3.0.2:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
|
|
@ -3768,6 +3858,10 @@ snapshots:
|
|||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
tdigest@0.1.2:
|
||||
dependencies:
|
||||
bintrees: 1.0.2
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyglobby@0.2.12:
|
||||
|
|
@ -3808,6 +3902,8 @@ snapshots:
|
|||
|
||||
typescript@5.8.3: {}
|
||||
|
||||
typical@7.3.0: {}
|
||||
|
||||
ufo@1.5.4: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
|
|
|||
10
src/env.ts
10
src/env.ts
|
|
@ -1,10 +1,10 @@
|
|||
import process from 'node:process'
|
||||
import process from "node:process";
|
||||
|
||||
const API_ID = Number.parseInt(process.env.API_ID!)
|
||||
const API_HASH = process.env.API_HASH!
|
||||
const API_ID = Number.parseInt(process.env.API_ID!);
|
||||
const API_HASH = process.env.API_HASH!;
|
||||
|
||||
if (Number.isNaN(API_ID) || !API_HASH) {
|
||||
throw new Error('API_ID or API_HASH not set!')
|
||||
throw new Error("API_ID or API_HASH not set!");
|
||||
}
|
||||
|
||||
export { API_HASH, API_ID }
|
||||
export { API_HASH, API_ID };
|
||||
|
|
|
|||
61
src/keywords.ts
Normal file
61
src/keywords.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { Dispatcher } from "@mtcute/dispatcher";
|
||||
import { PropagationAction } from "@mtcute/dispatcher";
|
||||
import { Counter } from "prom-client";
|
||||
|
||||
interface KeywordPattern {
|
||||
name: string;
|
||||
pattern: RegExp;
|
||||
}
|
||||
|
||||
export type KeywordLike = string | KeywordPattern;
|
||||
|
||||
export function newWordsCounter(dp: Dispatcher) {
|
||||
const counter = new Counter({
|
||||
name: "messenger_dialog_words_count",
|
||||
help: "Number of words in messages since exporter startup",
|
||||
labelNames: ["peerId", "word"],
|
||||
});
|
||||
dp.onNewMessage(async (msg) => {
|
||||
const words = msg.text.toLowerCase().split(" ");
|
||||
for (const w of words) {
|
||||
counter.inc({
|
||||
peerId: msg.chat.id,
|
||||
word: w,
|
||||
});
|
||||
}
|
||||
return PropagationAction.Continue;
|
||||
});
|
||||
return counter;
|
||||
}
|
||||
|
||||
export function newKeywordsCounter(dp: Dispatcher, keywords: KeywordLike[]) {
|
||||
const counter = new Counter({
|
||||
name: "messenger_dialog_keywords_count",
|
||||
help: "Number of keywords found in messages since exporter startup",
|
||||
labelNames: ["peerId", "keyword"],
|
||||
});
|
||||
dp.onNewMessage(async (msg) => {
|
||||
for (const kw of keywords) {
|
||||
let count;
|
||||
let kwname;
|
||||
if (typeof kw === "string") {
|
||||
const words = msg.text.toLowerCase().split(" ");
|
||||
count = words.filter(w => w === kw).length;
|
||||
kwname = kw;
|
||||
} else {
|
||||
count = (msg.text.match(kw.pattern) || []).length;
|
||||
kwname = kw.name;
|
||||
}
|
||||
if (count === 0) {
|
||||
continue;
|
||||
}
|
||||
counter.inc({
|
||||
peerId: msg.chat.id,
|
||||
keyword: kwname,
|
||||
}, count);
|
||||
}
|
||||
return PropagationAction.Continue;
|
||||
});
|
||||
|
||||
return counter;
|
||||
}
|
||||
77
src/main.ts
77
src/main.ts
|
|
@ -1,19 +1,74 @@
|
|||
import { Dispatcher, filters } from '@mtcute/dispatcher'
|
||||
import { TelegramClient } from '@mtcute/node'
|
||||
import type { OptionDefinition } from "command-line-args";
|
||||
import type { KeywordLike } from "./keywords.js";
|
||||
import fs from "node:fs";
|
||||
import { Dispatcher } from "@mtcute/dispatcher";
|
||||
import { TelegramClient } from "@mtcute/node";
|
||||
import cmdline from "command-line-args";
|
||||
import yaml from "js-yaml";
|
||||
import { collectDefaultMetrics, Registry } from "prom-client";
|
||||
|
||||
import * as env from './env.js'
|
||||
import * as env from "./env.js";
|
||||
import * as metrics from "./metrics.js";
|
||||
import MetricsServer from "./server.js";
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
const cli = cmdline(optionDefinitions);
|
||||
|
||||
const keywords: KeywordLike[] = [];
|
||||
if (cli["keywords-file"]) {
|
||||
if (!fs.existsSync(cli["keywords-file"])) {
|
||||
throw new Error("--keywords-file set, but file does not exist.");
|
||||
}
|
||||
const doc = yaml.load(fs.readFileSync(cli["keywords-file"], "utf8")) as { keywords?: any[] };
|
||||
|
||||
if (doc.keywords && doc.keywords.constructor.name === "Array") {
|
||||
for (const item of doc.keywords) {
|
||||
if (typeof item === "string") {
|
||||
keywords.push(item);
|
||||
} else if (typeof item === "object" && item.name && item.pattern) {
|
||||
keywords.push({
|
||||
name: item.name,
|
||||
pattern: new RegExp(item.pattern, "gi"),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("Keywords file format error: no 'keywords' property, or not an array.");
|
||||
}
|
||||
}
|
||||
|
||||
const registry = new Registry();
|
||||
|
||||
collectDefaultMetrics({ register: registry });
|
||||
|
||||
const server = new MetricsServer(registry);
|
||||
server.listen(cli["bind-host"], cli.port);
|
||||
|
||||
const tg = new TelegramClient({
|
||||
apiId: env.API_ID,
|
||||
apiHash: env.API_HASH,
|
||||
storage: 'bot-data/session',
|
||||
})
|
||||
storage: "bot-data/session",
|
||||
});
|
||||
|
||||
const dp = Dispatcher.for(tg)
|
||||
const dp = Dispatcher.for(tg);
|
||||
|
||||
dp.onNewMessage(filters.start, async (msg) => {
|
||||
await msg.answerText('Hello, world!')
|
||||
})
|
||||
registry.registerMetric(metrics.newStaticPeerInfoGauge(tg));
|
||||
registry.registerMetric(metrics.newUnreadCountGauge(tg));
|
||||
registry.registerMetric(metrics.newMessagesCounter(dp));
|
||||
|
||||
const user = await tg.start()
|
||||
console.log('Logged in as', user.username)
|
||||
if (keywords.length > 0) {
|
||||
registry.registerMetric(metrics.newKeywordsCounter(dp, keywords));
|
||||
}
|
||||
|
||||
if (cli["words-counter"]) {
|
||||
registry.registerMetric(metrics.newWordsCounter(dp));
|
||||
}
|
||||
|
||||
const user = await tg.start();
|
||||
console.log("Logged in as", user.username);
|
||||
|
|
|
|||
63
src/metrics.ts
Normal file
63
src/metrics.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type { Dispatcher } from "@mtcute/dispatcher";
|
||||
import type { TelegramClient } from "@mtcute/node";
|
||||
import { PropagationAction } from "@mtcute/dispatcher";
|
||||
import { Counter, Gauge } from "prom-client";
|
||||
|
||||
import { newKeywordsCounter, newWordsCounter } from "./keywords.js";
|
||||
|
||||
function newMessagesCounter(dp: Dispatcher) {
|
||||
const counter = new Counter({
|
||||
name: "messenger_dialog_messages_count",
|
||||
help: "Messages count since exporter startup",
|
||||
labelNames: ["peerId"],
|
||||
});
|
||||
dp.onNewMessage(async (msg) => {
|
||||
counter.inc({
|
||||
peerId: msg.chat.id,
|
||||
});
|
||||
return PropagationAction.Continue;
|
||||
});
|
||||
return counter;
|
||||
}
|
||||
|
||||
function newStaticPeerInfoGauge(tg: TelegramClient) {
|
||||
const gauge = new Gauge({
|
||||
name: "messenger_dialog_info",
|
||||
help: "Dialog information exposed as labels",
|
||||
labelNames: ["peerId", "peerType", "displayName"],
|
||||
collect: async () => {
|
||||
for await (const d of tg.iterDialogs()) {
|
||||
gauge.set({
|
||||
peerId: d.peer.id,
|
||||
peerType: d.peer.type,
|
||||
displayName: d.peer.displayName,
|
||||
}, 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
return gauge;
|
||||
}
|
||||
|
||||
function newUnreadCountGauge(tg: TelegramClient) {
|
||||
const gauge = new Gauge({
|
||||
name: "messenger_dialog_unread_messages_count",
|
||||
help: "Number of unread messages in dialogs",
|
||||
labelNames: ["peerId"],
|
||||
collect: async () => {
|
||||
for await (const d of tg.iterDialogs()) {
|
||||
gauge.set({
|
||||
peerId: d.peer.id,
|
||||
}, d.unreadCount);
|
||||
}
|
||||
},
|
||||
});
|
||||
return gauge;
|
||||
}
|
||||
|
||||
export {
|
||||
newKeywordsCounter,
|
||||
newMessagesCounter,
|
||||
newStaticPeerInfoGauge,
|
||||
newUnreadCountGauge,
|
||||
newWordsCounter,
|
||||
};
|
||||
51
src/server.ts
Normal file
51
src/server.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import type { Registry } from "prom-client";
|
||||
import http from "node:http";
|
||||
|
||||
export default class MetricsServer {
|
||||
private _registry: Registry;
|
||||
private _httpServer?: http.Server;
|
||||
|
||||
public constructor(registry: Registry) {
|
||||
this._registry = registry;
|
||||
}
|
||||
|
||||
public get registry() {
|
||||
return this._registry;
|
||||
}
|
||||
|
||||
public listen(address: string, port: number) {
|
||||
if (this._httpServer) {
|
||||
throw new Error("This server is already listening");
|
||||
}
|
||||
this._httpServer = http.createServer(this._requestHandler.bind(this));
|
||||
this._httpServer.listen(port, address);
|
||||
console.log(`HTTP Metrics Server is listening on ${address}:${port}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/metrics") {
|
||||
try {
|
||||
const metrics = await this._registry.metrics();
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", this._registry.contentType);
|
||||
res.write(metrics);
|
||||
} catch (e) {
|
||||
console.error("Metrics collection failed:", e);
|
||||
res.statusCode = 500;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8;");
|
||||
res.write("{ \"error\":{\"message\":\"failed to collect metrics.\"}}");
|
||||
}
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (!res.writableEnded) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8;");
|
||||
res.write("{\"error\":{\"message\":\"not found.\"}}");
|
||||
return res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue