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)