├── LICENSE ├── README.md ├── aiogram_logging ├── __init__.py ├── logger.py └── sender.py ├── grafana-dashboard.json └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 dkeysil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aiogram-logger 2 | --- 3 | ### Simplifies sending logs from your bots to DB. 4 | ![example](https://i.imgur.com/gFfQDmD.png) 5 | --- 6 | ## Quick start with InfluxDB + Grafana 7 | Install package from pip 8 | ``` 9 | pip install aiogram_logging 10 | ``` 11 | 12 | Prepare InlfuxDB and Grafana with this [repo](https://github.com/DKeysil/influxdb-grafana-docker-compose). 13 | 14 | Import and create instances 15 | ```python 16 | from aiogram_logging import Logger, InfluxSender 17 | 18 | sender = InfluxSender(host='localhost', 19 | db='db-name', 20 | username='db-user', 21 | password='db-password') 22 | logger = Logger(sender) 23 | ``` 24 | 25 | Create StatMiddleware to logging every incoming message 26 | ```python 27 | class StatMiddleware(BaseMiddleware): 28 | 29 | def __init__(self): 30 | super(StatMiddleware, self).__init__() 31 | 32 | async def on_process_message(self, message: types.Message, data: dict): 33 | await logger.write_logs(self._manager.bot.id, message, parse_text=True) 34 | 35 | 36 | dp.middleware.setup(StatMiddleware()) 37 | ``` 38 | 39 | Create dashboard by yourself or import from `grafana-dashboard.json` 40 | 41 | Yeah, you can connect several bots for one InfluxDB 42 | ## TODO: 43 | 1. Explain how to manage logs from several bots in Grafana 44 | 2. Parse more different data -------------------------------------------------------------------------------- /aiogram_logging/__init__.py: -------------------------------------------------------------------------------- 1 | from .logger import Logger 2 | from .sender import Sender, InfluxSender 3 | 4 | __all__ = ['Logger', 'Sender', 'InfluxSender'] 5 | -------------------------------------------------------------------------------- /aiogram_logging/logger.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from datetime import datetime 3 | 4 | 5 | class Logger: 6 | """ 7 | Class parses data from telegram bot 8 | then send it to storage 9 | """ 10 | def __init__(self, sender_class=None) -> None: 11 | self.sender_class = sender_class 12 | 13 | def parse_data(self, bot_id: int, 14 | message: types.Message, 15 | parse_text) -> dict: 16 | data = { 17 | 'is_command': message.is_command(), 18 | 'bot_id': bot_id, 19 | 'datetime': datetime.utcnow(), 20 | 'message_type': 'command' if message.is_command() else 'text', 21 | 'user_id': message.from_user.id 22 | } 23 | 24 | if message.content_type == types.ContentType.TEXT: 25 | data.update({ 26 | 'text': message.text 27 | if message.is_command() or parse_text 28 | else None 29 | }) 30 | elif message.content_type == types.ContentType.PHOTO and parse_text: 31 | data.update({ 32 | 'text': message.caption 33 | if message.caption 34 | else None 35 | }) 36 | 37 | return data 38 | 39 | async def write_logs(self, bot_id: int, 40 | message: types.Message, 41 | parse_text=False) -> None: 42 | """ 43 | Pass data to sender class 44 | 45 | Args: 46 | bot_id (int): bot id (self._manager.bot.id) 47 | message (types.Message): 48 | parse_text (bool, optional): pass text to db. Defaults to False. 49 | """ 50 | data = self.parse_data(bot_id, message, parse_text) 51 | await self.sender_class.write_message(data) 52 | -------------------------------------------------------------------------------- /aiogram_logging/sender.py: -------------------------------------------------------------------------------- 1 | from aioinflux import InfluxDBClient, InfluxDBWriteError 2 | import logging 3 | 4 | 5 | class Sender: 6 | """ 7 | Interface for sender class 8 | """ 9 | def __init__(self) -> None: 10 | pass 11 | 12 | async def write_message(self, data: dict) -> None: 13 | """ 14 | Upload message to db 15 | 16 | Args: 17 | data (dict): data from logger class 18 | """ 19 | pass 20 | 21 | 22 | class InfluxSender(Sender): 23 | def __init__(self, 24 | host: str, 25 | db: str, 26 | username: str, 27 | password: str) -> None: 28 | self.host = host 29 | self.db = db 30 | self.username = username 31 | self.password = password 32 | self.client = InfluxDBClient(host=host, 33 | db=db, 34 | username=username, 35 | password=password, 36 | mode='blocking') 37 | 38 | try: 39 | self.client.ping() 40 | except InfluxDBWriteError as ex: 41 | logging.error(f'InfluxDB init error: {str(ex)}') 42 | else: 43 | self.client.mode = 'async' 44 | 45 | def parse_data(self, data: dict) -> dict: 46 | _data = { 47 | 'measurement': 'bot', 48 | 'time': data.get('datetime'), 49 | 'fields': { 50 | "event": 1 51 | }, 52 | 'tags': { 53 | 'user': str(data.get('user_id')), 54 | 'bot_id': str(data.get('bot_id')) 55 | } 56 | } 57 | 58 | if data.get('is_command'): 59 | _data['tags'].update({ 60 | 'command': data.get('text') 61 | }) 62 | if data.get('text'): 63 | _data['fields'].update({ 64 | 'text': data.get('text') 65 | }) 66 | return _data 67 | 68 | async def write_message(self, data) -> None: 69 | data = self.parse_data(data) 70 | 71 | async with InfluxDBClient(host=self.host, 72 | db=self.db, 73 | username=self.username, 74 | password=self.password) as client: 75 | 76 | response = await client.write(data) 77 | logging.debug(f'write status={response}\n' 78 | f'data={data}') 79 | -------------------------------------------------------------------------------- /grafana-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 1, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "datasource": null, 23 | "fieldConfig": { 24 | "defaults": { 25 | "custom": {}, 26 | "mappings": [], 27 | "thresholds": { 28 | "mode": "absolute", 29 | "steps": [ 30 | { 31 | "color": "green", 32 | "value": null 33 | }, 34 | { 35 | "color": "red", 36 | "value": 80 37 | } 38 | ] 39 | } 40 | }, 41 | "overrides": [] 42 | }, 43 | "gridPos": { 44 | "h": 9, 45 | "w": 12, 46 | "x": 0, 47 | "y": 0 48 | }, 49 | "id": 6, 50 | "options": { 51 | "displayMode": "gradient", 52 | "orientation": "vertical", 53 | "reduceOptions": { 54 | "calcs": [ 55 | "mean" 56 | ], 57 | "fields": "", 58 | "values": false 59 | }, 60 | "showUnfilled": true 61 | }, 62 | "pluginVersion": "7.3.4", 63 | "targets": [ 64 | { 65 | "alias": "$tag_command", 66 | "groupBy": [ 67 | { 68 | "params": [ 69 | "1h" 70 | ], 71 | "type": "time" 72 | }, 73 | { 74 | "params": [ 75 | "command" 76 | ], 77 | "type": "tag" 78 | } 79 | ], 80 | "measurement": "bot", 81 | "orderByTime": "ASC", 82 | "policy": "default", 83 | "query": "SELECT count(\"event\") FROM \"bottest\" WHERE (\"command\" = '/efada') AND $timeFilter GROUP BY time(1m), \"command\"", 84 | "rawQuery": false, 85 | "refId": "A", 86 | "resultFormat": "time_series", 87 | "select": [ 88 | [ 89 | { 90 | "params": [ 91 | "event" 92 | ], 93 | "type": "field" 94 | }, 95 | { 96 | "params": [], 97 | "type": "sum" 98 | } 99 | ] 100 | ], 101 | "tags": [ 102 | { 103 | "key": "command", 104 | "operator": "=", 105 | "value": "/gfdgdfgdg" 106 | }, 107 | { 108 | "condition": "OR", 109 | "key": "command", 110 | "operator": "=", 111 | "value": "/test" 112 | }, 113 | { 114 | "condition": "OR", 115 | "key": "command", 116 | "operator": "=", 117 | "value": "/start" 118 | } 119 | ] 120 | } 121 | ], 122 | "timeFrom": null, 123 | "timeShift": null, 124 | "title": "Count of command uses", 125 | "type": "bargauge" 126 | }, 127 | { 128 | "datasource": null, 129 | "fieldConfig": { 130 | "defaults": { 131 | "custom": { 132 | "align": null, 133 | "filterable": false 134 | }, 135 | "mappings": [], 136 | "thresholds": { 137 | "mode": "absolute", 138 | "steps": [ 139 | { 140 | "color": "green", 141 | "value": null 142 | }, 143 | { 144 | "color": "red", 145 | "value": 80 146 | } 147 | ] 148 | } 149 | }, 150 | "overrides": [] 151 | }, 152 | "gridPos": { 153 | "h": 9, 154 | "w": 12, 155 | "x": 12, 156 | "y": 0 157 | }, 158 | "id": 2, 159 | "options": { 160 | "showHeader": true, 161 | "sortBy": [ 162 | { 163 | "desc": true, 164 | "displayName": "Time" 165 | } 166 | ] 167 | }, 168 | "pluginVersion": "7.3.4", 169 | "targets": [ 170 | { 171 | "groupBy": [ 172 | { 173 | "params": [ 174 | "user" 175 | ], 176 | "type": "tag" 177 | } 178 | ], 179 | "measurement": "bot", 180 | "orderByTime": "ASC", 181 | "policy": "default", 182 | "refId": "A", 183 | "resultFormat": "table", 184 | "select": [ 185 | [ 186 | { 187 | "params": [ 188 | "text" 189 | ], 190 | "type": "field" 191 | } 192 | ] 193 | ], 194 | "tags": [] 195 | } 196 | ], 197 | "timeFrom": null, 198 | "timeShift": null, 199 | "title": "Messages table", 200 | "type": "table" 201 | }, 202 | { 203 | "datasource": null, 204 | "description": "Количество сообщений в минуту", 205 | "fieldConfig": { 206 | "defaults": { 207 | "custom": {}, 208 | "mappings": [], 209 | "thresholds": { 210 | "mode": "absolute", 211 | "steps": [ 212 | { 213 | "color": "green", 214 | "value": null 215 | }, 216 | { 217 | "color": "red", 218 | "value": 80 219 | } 220 | ] 221 | } 222 | }, 223 | "overrides": [] 224 | }, 225 | "gridPos": { 226 | "h": 9, 227 | "w": 12, 228 | "x": 0, 229 | "y": 9 230 | }, 231 | "id": 4, 232 | "options": { 233 | "colorMode": "value", 234 | "graphMode": "area", 235 | "justifyMode": "auto", 236 | "orientation": "auto", 237 | "reduceOptions": { 238 | "calcs": [ 239 | "mean" 240 | ], 241 | "fields": "", 242 | "values": false 243 | }, 244 | "textMode": "auto" 245 | }, 246 | "pluginVersion": "7.3.4", 247 | "targets": [ 248 | { 249 | "groupBy": [ 250 | { 251 | "params": [ 252 | "1h" 253 | ], 254 | "type": "time" 255 | } 256 | ], 257 | "measurement": "bot", 258 | "orderByTime": "ASC", 259 | "policy": "default", 260 | "refId": "A", 261 | "resultFormat": "time_series", 262 | "select": [ 263 | [ 264 | { 265 | "params": [ 266 | "event" 267 | ], 268 | "type": "field" 269 | }, 270 | { 271 | "params": [], 272 | "type": "sum" 273 | } 274 | ] 275 | ], 276 | "tags": [] 277 | } 278 | ], 279 | "timeFrom": null, 280 | "timeShift": null, 281 | "title": "Count of bot uses", 282 | "type": "stat" 283 | }, 284 | { 285 | "datasource": null, 286 | "fieldConfig": { 287 | "defaults": { 288 | "custom": { 289 | "align": null, 290 | "filterable": false 291 | }, 292 | "mappings": [], 293 | "thresholds": { 294 | "mode": "absolute", 295 | "steps": [ 296 | { 297 | "color": "green", 298 | "value": null 299 | }, 300 | { 301 | "color": "red", 302 | "value": 80 303 | } 304 | ] 305 | } 306 | }, 307 | "overrides": [] 308 | }, 309 | "gridPos": { 310 | "h": 9, 311 | "w": 12, 312 | "x": 12, 313 | "y": 9 314 | }, 315 | "id": 8, 316 | "options": { 317 | "showHeader": true, 318 | "sortBy": [ 319 | { 320 | "desc": true, 321 | "displayName": "sum" 322 | } 323 | ] 324 | }, 325 | "pluginVersion": "7.3.4", 326 | "targets": [ 327 | { 328 | "alias": "$tag_user", 329 | "groupBy": [ 330 | { 331 | "params": [ 332 | "24h" 333 | ], 334 | "type": "time" 335 | }, 336 | { 337 | "params": [ 338 | "user" 339 | ], 340 | "type": "tag" 341 | } 342 | ], 343 | "measurement": "bot", 344 | "orderByTime": "ASC", 345 | "policy": "default", 346 | "refId": "A", 347 | "resultFormat": "table", 348 | "select": [ 349 | [ 350 | { 351 | "params": [ 352 | "event" 353 | ], 354 | "type": "field" 355 | }, 356 | { 357 | "params": [], 358 | "type": "sum" 359 | } 360 | ] 361 | ], 362 | "tags": [] 363 | } 364 | ], 365 | "timeFrom": null, 366 | "timeShift": null, 367 | "title": "Count of calls by user", 368 | "type": "table" 369 | } 370 | ], 371 | "refresh": "5s", 372 | "schemaVersion": 26, 373 | "style": "dark", 374 | "tags": [], 375 | "templating": { 376 | "list": [] 377 | }, 378 | "time": { 379 | "from": "now-1h", 380 | "to": "now" 381 | }, 382 | "timepicker": {}, 383 | "timezone": "", 384 | "title": "Telegram stat", 385 | "uid": "pZUK54wGk", 386 | "version": 2 387 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from os import path 4 | 5 | from setuptools import setup, find_packages 6 | 7 | this_directory = path.abspath(path.dirname(__file__)) 8 | with open("README.md", "r", encoding="utf-8") as fh: 9 | long_description = fh.read() 10 | 11 | setup( 12 | name='aiogram-logging', 13 | description='Simplifies sending logs from your bots to DB.', 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | version='0.0.1', 17 | url='https://github.com/dkeysil/aiogram-logging', 18 | author='Dmitry Keysil', 19 | author_email='kl0opa.11@gmail.com', 20 | license='MIT', 21 | classifiers=[ 22 | 'Operating System :: OS Independent', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Programming Language :: Python :: 3' 26 | ], 27 | packages=find_packages(include=['aiogram_logging', 'aiogram_logging.*']), 28 | install_requires=[ 29 | 'aiogram<3', 30 | 'aioinflux', 31 | ], 32 | python_requires=">=3.6", 33 | ) --------------------------------------------------------------------------------