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
|
.vscode
|
||||||
*.log
|
*.log
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.env
|
.env
|
||||||
|
keywords.yml
|
||||||
45
README.md
45
README.md
|
|
@ -1,6 +1,49 @@
|
||||||
# mtproto_exporter
|
# 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
|
## Development
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import antfu from '@antfu/eslint-config'
|
import antfu from "@antfu/eslint-config";
|
||||||
|
|
||||||
export default antfu({
|
export default antfu({
|
||||||
stylistic: {
|
stylistic: {
|
||||||
indent: 4,
|
indent: 4,
|
||||||
|
semi: true,
|
||||||
|
quotes: "double",
|
||||||
},
|
},
|
||||||
typescript: true,
|
typescript: true,
|
||||||
yaml: false,
|
yaml: false,
|
||||||
rules: {
|
rules: {
|
||||||
'curly': ['error', 'multi-line'],
|
"curly": ["error", "multi-line"],
|
||||||
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
"style/brace-style": ["error", "1tbs", { allowSingleLine: true }],
|
||||||
'style/quotes': ['error', 'single', { avoidEscape: true }],
|
// "import/order": ["error", { "newlines-between": "always" }], this shit breaks eslint
|
||||||
'import/order': ['error', { 'newlines-between': 'always' }],
|
"antfu/if-newline": "off",
|
||||||
'antfu/if-newline': 'off',
|
"style/max-statements-per-line": ["error", { max: 2 }],
|
||||||
'style/max-statements-per-line': ['error', { max: 2 }],
|
"no-console": "off",
|
||||||
'no-console': 'off',
|
"antfu/no-top-level-await": "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": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint --fix .",
|
"lint:fix": "eslint --fix .",
|
||||||
"start": "dotenv tsx ./src/main.ts",
|
"start": "dotenv tsx ./src/main.ts --",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mtcute/dispatcher": "^0.22.2",
|
"@mtcute/dispatcher": "^0.22.2",
|
||||||
"@mtcute/node": "^0.22.3",
|
"@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": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^4.11.0",
|
"@antfu/eslint-config": "^4.11.0",
|
||||||
|
"@types/command-line-args": "^5.2.3",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
|
|
|
||||||
96
pnpm-lock.yaml
generated
96
pnpm-lock.yaml
generated
|
|
@ -14,9 +14,24 @@ importers:
|
||||||
'@mtcute/node':
|
'@mtcute/node':
|
||||||
specifier: ^0.22.3
|
specifier: ^0.22.3
|
||||||
version: 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:
|
dotenv-cli:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 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:
|
devDependencies:
|
||||||
'@antfu/eslint-config':
|
'@antfu/eslint-config':
|
||||||
specifier: ^4.11.0
|
specifier: ^4.11.0
|
||||||
|
|
@ -421,6 +436,10 @@ packages:
|
||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
engines: {node: '>= 8'}
|
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':
|
'@pkgr/core@0.1.2':
|
||||||
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
|
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
@ -438,6 +457,9 @@ packages:
|
||||||
'@tybys/wasm-util@0.9.0':
|
'@tybys/wasm-util@0.9.0':
|
||||||
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
|
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':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
|
|
@ -453,6 +475,9 @@ packages:
|
||||||
'@types/events@3.0.0':
|
'@types/events@3.0.0':
|
||||||
resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==}
|
resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==}
|
||||||
|
|
||||||
|
'@types/js-yaml@4.0.9':
|
||||||
|
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
|
@ -649,6 +674,10 @@ packages:
|
||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
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:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
|
@ -661,6 +690,9 @@ packages:
|
||||||
bindings@1.5.0:
|
bindings@1.5.0:
|
||||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||||
|
|
||||||
|
bintrees@1.0.2:
|
||||||
|
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
|
|
@ -728,6 +760,15 @@ packages:
|
||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
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:
|
comment-parser@1.4.1:
|
||||||
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
|
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
@ -1097,6 +1138,15 @@ packages:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
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:
|
find-up-simple@1.0.1:
|
||||||
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
|
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -1271,6 +1321,9 @@ packages:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
lodash.camelcase@4.3.0:
|
||||||
|
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||||
|
|
||||||
lodash.merge@4.6.2:
|
lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
|
|
||||||
|
|
@ -1566,6 +1619,10 @@ packages:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
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:
|
pump@3.0.2:
|
||||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||||
|
|
||||||
|
|
@ -1726,6 +1783,9 @@ packages:
|
||||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tdigest@0.1.2:
|
||||||
|
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
|
||||||
|
|
||||||
tinyexec@0.3.2:
|
tinyexec@0.3.2:
|
||||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||||
|
|
||||||
|
|
@ -1771,6 +1831,10 @@ packages:
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
typical@7.3.0:
|
||||||
|
resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==}
|
||||||
|
engines: {node: '>=12.17'}
|
||||||
|
|
||||||
ufo@1.5.4:
|
ufo@1.5.4:
|
||||||
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
||||||
|
|
||||||
|
|
@ -2217,6 +2281,8 @@ snapshots:
|
||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.19.1
|
fastq: 1.19.1
|
||||||
|
|
||||||
|
'@opentelemetry/api@1.9.0': {}
|
||||||
|
|
||||||
'@pkgr/core@0.1.2': {}
|
'@pkgr/core@0.1.2': {}
|
||||||
|
|
||||||
'@pkgr/core@0.2.0': {}
|
'@pkgr/core@0.2.0': {}
|
||||||
|
|
@ -2238,6 +2304,8 @@ snapshots:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/command-line-args@5.2.3': {}
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
|
|
@ -2253,6 +2321,8 @@ snapshots:
|
||||||
|
|
||||||
'@types/events@3.0.0': {}
|
'@types/events@3.0.0': {}
|
||||||
|
|
||||||
|
'@types/js-yaml@4.0.9': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
|
|
@ -2455,6 +2525,8 @@ snapshots:
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
|
array-back@6.2.2: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
@ -2468,6 +2540,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
file-uri-to-path: 1.0.0
|
file-uri-to-path: 1.0.0
|
||||||
|
|
||||||
|
bintrees@1.0.2: {}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 5.7.1
|
buffer: 5.7.1
|
||||||
|
|
@ -2532,6 +2606,13 @@ snapshots:
|
||||||
|
|
||||||
color-name@1.1.4: {}
|
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: {}
|
comment-parser@1.4.1: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
@ -2989,6 +3070,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
|
|
||||||
|
find-replace@5.0.2: {}
|
||||||
|
|
||||||
find-up-simple@1.0.1: {}
|
find-up-simple@1.0.1: {}
|
||||||
|
|
||||||
find-up@5.0.0:
|
find-up@5.0.0:
|
||||||
|
|
@ -3132,6 +3215,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
|
|
||||||
|
lodash.camelcase@4.3.0: {}
|
||||||
|
|
||||||
lodash.merge@4.6.2: {}
|
lodash.merge@4.6.2: {}
|
||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
|
@ -3603,6 +3688,11 @@ snapshots:
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
prom-client@15.1.3:
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
tdigest: 0.1.2
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.4
|
end-of-stream: 1.4.4
|
||||||
|
|
@ -3768,6 +3858,10 @@ snapshots:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
|
tdigest@0.1.2:
|
||||||
|
dependencies:
|
||||||
|
bintrees: 1.0.2
|
||||||
|
|
||||||
tinyexec@0.3.2: {}
|
tinyexec@0.3.2: {}
|
||||||
|
|
||||||
tinyglobby@0.2.12:
|
tinyglobby@0.2.12:
|
||||||
|
|
@ -3808,6 +3902,8 @@ snapshots:
|
||||||
|
|
||||||
typescript@5.8.3: {}
|
typescript@5.8.3: {}
|
||||||
|
|
||||||
|
typical@7.3.0: {}
|
||||||
|
|
||||||
ufo@1.5.4: {}
|
ufo@1.5.4: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
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_ID = Number.parseInt(process.env.API_ID!);
|
||||||
const API_HASH = process.env.API_HASH!
|
const API_HASH = process.env.API_HASH!;
|
||||||
|
|
||||||
if (Number.isNaN(API_ID) || !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 type { OptionDefinition } from "command-line-args";
|
||||||
import { TelegramClient } from '@mtcute/node'
|
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({
|
const tg = new TelegramClient({
|
||||||
apiId: env.API_ID,
|
apiId: env.API_ID,
|
||||||
apiHash: env.API_HASH,
|
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) => {
|
registry.registerMetric(metrics.newStaticPeerInfoGauge(tg));
|
||||||
await msg.answerText('Hello, world!')
|
registry.registerMetric(metrics.newUnreadCountGauge(tg));
|
||||||
})
|
registry.registerMetric(metrics.newMessagesCounter(dp));
|
||||||
|
|
||||||
const user = await tg.start()
|
if (keywords.length > 0) {
|
||||||
console.log('Logged in as', user.username)
|
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