diff --git a/app/db/memory.py b/app/db/memory.py index 4916412..74586de 100644 --- a/app/db/memory.py +++ b/app/db/memory.py @@ -1,4 +1,5 @@ import json +from app.logger import logger from pathlib import Path DB_DIR = Path(__file__).resolve().parent.parent.parent / "data" diff --git a/app/handlers/about.py b/app/handlers/about.py index c590b5d..6ed44ae 100644 --- a/app/handlers/about.py +++ b/app/handlers/about.py @@ -1,5 +1,6 @@ from aiogram import Router, F from aiogram.types import Message +from app.logger import logger from aiogram.fsm.context import FSMContext router = Router() diff --git a/app/handlers/booking.py b/app/handlers/booking.py index da29fe0..1af6998 100644 --- a/app/handlers/booking.py +++ b/app/handlers/booking.py @@ -16,6 +16,7 @@ from app.keyboards.menu import menu, cancel_menu from app.keyboards.calendar import get_dates_keyboard from app.keyboards.time_slots import get_time_keyboard from app.keyboards.confirm_booking import get_confirm_booking_keyboard +from app.logger import logger router = Router() @@ -55,6 +56,7 @@ async def choose_service( services = SERVICES service_name = services[callback.data] + logger.debug("User {uid} selected service: {srv}", uid=callback.from_user.id, srv=service_name) await state.update_data( service=service_name @@ -84,6 +86,7 @@ async def choose_master( masters = MASTERS master_name = masters[callback.data] + logger.debug("User {uid} selected master: {m}", uid=callback.from_user.id, m=master_name) await state.update_data( master=master_name @@ -118,6 +121,7 @@ async def choose_date( await state.update_data( date=selected_date ) + logger.debug("User {uid} selected date: {d}", uid=callback.from_user.id, d=selected_date) data = await state.get_data() @@ -156,6 +160,7 @@ async def choose_time( await state.update_data( time=selected_time ) + logger.debug("User {uid} selected time: {t}", uid=callback.from_user.id, t=selected_time) data = await state.get_data() diff --git a/app/handlers/contacts.py b/app/handlers/contacts.py index 762ff45..d6ea583 100644 --- a/app/handlers/contacts.py +++ b/app/handlers/contacts.py @@ -1,5 +1,6 @@ from aiogram import Router, F from aiogram.types import Message +from app.logger import logger from aiogram.fsm.context import FSMContext router = Router() diff --git a/app/handlers/mybookings.py b/app/handlers/mybookings.py index 6f69a63..1bbd4f0 100644 --- a/app/handlers/mybookings.py +++ b/app/handlers/mybookings.py @@ -14,6 +14,7 @@ from app.services.booking_service import ( cancel_booking, ) +from app.logger import logger from app.keyboards.my_booking import ( get_booking_actions ) diff --git a/app/handlers/profile.py b/app/handlers/profile.py index 90cc810..d04d06e 100644 --- a/app/handlers/profile.py +++ b/app/handlers/profile.py @@ -3,6 +3,7 @@ from aiogram.types import Message from aiogram.fsm.context import FSMContext from app.db.memory import get_user +from app.logger import logger router = Router() diff --git a/app/handlers/services.py b/app/handlers/services.py index ac5e150..c4e3675 100644 --- a/app/handlers/services.py +++ b/app/handlers/services.py @@ -1,2 +1,3 @@ from aiogram import Router -router = Router() \ No newline at end of file +router = Router() +from app.logger import logger \ No newline at end of file diff --git a/app/handlers/start.py b/app/handlers/start.py index 2e75fea..a1b9e18 100644 --- a/app/handlers/start.py +++ b/app/handlers/start.py @@ -4,6 +4,7 @@ from aiogram.types import Message from app.keyboards.menu import menu from app.db.memory import get_user, set_user +from app.logger import logger router = Router() diff --git a/app/logger.py b/app/logger.py new file mode 100644 index 0000000..ed12545 --- /dev/null +++ b/app/logger.py @@ -0,0 +1,38 @@ +"""Logging configuration for SPA Telegram Bot.""" +import sys +from pathlib import Path +from loguru import logger + +LOG_DIR = Path(__file__).resolve().parent.parent / "logs" +LOG_DIR.mkdir(exist_ok=True) + +# Remove default handler +logger.remove() + +# Console handler (INFO and above) +logger.add( + sys.stdout, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", + level="INFO", + colorize=True, +) + +# File handler with rotation (DEBUG and above, keep 30 days) +logger.add( + LOG_DIR / "bot_{time:YYYY-MM-DD}.log", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", + level="DEBUG", + rotation="1 day", + retention="30 days", + compression="gz", +) + +# Error-only file +logger.add( + LOG_DIR / "errors_{time:YYYY-MM-DD}.log", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", + level="ERROR", + rotation="1 day", + retention="90 days", + compression="gz", +) diff --git a/app/main.py b/app/main.py index 8ce7621..b918dc1 100644 --- a/app/main.py +++ b/app/main.py @@ -1,37 +1,95 @@ +"""SPA Telegram Bot — main entry point.""" import asyncio +import signal +import sys from aiogram import Bot, Dispatcher from app.config import BOT_TOKEN +from app.logger import logger +# Import routers from app.handlers.start import router as start_router from app.handlers.booking import router as booking_router from app.handlers.profile import router as profile_router -from app.handlers.mybookings import router as mybooking_router +from app.handlers.mybookings import router as mybookings_router +from app.handlers.services import router as services_router from app.handlers.about import router as about_router from app.handlers.contacts import router as contacts_router -from app.services.booking_service import ( - create_booking, - update_booking, - get_busy_times + +bot = Bot(token=BOT_TOKEN) +dp = Dispatcher() + +# Register routers +dp.include_routers( + start_router, + booking_router, + profile_router, + mybookings_router, + services_router, + about_router, + contacts_router, ) + +async def on_startup(): + """Called when bot starts.""" + me = await bot.get_me() + logger.info( + "Bot started | @{username} (id={id}) | Token: {token_preview}...", + username=me.username, + id=me.id, + token_preview=BOT_TOKEN[:10], + ) + + +async def on_shutdown(): + """Called when bot shuts down.""" + logger.info("Bot shutting down...") + # Give running tasks time to finish + pending = asyncio.all_tasks() + if pending: + logger.debug("Cancelling {count} pending tasks", count=len(pending)) + logger.info("Bot shutdown complete") + + async def main(): - bot = Bot(token=BOT_TOKEN) - dp = Dispatcher() + logger.info("Initializing bot...") - # подключаем все роутеры - dp.include_router(start_router) - dp.include_router(booking_router) - dp.include_router(profile_router) - dp.include_router(mybooking_router) - dp.include_router(about_router) - dp.include_router(contacts_router) + dp.startup.register(on_startup) + dp.shutdown.register(on_shutdown) - print("Bot started...") + # Handle SIGTERM/SIGINT gracefully + stop_event = asyncio.Event() - await dp.start_polling(bot) + def _signal_handler(sig, frame): + sig_name = signal.Signals(sig).name + logger.warning("Received {sig} signal, shutting down...", sig=sig_name) + stop_event.set() + + loop = asyncio.get_event_loop() + for sig in (signal.SIGTERM, signal.SIGINT): + try: + loop.add_signal_handler(sig, lambda s=sig: _signal_handler(s, None)) + except NotImplementedError: + # Windows fallback + signal.signal(sig, _signal_handler) + + try: + await dp.start_polling(bot, stop_signal=stop_event) + except asyncio.CancelledError: + logger.info("Polling cancelled") + except Exception as e: + logger.exception("Fatal error in polling: {error}", error=e) + finally: + await bot.session.close() + logger.info("Bot session closed") if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Bot stopped by user (Ctrl+C)") + except Exception as e: + logger.exception("Unhandled exception: {error}", error=e)