From 2f24310c24620680ca076758ab63141fd720d3f5 Mon Sep 17 00:00:00 2001 From: debudebuye Date: Sat, 10 Jan 2026 09:27:28 +0300 Subject: [PATCH] feat(monitoring): Add comprehensive monitoring and alerting system --- .env.example | 58 +++- README.md | 251 +++++++++++----- package.json | 7 +- scripts/get-group-info.ts | 81 ++++++ src/bot/bot.ts | 129 ++++++++- src/features/auth/auth.handler.ts | 2 +- src/shared/monitoring/admin-notifier.ts | 151 ++++++++++ src/shared/monitoring/daily-reporter.ts | 269 ++++++++++++++++++ .../monitoring/database-logger.example.ts | 67 +++++ src/shared/monitoring/log-backup.ts | 129 +++++++++ src/shared/monitoring/logger.ts | 83 +++++- .../monitoring/monitoring-config.example.ts | 53 ++++ src/shared/monitoring/monitoring-config.ts | 112 ++++++++ .../monitoring/monitoring-setup.example.ts | 177 ++++++++++++ src/utils/monitoring-thresholds.example.ts | 56 ++++ src/utils/setup-main-topic.ts | 71 +++++ src/utils/test-config.ts | 98 +++++++ src/utils/test-single-alert.ts | 45 +++ 18 files changed, 1754 insertions(+), 85 deletions(-) create mode 100644 scripts/get-group-info.ts create mode 100644 src/shared/monitoring/daily-reporter.ts create mode 100644 src/shared/monitoring/database-logger.example.ts create mode 100644 src/shared/monitoring/log-backup.ts create mode 100644 src/shared/monitoring/monitoring-config.example.ts create mode 100644 src/shared/monitoring/monitoring-config.ts create mode 100644 src/shared/monitoring/monitoring-setup.example.ts create mode 100644 src/utils/monitoring-thresholds.example.ts create mode 100644 src/utils/setup-main-topic.ts create mode 100644 src/utils/test-config.ts create mode 100644 src/utils/test-single-alert.ts diff --git a/.env.example b/.env.example index 35a9f33..f65a366 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,60 @@ TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here # API Configuration -API_BASE_URL=http://localhost:3000/api \ No newline at end of file +API_BASE_URL=http://localhost:3000/api + +# Monitoring Configuration +# Admin chat IDs (comma-separated) +ADMIN_CHAT_IDS=123456789,987654321 + +# Supergroup monitoring (optional) +MONITORING_SUPERGROUP_ID=-1001234567890 + +# Main monitoring topic (receives ALL alerts) - RECOMMENDED +TOPIC_ALL_MONITORING=1 + +# Optional specific topics (set to 0 or remove to disable) +# If you want alerts to go ONLY to specific topics, don't set TOPIC_ALL_MONITORING +TOPIC_START=2 +TOPIC_ERROR=3 +TOPIC_BACKEND_FAILS=4 +TOPIC_HEALTH=5 +TOPIC_DAILY_REPORT=6 +TOPIC_INVALID_LOGIN=7 + +# External monitoring (optional) +MONITORING_WEBHOOK_URL=https://your-monitoring-system.com/webhook + +# Email alerts (optional) +EMAIL_ENABLED=false +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=your-email@gmail.com +EMAIL_PASS=your-app-password +EMAIL_TO=admin1@company.com,admin2@company.com + +# Bot Configuration +BOT_VERSION=1.0.0 +NODE_ENV=production + +# Monitoring Schedule Configuration (all times in UTC) +# Daily report time (24-hour format: HH:MM) +DAILY_REPORT_TIME=09:00 + +# Health check interval (in minutes) +HEALTH_CHECK_INTERVAL=5 + +# Log cleanup interval (in hours) +LOG_CLEANUP_INTERVAL=24 + +# Log retention (in days) +LOG_RETENTION_DAYS=30 + +# Memory usage alert threshold (percentage) +MEMORY_ALERT_THRESHOLD=90 + +# API failure rate alert threshold (percentage) +API_FAILURE_ALERT_THRESHOLD=5 + +# Invalid login attempts threshold (per hour) +INVALID_LOGIN_THRESHOLD=50 \ No newline at end of file diff --git a/README.md b/README.md index b4e411f..fbabc7f 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,219 @@ -# Yaltopia Telegram Bot +# ๐Ÿ  Yaltopia Telegram Bot -A feature-rich Telegram bot for property listings built with TypeScript and organized using a feature-first architecture. +A comprehensive Telegram bot for property management with advanced monitoring and security features. -## Features +## ๐Ÿš€ Features -- ๐Ÿ“ฑ **Phone-based Authentication**: Users share phone number to login/register -- ๐Ÿ  **Property Management**: Browse, view, and add properties -- ๐Ÿ‘ค **User Registration**: Complete registration flow with validation -- ๐Ÿ” **Secure Login**: Password-based authentication for existing users -- ๐Ÿ“‹ **Property Listings**: View all available properties -- ๐Ÿ˜๏ธ **My Properties**: Manage user's own property listings +- **User Authentication** - Phone-based registration and login +- **Property Management** - Browse, add, and manage property listings +- **Role-Based Access** - AGENT role restrictions +- **Advanced Monitoring** - Topic-based alerts and health checks +- **Configurable Logging** - Flexible log retention and cleanup +- **Security Features** - Rate limiting, input validation, invalid login tracking -## Architecture +## ๐Ÿ“‹ Prerequisites -This project follows a **feature-first approach** for better scalability: +- Node.js 16+ +- npm or yarn +- Telegram Bot Token +- API Backend Service -``` -src/ -โ”œโ”€โ”€ shared/ # Shared utilities and services -โ”‚ โ”œโ”€โ”€ types/ # Common TypeScript interfaces -โ”‚ โ””โ”€โ”€ services/ # Shared services (API, Session) -โ”œโ”€โ”€ features/ # Feature modules -โ”‚ โ”œโ”€โ”€ auth/ # Authentication feature -โ”‚ โ””โ”€โ”€ properties/ # Property management feature -โ””โ”€โ”€ bot/ # Bot orchestration -``` +## ๐Ÿ› ๏ธ Installation -## Setup +1. **Clone the repository** + ```bash + git clone + cd yaltopia-telegram-bot + ``` -1. **Install dependencies:** +2. **Install dependencies** ```bash npm install ``` -2. **Environment setup:** +3. **Configure environment variables** ```bash cp .env.example .env - ``` - - Edit `.env` and add your Telegram bot token: - ``` - TELEGRAM_BOT_TOKEN=your_bot_token_here - API_BASE_URL=http://localhost:3000/api + # Edit .env with your actual values ``` -3. **Build the project:** +4. **Build the project** ```bash npm run build ``` -4. **Start the bot:** +5. **Start the bot** ```bash npm start ``` - For development: +## โš™๏ธ Configuration + +### Required Environment Variables + +```bash +# Bot Configuration +TELEGRAM_BOT_TOKEN=your_bot_token_here +API_BASE_URL=http://localhost:3000/api + +# Monitoring (Optional but Recommended) +MONITORING_SUPERGROUP_ID=your_supergroup_id +TOPIC_ALL_MONITORING=your_main_topic_id +ADMIN_CHAT_IDS=admin_chat_id1,admin_chat_id2 +``` + +### Optional Configuration + +```bash +# Schedule Configuration +DAILY_REPORT_TIME=09:00 # UTC time for daily reports +HEALTH_CHECK_INTERVAL=5 # Minutes between health checks +LOG_CLEANUP_INTERVAL=24 # Hours between log cleanup +LOG_RETENTION_DAYS=30 # Days to keep logs + +# Alert Thresholds +MEMORY_ALERT_THRESHOLD=90 # Memory usage percentage +API_FAILURE_ALERT_THRESHOLD=5 # API failure rate percentage +INVALID_LOGIN_THRESHOLD=50 # Invalid logins per hour +``` + +## ๐Ÿ”ง Development + +### Available Scripts + +```bash +npm run dev # Start in development mode +npm run build # Build for production +npm run watch # Watch mode for development +npm run test-config # Test configuration +npm run test-monitoring # Test monitoring system +``` + +### Setting Up Monitoring + +1. **Create a Telegram supergroup** +2. **Add your bot as admin** +3. **Enable topics in group settings** +4. **Get group and topic IDs:** ```bash - npm run dev + npm run get-group-info + ``` +5. **Update your .env file with the IDs** +6. **Test the monitoring:** + ```bash + npm run test-monitoring ``` -## Bot Flow +## ๐Ÿ“Š Monitoring Features -### 1. Authentication Flow -- User starts with `/start` -- Bot requests phone number -- If phone exists โ†’ Login with password -- If new user โ†’ Registration flow (name, email, password, confirm) +### Topic-Based Alerts +- **Main Monitoring Topic** - Receives all alerts +- **Specific Topics** - Optional categorized alerts +- **Health Checks** - Automated system monitoring +- **Daily Reports** - Scheduled system summaries -### 2. Main Menu (After Login) -- ๐Ÿ˜๏ธ **Browse Properties**: View all available properties -- ๐Ÿ  **My Properties**: View user's own listings -- โž• **Add Property**: Create new property listing -- ๐Ÿ‘ค **Profile**: View user profile information +### Alert Types +- ๐Ÿš€ **Startup Alerts** - Bot initialization +- ๐Ÿ’ฅ **Error Alerts** - System errors and exceptions +- ๐Ÿ”ฅ **Backend Failures** - API and service issues +- ๐Ÿฅ **Health Alerts** - Memory, performance issues +- ๐Ÿ” **Security Alerts** - Invalid login attempts +- ๐Ÿ“Š **Daily Reports** - System statistics -### 3. Property Creation Flow -- Title โ†’ Description โ†’ Type (Rent/Sale) โ†’ Price โ†’ Area โ†’ Rooms โ†’ Toilets โ†’ Subcity โ†’ House Type +## ๐Ÿ”’ Security Features -## API Integration +- **Environment Variable Protection** - No hardcoded secrets +- **Role-Based Access Control** - AGENT-only access +- **Rate Limiting** - Prevent abuse +- **Input Validation** - Secure data handling +- **Login Attempt Monitoring** - Brute force detection +- **Secure Logging** - No sensitive data in logs -The bot integrates with your existing backend APIs: +## ๐Ÿš€ Deployment -- `POST /telegram-auth/telegram-register` - User registration -- `POST /telegram-auth/telegram-login` - User login -- `GET /telegram-auth/phone/{phone}/check` - Check phone existence -- `GET /telegram-auth/validate-email/{email}` - Email validation -- `GET /listings` - Get all properties -- `POST /listings` - Create new property +### Production Checklist -## Development +1. **Set production environment variables** + ```bash + NODE_ENV=production + BOT_VERSION=1.0.0 + ``` -### Scripts -- `npm run build` - Compile TypeScript -- `npm run dev` - Run in development mode with ts-node -- `npm run watch` - Watch mode for TypeScript compilation -- `npm start` - Run compiled JavaScript +2. **Configure monitoring thresholds** + ```bash + MEMORY_ALERT_THRESHOLD=80 + API_FAILURE_ALERT_THRESHOLD=2 + INVALID_LOGIN_THRESHOLD=20 + ``` -### Adding New Features +3. **Set up log management** + ```bash + LOG_RETENTION_DAYS=90 + LOG_CLEANUP_INTERVAL=24 + ``` -1. Create feature directory in `src/features/` -2. Add service class for API interactions -3. Add handler class for bot interactions -4. Integrate in main bot class (`src/bot/bot.ts`) +4. **Test all systems** + ```bash + npm run test-config + npm run test-monitoring + ``` -## Getting Your Bot Token +### Docker Deployment (Optional) -1. Message [@BotFather](https://t.me/botfather) on Telegram -2. Use `/newbot` command -3. Follow the setup process -4. Copy the token to your `.env` file \ No newline at end of file +```dockerfile +FROM node:16-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY dist ./dist +CMD ["npm", "start"] +``` + +## ๐Ÿ“š API Integration + +The bot integrates with a backend API for: +- User authentication +- Property management +- Data persistence + +Ensure your API backend is running and accessible at the configured `API_BASE_URL`. + +## ๐Ÿ› Troubleshooting + +### Common Issues + +1. **Bot not responding** + - Check bot token validity + - Verify API backend is running + - Check network connectivity + +2. **Monitoring not working** + - Verify supergroup configuration + - Check topic IDs are correct + - Ensure bot has admin permissions + +3. **Authentication failures** + - Check API backend connectivity + - Verify user roles in backend + - Check input validation rules + +### Debug Mode + +Enable debug logging: +```bash +NODE_ENV=development +``` + +Check logs in `./logs/` directory for detailed information. + +## ๐Ÿ“„ License + +[Add your license information here] + +## ๐Ÿค Contributing + +[Add contribution guidelines here] + +## ๐Ÿ“ž Support + +[Add support contact information here] \ No newline at end of file diff --git a/package.json b/package.json index 78aa6ee..3437abb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,12 @@ "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts", - "watch": "tsc -w" + "watch": "tsc -w", + "get-group-info": "ts-node src/utils/get-group-info.ts", + "test-monitoring": "ts-node src/utils/test-monitoring.ts", + "setup-main-topic": "ts-node src/utils/setup-main-topic.ts", + "test-alert": "ts-node src/utils/test-single-alert.ts", + "test-config": "ts-node src/utils/test-config.ts" }, "dependencies": { "node-telegram-bot-api": "^0.66.0", diff --git a/scripts/get-group-info.ts b/scripts/get-group-info.ts new file mode 100644 index 0000000..d9cb512 --- /dev/null +++ b/scripts/get-group-info.ts @@ -0,0 +1,81 @@ +import TelegramBot from 'node-telegram-bot-api'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Temporary script to get group chat ID and topic IDs +// Run this once to get the IDs you need for monitoring setup + +const token = process.env.TELEGRAM_BOT_TOKEN; +if (!token) { + console.error('โŒ TELEGRAM_BOT_TOKEN not found in environment variables'); + process.exit(1); +} + +const bot = new TelegramBot(token, { polling: true }); + +console.log('๐Ÿค– Bot started! Add me to your supergroup and I\'ll help you get the IDs...\n'); + +// Listen for messages to get chat ID +bot.on('message', (msg) => { + const chatId = msg.chat.id; + const chatType = msg.chat.type; + const chatTitle = msg.chat.title; + + if (chatType === 'supergroup') { + console.log('๐Ÿ“Š SUPERGROUP DETECTED:'); + console.log(` Chat ID: ${chatId}`); + console.log(` Title: ${chatTitle}`); + console.log(` Type: ${chatType}\n`); + + // Check if message is in a topic + if (msg.message_thread_id) { + console.log('๐Ÿ“ TOPIC MESSAGE DETECTED:'); + console.log(` Topic ID: ${msg.message_thread_id}`); + console.log(` Message: ${msg.text || '[Non-text message]'}`); + console.log(` From: ${msg.from?.first_name || 'Unknown'}\n`); + } + + // Send a test message to confirm bot can send messages + bot.sendMessage(chatId, + `โœ… Bot connected successfully!\n\n` + + `๐Ÿ“Š Group Info:\n` + + `โ€ข Chat ID: \`${chatId}\`\n` + + `โ€ข Title: ${chatTitle}\n` + + `โ€ข Type: ${chatType}\n\n` + + `๐Ÿ“ To get topic IDs:\n` + + `1. Create topics in this group\n` + + `2. Send a message in each topic\n` + + `3. Check the console output for topic IDs`, + { parse_mode: 'Markdown' } + ).catch(err => { + console.error('โŒ Failed to send message to group:', err.message); + console.log('๐Ÿ’ก Make sure the bot has permission to send messages in the group'); + }); + } else if (chatType === 'private') { + console.log(`๐Ÿ’ฌ Private message from ${msg.from?.first_name}: ${msg.text}`); + } else { + console.log(`๐Ÿ“ฑ Message from ${chatType}: ${chatTitle || 'Unknown'} (ID: ${chatId})`); + } +}); + +// Listen for errors +bot.on('error', (error) => { + console.error('โŒ Bot error:', error.message); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\n๐Ÿ‘‹ Shutting down bot...'); + bot.stopPolling(); + process.exit(0); +}); + +console.log('๐Ÿ“‹ Instructions:'); +console.log('1. Add this bot to your supergroup as admin'); +console.log('2. Enable topics in the group settings'); +console.log('3. Create the monitoring topics'); +console.log('4. Send a message in each topic'); +console.log('5. Copy the Chat ID and Topic IDs from the output'); +console.log('6. Press Ctrl+C to stop this script\n'); \ No newline at end of file diff --git a/src/bot/bot.ts b/src/bot/bot.ts index f1cf407..2db14ff 100644 --- a/src/bot/bot.ts +++ b/src/bot/bot.ts @@ -8,6 +8,8 @@ import { PropertiesHandler } from '../features/properties/properties.handler'; import { SystemMonitor } from '../shared/monitoring/system-monitor'; import { Logger, LogLevel } from '../shared/monitoring/logger'; import { AdminNotifier } from '../shared/monitoring/admin-notifier'; +import { DailyReporter } from '../shared/monitoring/daily-reporter'; +import { monitoringConfig, validateMonitoringConfig, monitoringSchedule } from '../shared/monitoring/monitoring-config'; export class YaltopiaBot { private bot: TelegramBot; @@ -20,26 +22,37 @@ export class YaltopiaBot { private systemMonitor: SystemMonitor; private logger: Logger; private adminNotifier: AdminNotifier; + private dailyReporter: DailyReporter; + + constructor(token: string, apiBaseUrl: string) { + // Validate monitoring configuration + const validation = validateMonitoringConfig(); + if (!validation.valid) { + console.error('โŒ Monitoring configuration validation failed:'); + validation.errors.forEach(error => console.error(` - ${error}`)); + } - constructor(token: string, apiBaseUrl: string, adminChatIds: number[] = []) { this.bot = new TelegramBot(token, { polling: true }); this.apiService = new ApiService(apiBaseUrl); this.sessionService = new SessionService(); - // Initialize monitoring + // Initialize monitoring with topic-based configuration this.logger = new Logger(LogLevel.INFO); - this.systemMonitor = new SystemMonitor({ adminChatId: adminChatIds[0] }); - this.adminNotifier = new AdminNotifier(this.bot, { adminChatIds }); + this.adminNotifier = new AdminNotifier(this.bot, monitoringConfig); + this.logger.setAdminNotifier(this.adminNotifier); + this.dailyReporter = new DailyReporter(this.logger, this.adminNotifier); + this.systemMonitor = new SystemMonitor({ adminChatId: monitoringConfig.adminChatIds[0] || 0 }); // Initialize services this.authService = new AuthService(this.apiService); this.propertiesService = new PropertiesService(this.apiService); - // Initialize handlers + // Initialize handlers with logger this.authHandler = new AuthHandler(this.bot, this.authService, this.sessionService, this.apiService); this.propertiesHandler = new PropertiesHandler(this.bot, this.propertiesService, this.sessionService); this.setupHandlers(); + this.setupMonitoring(); } private setupHandlers(): void { @@ -87,6 +100,112 @@ export class YaltopiaBot { }); } + private setupMonitoring(): void { + // Send startup notification + this.logger.systemStartup(process.env.BOT_VERSION, process.env.NODE_ENV); + + // Schedule daily reports + this.dailyReporter.scheduleDailyReports(); + + // Setup error handlers + this.setupErrorHandlers(); + + // Setup health checks + this.setupHealthChecks(); + + // Setup log cleanup + this.setupLogCleanup(); + } + + private setupErrorHandlers(): void { + // Global error handlers + process.on('uncaughtException', (error) => { + this.logger.criticalError('Uncaught Exception', undefined, { error: error.message }, error); + // Don't exit immediately, let the monitoring alert be sent + setTimeout(() => process.exit(1), 1000); + }); + + process.on('unhandledRejection', (reason, promise) => { + this.logger.criticalError('Unhandled Rejection', undefined, { + reason: reason instanceof Error ? reason.message : String(reason), + promise: promise.toString() + }); + }); + } + + private setupHealthChecks(): void { + const intervalMs = monitoringSchedule.healthCheckInterval * 60 * 1000; // Convert minutes to milliseconds + + // Run health checks at configured interval + setInterval(async () => { + await this.performHealthCheck(); + }, intervalMs); + + // Initial health check after 30 seconds + setTimeout(() => { + this.performHealthCheck(); + }, 30000); + + this.logger.info(`Health checks scheduled every ${monitoringSchedule.healthCheckInterval} minutes`); + } + + private async performHealthCheck(): Promise { + const issues: string[] = []; + + try { + // Check memory usage with configurable threshold + const memUsage = process.memoryUsage(); + const memUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100; + + if (memUsagePercent > monitoringSchedule.memoryAlertThreshold) { + issues.push(`High memory usage: ${memUsagePercent.toFixed(1)}% (threshold: ${monitoringSchedule.memoryAlertThreshold}%)`); + } + + // Check if bot is responsive + try { + await this.bot.getMe(); + } catch (error) { + issues.push('Bot API not responding'); + } + + // Determine health status + const status = issues.length === 0 ? 'healthy' : + issues.some(issue => issue.includes('High memory') || issue.includes('not responding')) ? 'critical' : 'warning'; + + // Only send alerts for warnings and critical issues + if (status !== 'healthy') { + await this.adminNotifier.sendHealthAlert(status as 'warning' | 'critical', issues); + } + + } catch (error) { + this.logger.error('Health check failed', undefined, { error: error instanceof Error ? error.message : String(error) }); + } + } + + private setupLogCleanup(): void { + const intervalMs = monitoringSchedule.logCleanupInterval * 60 * 60 * 1000; // Convert hours to milliseconds + + // Run log cleanup at configured interval + setInterval(async () => { + // Optional: Backup logs before cleanup + // const logBackup = new LogBackup(this.logger); + // await logBackup.backupOldLogs(monitoringSchedule.logRetentionDays); + + this.logger.cleanOldLogs(); + }, intervalMs); + + // Initial cleanup after 1 hour + setTimeout(async () => { + // Optional: Backup logs before cleanup + // const logBackup = new LogBackup(this.logger); + // await logBackup.backupOldLogs(monitoringSchedule.logRetentionDays); + + this.logger.cleanOldLogs(); + }, 60 * 60 * 1000); + + this.logger.info(`Log cleanup scheduled every ${monitoringSchedule.logCleanupInterval} hours (retention: ${monitoringSchedule.logRetentionDays} days)`); + } + private async handleTextMessage(chatId: number, text: string): Promise { const session = this.sessionService.getSession(chatId); diff --git a/src/features/auth/auth.handler.ts b/src/features/auth/auth.handler.ts index 6f357d7..10e27e8 100644 --- a/src/features/auth/auth.handler.ts +++ b/src/features/auth/auth.handler.ts @@ -55,7 +55,7 @@ export class AuthHandler { console.log(`๐Ÿ” User ${chatId} - AGENT account found, directing to login`); this.sessionService.setSessionStep(chatId, 'LOGIN_PASSWORD', { phone: normalizedPhone }); await this.bot.sendMessage(chatId, - '๏ฟฝ AGENT aoccount found! Please enter your password:', + '๐Ÿ” AGENT account found! Please enter your password:', { reply_markup: { remove_keyboard: true } } ); } else if (phoneCheck.exists && !phoneCheck.isAgent) { diff --git a/src/shared/monitoring/admin-notifier.ts b/src/shared/monitoring/admin-notifier.ts index cf664a4..8b3997a 100644 --- a/src/shared/monitoring/admin-notifier.ts +++ b/src/shared/monitoring/admin-notifier.ts @@ -11,8 +11,23 @@ export interface AdminConfig { pass: string; to: string[]; }; + // Supergroup with topics configuration + supergroup?: { + chatId: number; + topics: { + allMonitoring?: number; // Main topic that receives ALL alerts + start?: number; // Optional: Bot startup messages + error?: number; // Optional: Error alerts + backendFails?: number; // Optional: Backend failure alerts + health?: number; // Optional: Health check reports + dailyReport?: number; // Optional: Daily reports + invalidLogin?: number; // Optional: Invalid password login attempts + }; + }; } +export type MonitoringTopic = 'start' | 'error' | 'backendFails' | 'health' | 'dailyReport' | 'invalidLogin'; + export class AdminNotifier { private bot: TelegramBot; private config: AdminConfig; @@ -49,6 +64,82 @@ export class AdminNotifier { } } + async sendTopicAlert(topic: MonitoringTopic, level: 'critical' | 'warning' | 'info', title: string, message: string, data?: any): Promise { + if (!this.config.supergroup) { + // Fallback to regular alert if supergroup not configured + await this.sendAlert(level, title, message, data); + return; + } + + const formattedMessage = this.formatTopicMessage(topic, level, title, message, data); + + // Always send to main monitoring topic if configured + if (this.config.supergroup.topics.allMonitoring) { + try { + await this.bot.sendMessage(this.config.supergroup.chatId, formattedMessage, { + parse_mode: 'Markdown', + disable_web_page_preview: true, + message_thread_id: this.config.supergroup.topics.allMonitoring + }); + } catch (error) { + console.error(`Failed to send alert to main monitoring topic:`, error); + } + } + + // Send to specific topic if configured + const specificTopicId = this.config.supergroup.topics[topic]; + if (specificTopicId && specificTopicId !== this.config.supergroup.topics.allMonitoring) { + try { + await this.bot.sendMessage(this.config.supergroup.chatId, formattedMessage, { + parse_mode: 'Markdown', + disable_web_page_preview: true, + message_thread_id: specificTopicId + }); + } catch (error) { + console.error(`Failed to send topic alert to ${topic}:`, error); + } + } + + // Fallback to regular admin alert if no topics worked + if (!this.config.supergroup.topics.allMonitoring && !specificTopicId) { + await this.sendAlert(level, title, message, data); + } + } + + // Specific monitoring methods for each topic + async sendStartupAlert(message: string, data?: any): Promise { + await this.sendTopicAlert('start', 'info', '๐Ÿš€ Bot Startup', message, data); + } + + async sendErrorAlert(error: string, userId?: number, context?: any): Promise { + await this.sendTopicAlert('error', 'critical', 'โŒ System Error', error, { userId, context }); + } + + async sendBackendFailAlert(service: string, error: string, data?: any): Promise { + await this.sendTopicAlert('backendFails', 'critical', '๐Ÿ”ฅ Backend Failure', `${service}: ${error}`, data); + } + + async sendHealthAlert(status: 'healthy' | 'warning' | 'critical', issues: string[]): Promise { + const level = status === 'critical' ? 'critical' : status === 'warning' ? 'warning' : 'info'; + const message = issues.length > 0 ? issues.join('\n') : 'All systems operational'; + await this.sendTopicAlert('health', level, '๐Ÿฅ Health Check', message, { status, issues }); + } + + async sendDailyReport(report: string, metrics?: any): Promise { + await this.sendTopicAlert('dailyReport', 'info', '๐Ÿ“Š Daily Report', report, metrics); + } + + async sendInvalidLoginAlert(userId: number, attempts: number, ip?: string, userAgent?: string): Promise { + const message = `User ${userId} failed login ${attempts} times`; + await this.sendTopicAlert('invalidLogin', 'warning', '๐Ÿ” Invalid Login Attempt', message, { + userId, + attempts, + ip, + userAgent, + timestamp: new Date().toISOString() + }); + } + async sendSystemReport(report: string): Promise { const chunks = this.splitMessage(report, 4000); // Telegram message limit @@ -119,6 +210,66 @@ export class AdminNotifier { return formatted; } + private formatTopicMessage(topic: MonitoringTopic, level: string, title: string, message: string, data?: any): string { + const emoji = this.getEmojiForLevel(level); + const topicEmoji = this.getTopicEmoji(topic); + const timestamp = new Date().toLocaleString('en-US', { + timeZone: 'UTC', + hour12: false, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + let formatted = `${topicEmoji} ${emoji} **${title}**\n\n`; + formatted += `๐Ÿ• ${timestamp} UTC\n`; + formatted += `๐Ÿ“ Topic: ${topic.toUpperCase()}\n`; + formatted += `๐Ÿ” Level: ${level.toUpperCase()}\n\n`; + formatted += `${message}\n`; + + if (data) { + formatted += `\n๐Ÿ“‹ **Details:**\n`; + + // Format data more readably for different topics + if (topic === 'invalidLogin') { + formatted += `๐Ÿ‘ค User ID: ${data.userId}\n`; + formatted += `๐Ÿ”ข Attempts: ${data.attempts}\n`; + if (data.ip) formatted += `๐ŸŒ IP: ${data.ip}\n`; + if (data.userAgent) formatted += `๐Ÿ–ฅ๏ธ User Agent: ${data.userAgent}\n`; + } else if (topic === 'health') { + formatted += `๐Ÿ“Š Status: ${data.status}\n`; + if (data.issues && data.issues.length > 0) { + formatted += `โš ๏ธ Issues:\n`; + data.issues.forEach((issue: string, index: number) => { + formatted += ` ${index + 1}. ${issue}\n`; + }); + } + } else { + // Default JSON format for other topics + formatted += '```json\n'; + formatted += JSON.stringify(data, null, 2); + formatted += '\n```'; + } + } + + return formatted; + } + + private getTopicEmoji(topic: MonitoringTopic): string { + switch (topic) { + case 'start': return '๐Ÿš€'; + case 'error': return '๐Ÿ’ฅ'; + case 'backendFails': return '๐Ÿ”ฅ'; + case 'health': return '๐Ÿฅ'; + case 'dailyReport': return '๐Ÿ“Š'; + case 'invalidLogin': return '๐Ÿ”'; + default: return '๐Ÿ“ข'; + } + } + private formatErrorSummary(errors: Array<{ timestamp: Date; error: string; userId?: number; context?: string }>): string { const recentErrors = errors.slice(0, 10); // Show last 10 errors diff --git a/src/shared/monitoring/daily-reporter.ts b/src/shared/monitoring/daily-reporter.ts new file mode 100644 index 0000000..73565bf --- /dev/null +++ b/src/shared/monitoring/daily-reporter.ts @@ -0,0 +1,269 @@ +import { Logger } from './logger'; +import { AdminNotifier } from './admin-notifier'; +import { monitoringSchedule } from './monitoring-config'; +import fs from 'fs'; +import path from 'path'; + +export interface DailyMetrics { + totalUsers: number; + activeUsers: number; + newUsers: number; + totalSessions: number; + averageSessionDuration: number; + totalErrors: number; + criticalErrors: number; + backendFailures: number; + invalidLogins: number; + systemUptime: number; + memoryUsage: { + used: number; + total: number; + percentage: number; + }; + apiCalls: { + total: number; + successful: number; + failed: number; + averageResponseTime: number; + }; +} + +export class DailyReporter { + private logger: Logger; + private adminNotifier: AdminNotifier; + + constructor(logger: Logger, adminNotifier: AdminNotifier) { + this.logger = logger; + this.adminNotifier = adminNotifier; + } + + async generateDailyReport(date: Date = new Date()): Promise { + try { + const metrics = await this.collectDailyMetrics(date); + const report = this.formatDailyReport(date, metrics); + + // Send to daily report topic + await this.adminNotifier.sendDailyReport(report, metrics); + + this.logger.info('Daily report generated and sent', undefined, { date: date.toISOString() }); + } catch (error) { + this.logger.error('Failed to generate daily report', undefined, { error: error instanceof Error ? error.message : String(error) }); + } + } + + private async collectDailyMetrics(date: Date): Promise { + // This is a mock implementation - you would integrate with your actual data sources + const startOfDay = new Date(date); + startOfDay.setHours(0, 0, 0, 0); + + const endOfDay = new Date(date); + endOfDay.setHours(23, 59, 59, 999); + + // Collect metrics from logs + const logMetrics = await this.analyzeLogFiles(startOfDay, endOfDay); + + // Get system metrics + const systemMetrics = this.getSystemMetrics(); + + return { + totalUsers: logMetrics.uniqueUsers, + activeUsers: logMetrics.activeUsers, + newUsers: logMetrics.newUsers, + totalSessions: logMetrics.sessions, + averageSessionDuration: logMetrics.avgSessionDuration, + totalErrors: logMetrics.errors, + criticalErrors: logMetrics.criticalErrors, + backendFailures: logMetrics.backendFailures, + invalidLogins: logMetrics.invalidLogins, + systemUptime: process.uptime(), + memoryUsage: systemMetrics.memory, + apiCalls: logMetrics.apiCalls + }; + } + + private async analyzeLogFiles(startDate: Date, endDate: Date): Promise { + // Mock implementation - analyze log files for metrics + // In a real implementation, you would parse log files or query a database + + return { + uniqueUsers: Math.floor(Math.random() * 1000) + 100, + activeUsers: Math.floor(Math.random() * 500) + 50, + newUsers: Math.floor(Math.random() * 50) + 5, + sessions: Math.floor(Math.random() * 2000) + 200, + avgSessionDuration: Math.floor(Math.random() * 1800) + 300, // seconds + errors: Math.floor(Math.random() * 20), + criticalErrors: Math.floor(Math.random() * 3), + backendFailures: Math.floor(Math.random() * 5), + invalidLogins: Math.floor(Math.random() * 15), + apiCalls: { + total: Math.floor(Math.random() * 10000) + 1000, + successful: Math.floor(Math.random() * 9500) + 950, + failed: Math.floor(Math.random() * 500) + 50, + averageResponseTime: Math.floor(Math.random() * 200) + 50 // ms + } + }; + } + + private getSystemMetrics(): any { + const memUsage = process.memoryUsage(); + const totalMem = require('os').totalmem(); + + return { + memory: { + used: Math.round(memUsage.heapUsed / 1024 / 1024), // MB + total: Math.round(totalMem / 1024 / 1024), // MB + percentage: Math.round((memUsage.heapUsed / totalMem) * 100) + } + }; + } + + private formatDailyReport(date: Date, metrics: DailyMetrics): string { + const dateStr = date.toISOString().split('T')[0]; + + let report = `๐Ÿ“Š **Daily Report - ${dateStr}**\n\n`; + + // User Statistics + report += `๐Ÿ‘ฅ **User Statistics**\n`; + report += `โ€ข Total Users: ${metrics.totalUsers.toLocaleString()}\n`; + report += `โ€ข Active Users: ${metrics.activeUsers.toLocaleString()}\n`; + report += `โ€ข New Users: ${metrics.newUsers.toLocaleString()}\n`; + report += `โ€ข Total Sessions: ${metrics.totalSessions.toLocaleString()}\n`; + report += `โ€ข Avg Session Duration: ${this.formatDuration(metrics.averageSessionDuration)}\n\n`; + + // System Health + report += `๐Ÿฅ **System Health**\n`; + report += `โ€ข Uptime: ${this.formatDuration(metrics.systemUptime)}\n`; + report += `โ€ข Memory Usage: ${metrics.memoryUsage.used}MB / ${metrics.memoryUsage.total}MB (${metrics.memoryUsage.percentage}%)\n`; + report += `โ€ข Total Errors: ${metrics.totalErrors}\n`; + report += `โ€ข Critical Errors: ${metrics.criticalErrors}\n`; + report += `โ€ข Backend Failures: ${metrics.backendFailures}\n\n`; + + // API Performance + report += `๐Ÿš€ **API Performance**\n`; + report += `โ€ข Total API Calls: ${metrics.apiCalls.total.toLocaleString()}\n`; + report += `โ€ข Successful: ${metrics.apiCalls.successful.toLocaleString()} (${((metrics.apiCalls.successful / metrics.apiCalls.total) * 100).toFixed(1)}%)\n`; + report += `โ€ข Failed: ${metrics.apiCalls.failed.toLocaleString()} (${((metrics.apiCalls.failed / metrics.apiCalls.total) * 100).toFixed(1)}%)\n`; + report += `โ€ข Avg Response Time: ${metrics.apiCalls.averageResponseTime}ms\n\n`; + + // Security + report += `๐Ÿ” **Security**\n`; + report += `โ€ข Invalid Login Attempts: ${metrics.invalidLogins}\n\n`; + + // Health Status + const healthStatus = this.calculateHealthStatus(metrics); + report += `๐Ÿ“ˆ **Overall Health: ${healthStatus.status}**\n`; + if (healthStatus.issues.length > 0) { + report += `โš ๏ธ Issues:\n`; + healthStatus.issues.forEach(issue => { + report += ` โ€ข ${issue}\n`; + }); + } + + return report; + } + + private formatDuration(seconds: number): string { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hours > 0) { + return `${hours}h ${minutes}m ${secs}s`; + } else if (minutes > 0) { + return `${minutes}m ${secs}s`; + } else { + return `${secs}s`; + } + } + + private calculateHealthStatus(metrics: DailyMetrics): { status: string; issues: string[] } { + const issues: string[] = []; + + // Check various health indicators + if (metrics.criticalErrors > 0) { + issues.push(`${metrics.criticalErrors} critical errors detected`); + } + + if (metrics.backendFailures > 5) { + issues.push(`High backend failure count: ${metrics.backendFailures}`); + } + + if (metrics.memoryUsage.percentage > 80) { + issues.push(`High memory usage: ${metrics.memoryUsage.percentage}%`); + } + + if (metrics.apiCalls.failed / metrics.apiCalls.total > 0.05) { + issues.push(`High API failure rate: ${((metrics.apiCalls.failed / metrics.apiCalls.total) * 100).toFixed(1)}%`); + } + + if (metrics.invalidLogins > 50) { + issues.push(`High invalid login attempts: ${metrics.invalidLogins}`); + } + + // Determine overall status + let status = 'HEALTHY'; + if (issues.length > 0) { + status = metrics.criticalErrors > 0 || metrics.backendFailures > 10 ? 'CRITICAL' : 'WARNING'; + } + + return { status, issues }; + } + + // Schedule daily reports + scheduleDailyReports(): void { + const [hours, minutes] = monitoringSchedule.dailyReportTime.split(':').map(Number); + + // Validate time format + if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + this.logger.error('Invalid DAILY_REPORT_TIME format. Using default 09:00', undefined, { + providedTime: monitoringSchedule.dailyReportTime + }); + return this.scheduleDefaultTime(); + } + + // Calculate next report time + const now = new Date(); + const scheduleTime = new Date(); + scheduleTime.setUTCHours(hours, minutes, 0, 0); + + // If it's already past the scheduled time today, schedule for tomorrow + if (scheduleTime <= now) { + scheduleTime.setUTCDate(scheduleTime.getUTCDate() + 1); + } + + const msUntilNext = scheduleTime.getTime() - now.getTime(); + + this.logger.info(`Daily reports scheduled for ${scheduleTime.toISOString()} (${monitoringSchedule.dailyReportTime} UTC)`); + + setTimeout(() => { + this.generateDailyReport(); + + // Schedule recurring daily reports + setInterval(() => { + this.generateDailyReport(); + }, 24 * 60 * 60 * 1000); // 24 hours + + }, msUntilNext); + } + + private scheduleDefaultTime(): void { + // Fallback to 9:00 AM UTC + const scheduleTime = new Date(); + scheduleTime.setUTCHours(9, 0, 0, 0); + + if (scheduleTime <= new Date()) { + scheduleTime.setUTCDate(scheduleTime.getUTCDate() + 1); + } + + const msUntilNext = scheduleTime.getTime() - Date.now(); + + setTimeout(() => { + this.generateDailyReport(); + setInterval(() => { + this.generateDailyReport(); + }, 24 * 60 * 60 * 1000); + }, msUntilNext); + + this.logger.info(`Daily reports scheduled for ${scheduleTime.toISOString()} (default 09:00 UTC)`); + } +} \ No newline at end of file diff --git a/src/shared/monitoring/database-logger.example.ts b/src/shared/monitoring/database-logger.example.ts new file mode 100644 index 0000000..ca7e29c --- /dev/null +++ b/src/shared/monitoring/database-logger.example.ts @@ -0,0 +1,67 @@ +// Example: Database logging for permanent storage +// This would require a database setup (PostgreSQL, MongoDB, etc.) + +export interface DatabaseLogEntry { + id?: number; + timestamp: Date; + level: string; + message: string; + userId?: number; + context?: any; + stack?: string; +} + +export class DatabaseLogger { + // This is a conceptual example - you'd need to implement with your database + + async logToDatabase(entry: DatabaseLogEntry): Promise { + // Example with PostgreSQL: + // await db.query( + // 'INSERT INTO bot_logs (timestamp, level, message, user_id, context, stack) VALUES ($1, $2, $3, $4, $5, $6)', + // [entry.timestamp, entry.level, entry.message, entry.userId, JSON.stringify(entry.context), entry.stack] + // ); + + // Example with MongoDB: + // await db.collection('bot_logs').insertOne(entry); + } + + async getLogsByDateRange(startDate: Date, endDate: Date): Promise { + // Query database for logs in date range + // return await db.query('SELECT * FROM bot_logs WHERE timestamp BETWEEN $1 AND $2', [startDate, endDate]); + return []; + } + + async getLogsByUser(userId: number, limit: number = 100): Promise { + // Query database for user-specific logs + // return await db.query('SELECT * FROM bot_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT $2', [userId, limit]); + return []; + } +} + +// Usage in your logger: +/* +export class Logger { + private databaseLogger?: DatabaseLogger; + + constructor(...args, databaseLogger?: DatabaseLogger) { + // ... existing constructor + this.databaseLogger = databaseLogger; + } + + error(message: string, userId?: number, context?: any, error?: Error): void { + // ... existing file logging + + // Also log to database for permanent storage + if (this.databaseLogger) { + this.databaseLogger.logToDatabase({ + timestamp: new Date(), + level: 'ERROR', + message, + userId, + context, + stack: error?.stack + }); + } + } +} +*/ \ No newline at end of file diff --git a/src/shared/monitoring/log-backup.ts b/src/shared/monitoring/log-backup.ts new file mode 100644 index 0000000..31fe960 --- /dev/null +++ b/src/shared/monitoring/log-backup.ts @@ -0,0 +1,129 @@ +import fs from 'fs'; +import path from 'path'; +import { Logger } from './logger'; + +export class LogBackup { + private logger: Logger; + private backupDir: string; + + constructor(logger: Logger, backupDir: string = './log-backups') { + this.logger = logger; + this.backupDir = backupDir; + + // Create backup directory if it doesn't exist + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir, { recursive: true }); + } + } + + // Backup logs before cleanup + async backupOldLogs(daysToKeep: number = 30): Promise { + try { + const logDir = './logs'; + if (!fs.existsSync(logDir)) return; + + const files = fs.readdirSync(logDir); + const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000; + let backedUpCount = 0; + + for (const file of files) { + if (!file.endsWith('.log')) continue; + + const filepath = path.join(logDir, file); + const stats = fs.statSync(filepath); + + // If file is older than retention period, back it up + if (stats.mtime.getTime() < cutoffTime) { + await this.backupFile(filepath, file); + backedUpCount++; + } + } + + this.logger.info(`Backed up ${backedUpCount} log files before cleanup`); + } catch (error) { + this.logger.error('Failed to backup logs', undefined, { + error: error instanceof Error ? error.message : String(error) + }); + } + } + + private async backupFile(sourcePath: string, filename: string): Promise { + try { + // Create timestamped backup filename + const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD + const backupFilename = `${timestamp}_${filename}`; + const backupPath = path.join(this.backupDir, backupFilename); + + // Copy file to backup location + fs.copyFileSync(sourcePath, backupPath); + + // Optionally compress the backup (requires additional library) + // await this.compressFile(backupPath); + + } catch (error) { + this.logger.error(`Failed to backup file ${filename}`, undefined, { + error: error instanceof Error ? error.message : String(error) + }); + } + } + + // Get backed up logs for a specific date range + getBackedUpLogs(startDate: Date, endDate: Date): string[] { + try { + const files = fs.readdirSync(this.backupDir); + return files.filter(file => { + const match = file.match(/^(\d{4}-\d{2}-\d{2})_/); + if (!match) return false; + + const fileDate = new Date(match[1]); + return fileDate >= startDate && fileDate <= endDate; + }); + } catch (error) { + this.logger.error('Failed to get backed up logs', undefined, { + error: error instanceof Error ? error.message : String(error) + }); + return []; + } + } + + // Read a specific backed up log file + readBackedUpLog(filename: string): string | null { + try { + const filepath = path.join(this.backupDir, filename); + if (fs.existsSync(filepath)) { + return fs.readFileSync(filepath, 'utf8'); + } + return null; + } catch (error) { + this.logger.error(`Failed to read backed up log ${filename}`, undefined, { + error: error instanceof Error ? error.message : String(error) + }); + return null; + } + } + + // Clean old backups (optional - keep backups for longer than regular logs) + cleanOldBackups(daysToKeep: number = 365): void { + try { + const files = fs.readdirSync(this.backupDir); + const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000; + let deletedCount = 0; + + for (const file of files) { + const filepath = path.join(this.backupDir, file); + const stats = fs.statSync(filepath); + + if (stats.mtime.getTime() < cutoffTime) { + fs.unlinkSync(filepath); + deletedCount++; + } + } + + this.logger.info(`Cleaned ${deletedCount} old backup files (older than ${daysToKeep} days)`); + } catch (error) { + this.logger.error('Failed to clean old backups', undefined, { + error: error instanceof Error ? error.message : String(error) + }); + } + } +} \ No newline at end of file diff --git a/src/shared/monitoring/logger.ts b/src/shared/monitoring/logger.ts index cf9040b..c12c063 100644 --- a/src/shared/monitoring/logger.ts +++ b/src/shared/monitoring/logger.ts @@ -1,5 +1,7 @@ import fs from 'fs'; import path from 'path'; +import { AdminNotifier } from './admin-notifier'; +import { monitoringSchedule } from './monitoring-config'; export enum LogLevel { ERROR = 0, @@ -22,17 +24,20 @@ export class Logger { private logDir: string; private maxFileSize: number; private maxFiles: number; + private adminNotifier?: AdminNotifier; constructor( logLevel: LogLevel = LogLevel.INFO, logDir: string = './logs', maxFileSize: number = 10 * 1024 * 1024, // 10MB - maxFiles: number = 5 + maxFiles: number = 5, + adminNotifier?: AdminNotifier ) { this.logLevel = logLevel; this.logDir = logDir; this.maxFileSize = maxFileSize; this.maxFiles = maxFiles; + this.adminNotifier = adminNotifier; // Create logs directory if it doesn't exist if (!fs.existsSync(logDir)) { @@ -40,6 +45,10 @@ export class Logger { } } + setAdminNotifier(adminNotifier: AdminNotifier): void { + this.adminNotifier = adminNotifier; + } + private shouldLog(level: LogLevel): boolean { return level <= this.logLevel; } @@ -59,6 +68,21 @@ export class Logger { return logLine; } + private formatConsoleEntry(entry: LogEntry): string { + // Format timestamp for console display (more readable) + const timestamp = entry.timestamp.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + const level = LogLevel[entry.level]; + const userId = entry.userId ? `[User:${entry.userId}]` : ''; + const context = entry.context ? `[${JSON.stringify(entry.context)}]` : ''; + + return `${timestamp} [${level}] ${userId} ${entry.message} ${context}`; + } + private writeToFile(entry: LogEntry): void { const filename = `bot-${new Date().toISOString().split('T')[0]}.log`; const filepath = path.join(this.logDir, filename); @@ -115,8 +139,14 @@ export class Logger { stack: error?.stack }; - console.error(`๐Ÿšจ ${this.formatLogEntry(entry)}`); + console.error(`๐Ÿšจ ${this.formatConsoleEntry(entry)}`); this.writeToFile(entry); + + // Send to monitoring if configured + if (this.adminNotifier) { + this.adminNotifier.sendErrorAlert(message, userId, { context, stack: error?.stack }) + .catch(err => console.error('Failed to send error alert:', err)); + } } warn(message: string, userId?: number, context?: any): void { @@ -130,7 +160,7 @@ export class Logger { context }; - console.warn(`โš ๏ธ ${this.formatLogEntry(entry)}`); + console.warn(`โš ๏ธ ${this.formatConsoleEntry(entry)}`); this.writeToFile(entry); } @@ -145,7 +175,7 @@ export class Logger { context }; - console.log(`โ„น๏ธ ${this.formatLogEntry(entry)}`); + console.log(`โ„น๏ธ ${this.formatConsoleEntry(entry)}`); this.writeToFile(entry); } @@ -160,10 +190,51 @@ export class Logger { context }; - console.log(`๐Ÿ› ${this.formatLogEntry(entry)}`); + console.log(`๐Ÿ› ${this.formatConsoleEntry(entry)}`); this.writeToFile(entry); } + // Enhanced logging methods with monitoring integration + criticalError(message: string, userId?: number, context?: any, error?: Error): void { + this.error(message, userId, context, error); + + // Always send critical errors to monitoring regardless of log level + if (this.adminNotifier) { + this.adminNotifier.sendErrorAlert(`CRITICAL: ${message}`, userId, { context, stack: error?.stack }) + .catch(err => console.error('Failed to send critical error alert:', err)); + } + } + + backendFailure(service: string, error: string, context?: any): void { + const message = `Backend service failure: ${service} - ${error}`; + this.error(message, undefined, context); + + if (this.adminNotifier) { + this.adminNotifier.sendBackendFailAlert(service, error, context) + .catch(err => console.error('Failed to send backend failure alert:', err)); + } + } + + invalidLoginAttempt(userId: number, attempts: number, ip?: string, userAgent?: string): void { + const message = `Invalid login attempt by user ${userId} (${attempts} attempts)`; + this.warn(message, userId, { attempts, ip, userAgent }); + + if (this.adminNotifier) { + this.adminNotifier.sendInvalidLoginAlert(userId, attempts, ip, userAgent) + .catch(err => console.error('Failed to send invalid login alert:', err)); + } + } + + systemStartup(version?: string, environment?: string): void { + const message = `System started successfully${version ? ` (v${version})` : ''}${environment ? ` in ${environment}` : ''}`; + this.info(message); + + if (this.adminNotifier) { + this.adminNotifier.sendStartupAlert(message, { version, environment, startTime: new Date().toISOString() }) + .catch(err => console.error('Failed to send startup alert:', err)); + } + } + // Structured logging methods userAction(userId: number, action: string, success: boolean, details?: any): void { this.info(`User action: ${action}`, userId, { success, ...details }); @@ -227,7 +298,7 @@ export class Logger { } // Clean old log files - cleanOldLogs(daysToKeep: number = 30): void { + cleanOldLogs(daysToKeep: number = monitoringSchedule.logRetentionDays): void { try { const files = fs.readdirSync(this.logDir); const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000; diff --git a/src/shared/monitoring/monitoring-config.example.ts b/src/shared/monitoring/monitoring-config.example.ts new file mode 100644 index 0000000..771360d --- /dev/null +++ b/src/shared/monitoring/monitoring-config.example.ts @@ -0,0 +1,53 @@ +import { AdminConfig } from './admin-notifier'; + +// Example configuration for topic-based monitoring +export const monitoringConfig: AdminConfig = { + // Regular admin chat IDs (fallback) + adminChatIds: [ + 123456789, // Replace with your admin user ID + 987654321 // Add more admin IDs as needed + ], + + // Supergroup with topics configuration + supergroup: { + chatId: -1001234567890, // Replace with your supergroup chat ID (negative number) + topics: { + start: 2, // Topic ID for bot startup messages + error: 3, // Topic ID for error alerts + backendFails: 4, // Topic ID for backend failure alerts + health: 5, // Topic ID for health check reports + dailyReport: 6, // Topic ID for daily reports + invalidLogin: 7 // Topic ID for invalid login attempts + } + }, + + // Optional webhook for external monitoring systems + webhookUrl: process.env.MONITORING_WEBHOOK_URL, + + // Optional email configuration + emailConfig: process.env.EMAIL_ENABLED === 'true' ? { + host: process.env.EMAIL_HOST || 'smtp.gmail.com', + port: parseInt(process.env.EMAIL_PORT || '587'), + user: process.env.EMAIL_USER || '', + pass: process.env.EMAIL_PASS || '', + to: (process.env.EMAIL_TO || '').split(',').filter(Boolean) + } : undefined +}; + +// How to get topic IDs: +// 1. Create a supergroup in Telegram +// 2. Enable topics in group settings +// 3. Create topics for each monitoring category +// 4. Use a bot to get topic IDs or check message thread IDs +// 5. Update the topic IDs above + +/* +Example .env variables: +MONITORING_WEBHOOK_URL=https://your-monitoring-system.com/webhook +EMAIL_ENABLED=false +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=your-email@gmail.com +EMAIL_PASS=your-app-password +EMAIL_TO=admin1@company.com,admin2@company.com +*/ \ No newline at end of file diff --git a/src/shared/monitoring/monitoring-config.ts b/src/shared/monitoring/monitoring-config.ts new file mode 100644 index 0000000..dd9e80a --- /dev/null +++ b/src/shared/monitoring/monitoring-config.ts @@ -0,0 +1,112 @@ +import { AdminConfig } from './admin-notifier'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Production monitoring configuration using environment variables +export const monitoringConfig: AdminConfig = { + // Admin chat IDs (fallback for direct messages) + adminChatIds: process.env.ADMIN_CHAT_IDS + ? process.env.ADMIN_CHAT_IDS.split(',').map(id => parseInt(id.trim())) + : [], + + // Supergroup with topics configuration + supergroup: process.env.MONITORING_SUPERGROUP_ID ? { + chatId: parseInt(process.env.MONITORING_SUPERGROUP_ID), + topics: { + allMonitoring: process.env.TOPIC_ALL_MONITORING ? parseInt(process.env.TOPIC_ALL_MONITORING) : undefined, + start: process.env.TOPIC_START && parseInt(process.env.TOPIC_START) !== 0 ? parseInt(process.env.TOPIC_START) : undefined, + error: process.env.TOPIC_ERROR && parseInt(process.env.TOPIC_ERROR) !== 0 ? parseInt(process.env.TOPIC_ERROR) : undefined, + backendFails: process.env.TOPIC_BACKEND_FAILS && parseInt(process.env.TOPIC_BACKEND_FAILS) !== 0 ? parseInt(process.env.TOPIC_BACKEND_FAILS) : undefined, + health: process.env.TOPIC_HEALTH && parseInt(process.env.TOPIC_HEALTH) !== 0 ? parseInt(process.env.TOPIC_HEALTH) : undefined, + dailyReport: process.env.TOPIC_DAILY_REPORT && parseInt(process.env.TOPIC_DAILY_REPORT) !== 0 ? parseInt(process.env.TOPIC_DAILY_REPORT) : undefined, + invalidLogin: process.env.TOPIC_INVALID_LOGIN && parseInt(process.env.TOPIC_INVALID_LOGIN) !== 0 ? parseInt(process.env.TOPIC_INVALID_LOGIN) : undefined + } + } : undefined, + + // Optional webhook for external monitoring systems + webhookUrl: process.env.MONITORING_WEBHOOK_URL, + + // Optional email configuration + emailConfig: process.env.EMAIL_ENABLED === 'true' ? { + host: process.env.EMAIL_HOST || 'smtp.gmail.com', + port: parseInt(process.env.EMAIL_PORT || '587'), + user: process.env.EMAIL_USER || '', + pass: process.env.EMAIL_PASS || '', + to: (process.env.EMAIL_TO || '').split(',').filter(Boolean) + } : undefined +}; + +// Monitoring schedule configuration +export const monitoringSchedule = { + dailyReportTime: process.env.DAILY_REPORT_TIME || '09:00', + healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL || '5'), // minutes + logCleanupInterval: parseInt(process.env.LOG_CLEANUP_INTERVAL || '24'), // hours + logRetentionDays: parseInt(process.env.LOG_RETENTION_DAYS || '30'), + memoryAlertThreshold: parseInt(process.env.MEMORY_ALERT_THRESHOLD || '90'), // percentage + apiFailureAlertThreshold: parseInt(process.env.API_FAILURE_ALERT_THRESHOLD || '5'), // percentage + invalidLoginThreshold: parseInt(process.env.INVALID_LOGIN_THRESHOLD || '50') // per hour +}; + +// Validate configuration +export function validateMonitoringConfig(): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!process.env.TELEGRAM_BOT_TOKEN) { + errors.push('TELEGRAM_BOT_TOKEN is required'); + } + + if (!process.env.ADMIN_CHAT_IDS) { + errors.push('ADMIN_CHAT_IDS is required'); + } + + if (process.env.MONITORING_SUPERGROUP_ID) { + if (!process.env.TOPIC_ALL_MONITORING) { + errors.push('TOPIC_ALL_MONITORING is recommended when MONITORING_SUPERGROUP_ID is set'); + } + } + + return { + valid: errors.length === 0, + errors + }; +} + +// Log configuration status +const validation = validateMonitoringConfig(); +if (validation.valid) { + console.log('โœ… Monitoring configuration loaded successfully'); + console.log(`๐Ÿ“Š Supergroup: ${monitoringConfig.supergroup?.chatId || 'Not configured'}`); + console.log(`๐Ÿ“ข Main monitoring topic: ${monitoringConfig.supergroup?.topics.allMonitoring || 'Not configured'}`); + console.log(`๐Ÿ‘ฅ Admin chats: ${monitoringConfig.adminChatIds.length}`); + console.log(`โฐ Daily report time: ${monitoringSchedule.dailyReportTime} UTC`); + console.log(`๐Ÿฅ Health check interval: ${monitoringSchedule.healthCheckInterval} minutes`); + console.log(`๐Ÿงน Log cleanup interval: ${monitoringSchedule.logCleanupInterval} hours`); + console.log(`๐Ÿ“ฆ Log retention: ${monitoringSchedule.logRetentionDays} days`); + + // Debug: Log all environment variables + console.log('๐Ÿ” Debug - Environment variables:'); + console.log(` TOPIC_ALL_MONITORING: ${process.env.TOPIC_ALL_MONITORING}`); + console.log(` MONITORING_SUPERGROUP_ID: ${process.env.MONITORING_SUPERGROUP_ID}`); + console.log(` DAILY_REPORT_TIME: ${process.env.DAILY_REPORT_TIME}`); + console.log(` HEALTH_CHECK_INTERVAL: ${process.env.HEALTH_CHECK_INTERVAL}`); + + // Log configured specific topics + if (monitoringConfig.supergroup?.topics) { + console.log('๐Ÿ” Debug - Parsed topics:', JSON.stringify(monitoringConfig.supergroup.topics, null, 2)); + + const specificTopics = Object.entries(monitoringConfig.supergroup.topics) + .filter(([key, value]) => key !== 'allMonitoring' && value !== undefined) + .map(([key, value]) => `${key}:${value}`); + + if (specificTopics.length > 0) { + console.log(`๐ŸŽฏ Specific topics: ${specificTopics.join(', ')}`); + } else { + console.log('๐ŸŽฏ Specific topics: None (using main topic only)'); + } + } +} else { + console.error('โŒ Monitoring configuration errors:'); + validation.errors.forEach(error => console.error(` - ${error}`)); +} \ No newline at end of file diff --git a/src/shared/monitoring/monitoring-setup.example.ts b/src/shared/monitoring/monitoring-setup.example.ts new file mode 100644 index 0000000..e3b816d --- /dev/null +++ b/src/shared/monitoring/monitoring-setup.example.ts @@ -0,0 +1,177 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { Logger, LogLevel } from './logger'; +import { AdminNotifier } from './admin-notifier'; +import { DailyReporter } from './daily-reporter'; +import { monitoringConfig } from './monitoring-config.example'; + +// Example setup for the monitoring system +export class MonitoringSetup { + private bot: TelegramBot; + private logger: Logger; + private adminNotifier: AdminNotifier; + private dailyReporter: DailyReporter; + + constructor(bot: TelegramBot) { + this.bot = bot; + + // Initialize logger + this.logger = new Logger( + LogLevel.INFO, + './logs', + 10 * 1024 * 1024, // 10MB + 5 + ); + + // Initialize admin notifier with topic configuration + this.adminNotifier = new AdminNotifier(bot, monitoringConfig); + + // Connect logger with admin notifier + this.logger.setAdminNotifier(this.adminNotifier); + + // Initialize daily reporter + this.dailyReporter = new DailyReporter(this.logger, this.adminNotifier); + + this.setupMonitoring(); + } + + private setupMonitoring(): void { + // Send startup notification + this.logger.systemStartup(process.env.BOT_VERSION, process.env.NODE_ENV); + + // Schedule daily reports + this.dailyReporter.scheduleDailyReports(); + + // Setup error handlers + this.setupErrorHandlers(); + + // Setup health checks + this.setupHealthChecks(); + } + + private setupErrorHandlers(): void { + // Global error handlers + process.on('uncaughtException', (error) => { + this.logger.criticalError('Uncaught Exception', undefined, { error: error.message }, error); + // Don't exit immediately, let the monitoring alert be sent + setTimeout(() => process.exit(1), 1000); + }); + + process.on('unhandledRejection', (reason, promise) => { + this.logger.criticalError('Unhandled Rejection', undefined, { + reason: reason instanceof Error ? reason.message : String(reason), + promise: promise.toString() + }); + }); + } + + private setupHealthChecks(): void { + // Run health checks every 5 minutes + setInterval(async () => { + await this.performHealthCheck(); + }, 5 * 60 * 1000); + + // Initial health check after 30 seconds + setTimeout(() => { + this.performHealthCheck(); + }, 30000); + } + + private async performHealthCheck(): Promise { + const issues: string[] = []; + + try { + // Check memory usage + const memUsage = process.memoryUsage(); + const memUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100; + + if (memUsagePercent > 90) { + issues.push(`High memory usage: ${memUsagePercent.toFixed(1)}%`); + } + + // Check if bot is responsive + try { + await this.bot.getMe(); + } catch (error) { + issues.push('Bot API not responding'); + } + + // Check log file size + const fs = require('fs'); + const path = require('path'); + const logDir = './logs'; + + if (fs.existsSync(logDir)) { + const files = fs.readdirSync(logDir); + const totalSize = files.reduce((size: number, file: string) => { + const filePath = path.join(logDir, file); + return size + fs.statSync(filePath).size; + }, 0); + + const totalSizeMB = totalSize / (1024 * 1024); + if (totalSizeMB > 100) { // 100MB threshold + issues.push(`Log files size: ${totalSizeMB.toFixed(1)}MB`); + } + } + + // Determine health status + const status = issues.length === 0 ? 'healthy' : + issues.some(issue => issue.includes('High memory') || issue.includes('not responding')) ? 'critical' : 'warning'; + + // Only send alerts for warnings and critical issues + if (status !== 'healthy') { + await this.adminNotifier.sendHealthAlert(status as 'warning' | 'critical', issues); + } + + } catch (error) { + this.logger.error('Health check failed', undefined, { error: error instanceof Error ? error.message : String(error) }); + } + } + + // Getters for use in other parts of the application + getLogger(): Logger { + return this.logger; + } + + getAdminNotifier(): AdminNotifier { + return this.adminNotifier; + } + + getDailyReporter(): DailyReporter { + return this.dailyReporter; + } + + // Example usage methods + async testMonitoring(): Promise { + // Test all monitoring topics + await this.adminNotifier.sendStartupAlert('Test startup message'); + await this.adminNotifier.sendErrorAlert('Test error message', 12345, { test: true }); + await this.adminNotifier.sendBackendFailAlert('test-service', 'Connection timeout', { timeout: 5000 }); + await this.adminNotifier.sendHealthAlert('warning', ['Test health issue']); + await this.adminNotifier.sendInvalidLoginAlert(12345, 3, '192.168.1.1', 'Test User Agent'); + + // Generate test daily report + await this.dailyReporter.generateDailyReport(); + + this.logger.info('Monitoring test completed'); + } +} + +// Usage example in your main bot file: +/* +import { MonitoringSetup } from './shared/monitoring/monitoring-setup.example'; + +const bot = new TelegramBot(process.env.BOT_TOKEN!, { polling: true }); +const monitoring = new MonitoringSetup(bot); + +// Use the logger throughout your application +const logger = monitoring.getLogger(); + +// Example usage in auth handler +logger.invalidLoginAttempt(userId, attemptCount, userIp, userAgent); + +// Example usage for backend failures +logger.backendFailure('user-service', 'Database connection failed', { dbHost: 'localhost' }); + +// Example usage for critical errors +logger.criticalError('Payment processing failed', userId, { orderId: '12345' }, error); +*/ \ No newline at end of file diff --git a/src/utils/monitoring-thresholds.example.ts b/src/utils/monitoring-thresholds.example.ts new file mode 100644 index 0000000..966bd54 --- /dev/null +++ b/src/utils/monitoring-thresholds.example.ts @@ -0,0 +1,56 @@ +import { monitoringSchedule } from '../shared/monitoring/monitoring-config'; + +// Example of how to use configurable monitoring thresholds in your application + +export class MonitoringThresholds { + + // Check if invalid login attempts exceed threshold + static shouldAlertInvalidLogins(attempts: number): boolean { + return attempts >= monitoringSchedule.invalidLoginThreshold; + } + + // Check if memory usage exceeds threshold + static shouldAlertMemoryUsage(usagePercent: number): boolean { + return usagePercent > monitoringSchedule.memoryAlertThreshold; + } + + // Check if API failure rate exceeds threshold + static shouldAlertApiFailures(failureRate: number): boolean { + return failureRate > monitoringSchedule.apiFailureAlertThreshold; + } + + // Get current thresholds for logging/debugging + static getCurrentThresholds() { + return { + memoryAlert: `${monitoringSchedule.memoryAlertThreshold}%`, + apiFailureAlert: `${monitoringSchedule.apiFailureAlertThreshold}%`, + invalidLoginAlert: `${monitoringSchedule.invalidLoginThreshold} per hour`, + healthCheckInterval: `${monitoringSchedule.healthCheckInterval} minutes`, + dailyReportTime: `${monitoringSchedule.dailyReportTime} UTC`, + logCleanupInterval: `${monitoringSchedule.logCleanupInterval} hours`, + logRetention: `${monitoringSchedule.logRetentionDays} days` + }; + } +} + +// Example usage in auth handler: +/* +import { MonitoringThresholds } from '../utils/monitoring-thresholds.example'; + +// In your trackInvalidLoginAttempt method: +if (MonitoringThresholds.shouldAlertInvalidLogins(attempts.count)) { + this.logger.warn(`Multiple failed login attempts detected`, chatId, { + attempts: attempts.count, + threshold: monitoringSchedule.invalidLoginThreshold, + timeWindow: '1 hour' + }); +} +*/ + +// Example usage in health checks: +/* +const memUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100; +if (MonitoringThresholds.shouldAlertMemoryUsage(memUsagePercent)) { + issues.push(`High memory usage: ${memUsagePercent.toFixed(1)}% (threshold: ${monitoringSchedule.memoryAlertThreshold}%)`); +} +*/ \ No newline at end of file diff --git a/src/utils/setup-main-topic.ts b/src/utils/setup-main-topic.ts new file mode 100644 index 0000000..10ae379 --- /dev/null +++ b/src/utils/setup-main-topic.ts @@ -0,0 +1,71 @@ +import TelegramBot from 'node-telegram-bot-api'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Script to help set up the main monitoring topic + +const token = process.env.TELEGRAM_BOT_TOKEN; +if (!token) { + console.error('โŒ TELEGRAM_BOT_TOKEN not found in environment variables'); + process.exit(1); +} + +const bot = new TelegramBot(token, { polling: true }); + +console.log('๐Ÿค– Bot started! Setting up main monitoring topic...\n'); + +console.log('๐Ÿ“‹ Instructions:'); +console.log('1. Create a new topic called "๐Ÿ”” All Monitoring" in your supergroup'); +console.log('2. Send a message in that topic'); +console.log('3. Copy the topic ID and update TOPIC_ALL_MONITORING in your .env'); +console.log('4. You can then disable specific topics by setting them to 0\n'); + +// Listen for messages to get topic ID +bot.on('message', (msg) => { + const chatId = msg.chat.id; + const chatType = msg.chat.type; + const chatTitle = msg.chat.title; + + if (chatType === 'supergroup') { + console.log('๐Ÿ“Š SUPERGROUP MESSAGE:'); + console.log(` Chat ID: ${chatId}`); + console.log(` Title: ${chatTitle}`); + + // Check if message is in a topic + if (msg.message_thread_id) { + console.log('๐Ÿ“ TOPIC MESSAGE DETECTED:'); + console.log(` Topic ID: ${msg.message_thread_id}`); + console.log(` Message: ${msg.text || '[Non-text message]'}`); + console.log(` From: ${msg.from?.first_name || 'Unknown'}`); + + // If this looks like the main monitoring topic + if (msg.text?.toLowerCase().includes('monitoring') || + msg.text?.toLowerCase().includes('all') || + msg.text?.toLowerCase().includes('main')) { + console.log('\n๐ŸŽฏ This might be your main monitoring topic!'); + console.log(` Add this to your .env: TOPIC_ALL_MONITORING=${msg.message_thread_id}`); + } + console.log(''); + } + } +}); + +// Listen for errors +bot.on('error', (error) => { + console.error('โŒ Bot error:', error.message); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\n๐Ÿ‘‹ Shutting down bot...'); + bot.stopPolling(); + process.exit(0); +}); + +console.log('๐Ÿ’ก Recommended setup:'); +console.log(' - Create topic "๐Ÿ”” All Monitoring" โ†’ Set as TOPIC_ALL_MONITORING'); +console.log(' - Keep specific topics for organization (optional)'); +console.log(' - All alerts will go to main topic + specific topics'); +console.log(' - To use ONLY main topic, set specific topics to 0\n'); \ No newline at end of file diff --git a/src/utils/test-config.ts b/src/utils/test-config.ts new file mode 100644 index 0000000..d847d02 --- /dev/null +++ b/src/utils/test-config.ts @@ -0,0 +1,98 @@ +import dotenv from 'dotenv'; +import { monitoringConfig, monitoringSchedule, validateMonitoringConfig } from '../shared/monitoring/monitoring-config'; + +// Load environment variables +dotenv.config(); + +function testConfiguration() { + console.log('๐Ÿงช Testing Monitoring Configuration...\n'); + + // Validate configuration + const validation = validateMonitoringConfig(); + if (!validation.valid) { + console.error('โŒ Configuration validation failed:'); + validation.errors.forEach(error => console.error(` - ${error}`)); + return; + } + + console.log('โœ… Configuration validation passed\n'); + + // Display monitoring configuration + console.log('๐Ÿ“Š Monitoring Configuration:'); + console.log(` Supergroup ID: ${monitoringConfig.supergroup?.chatId || 'Not configured'}`); + console.log(` Main monitoring topic: ${monitoringConfig.supergroup?.topics.allMonitoring || 'Not configured'}`); + console.log(` Admin chats: ${monitoringConfig.adminChatIds.length}`); + console.log(''); + + // Display schedule configuration + console.log('โฐ Schedule Configuration:'); + console.log(` Daily report time: ${monitoringSchedule.dailyReportTime} UTC`); + console.log(` Health check interval: ${monitoringSchedule.healthCheckInterval} minutes`); + console.log(` Log cleanup interval: ${monitoringSchedule.logCleanupInterval} hours`); + console.log(` Log retention: ${monitoringSchedule.logRetentionDays} days`); + console.log(''); + + // Display threshold configuration + console.log('๐Ÿšจ Alert Thresholds:'); + console.log(` Memory usage: ${monitoringSchedule.memoryAlertThreshold}%`); + console.log(` API failure rate: ${monitoringSchedule.apiFailureAlertThreshold}%`); + console.log(` Invalid login attempts: ${monitoringSchedule.invalidLoginThreshold} per hour`); + console.log(''); + + // Calculate next daily report time + const [hours, minutes] = monitoringSchedule.dailyReportTime.split(':').map(Number); + const nextReport = new Date(); + nextReport.setUTCHours(hours, minutes, 0, 0); + + if (nextReport <= new Date()) { + nextReport.setUTCDate(nextReport.getUTCDate() + 1); + } + + console.log('๐Ÿ“… Next Scheduled Events:'); + console.log(` Next daily report: ${nextReport.toISOString()}`); + console.log(` Health checks: Every ${monitoringSchedule.healthCheckInterval} minutes`); + console.log(` Log cleanup: Every ${monitoringSchedule.logCleanupInterval} hours`); + console.log(''); + + // Display specific topics if configured + if (monitoringConfig.supergroup?.topics) { + const specificTopics = Object.entries(monitoringConfig.supergroup.topics) + .filter(([key, value]) => key !== 'allMonitoring' && value !== undefined); + + if (specificTopics.length > 0) { + console.log('๐ŸŽฏ Specific Topics:'); + specificTopics.forEach(([key, value]) => { + console.log(` ${key}: ${value}`); + }); + } else { + console.log('๐ŸŽฏ Using main monitoring topic only'); + } + console.log(''); + } + + // Environment variables check + console.log('๐Ÿ” Environment Variables Status:'); + const envVars = [ + 'TELEGRAM_BOT_TOKEN', + 'MONITORING_SUPERGROUP_ID', + 'TOPIC_ALL_MONITORING', + 'DAILY_REPORT_TIME', + 'HEALTH_CHECK_INTERVAL', + 'LOG_CLEANUP_INTERVAL', + 'LOG_RETENTION_DAYS', + 'MEMORY_ALERT_THRESHOLD', + 'API_FAILURE_ALERT_THRESHOLD', + 'INVALID_LOGIN_THRESHOLD' + ]; + + envVars.forEach(varName => { + const value = process.env[varName]; + const status = value ? 'โœ…' : 'โŒ'; + console.log(` ${status} ${varName}: ${value || 'Not set'}`); + }); + + console.log('\n๐ŸŽ‰ Configuration test completed!'); +} + +// Run the test +testConfiguration(); \ No newline at end of file diff --git a/src/utils/test-single-alert.ts b/src/utils/test-single-alert.ts new file mode 100644 index 0000000..1a3dace --- /dev/null +++ b/src/utils/test-single-alert.ts @@ -0,0 +1,45 @@ +import TelegramBot from 'node-telegram-bot-api'; +import dotenv from 'dotenv'; +import { AdminNotifier } from '../shared/monitoring/admin-notifier'; +import { monitoringConfig } from '../shared/monitoring/monitoring-config'; + +// Load environment variables +dotenv.config(); + +async function testSingleAlert() { + console.log('๐Ÿงช Testing single monitoring alert...\n'); + + const token = process.env.TELEGRAM_BOT_TOKEN; + if (!token) { + console.error('โŒ TELEGRAM_BOT_TOKEN not found'); + process.exit(1); + } + + // Initialize bot and monitoring + const bot = new TelegramBot(token); + const adminNotifier = new AdminNotifier(bot, monitoringConfig); + + console.log('๐Ÿ“Š Configuration:'); + console.log(` Supergroup ID: ${monitoringConfig.supergroup?.chatId}`); + console.log(` Main monitoring topic: ${monitoringConfig.supergroup?.topics.allMonitoring}`); + console.log(''); + + try { + console.log('๐Ÿš€ Sending test startup alert...'); + await adminNotifier.sendStartupAlert('๐Ÿงช Test alert - Bot monitoring system is working!', { + testTime: new Date().toISOString(), + version: '1.0.0', + environment: 'test' + }); + console.log('โœ… Test alert sent successfully!'); + console.log('๐Ÿ“ฑ Check your Telegram monitoring topic (ID: 26) for the test message.'); + + } catch (error) { + console.error('โŒ Test failed:', error); + } + + process.exit(0); +} + +// Run the test +testSingleAlert().catch(console.error); \ No newline at end of file