feat(monitoring): Add comprehensive monitoring and alerting system

This commit is contained in:
debudebuye 2026-01-10 09:27:28 +03:00
parent 23a455db95
commit 2f24310c24
18 changed files with 1754 additions and 85 deletions

View File

@ -3,3 +3,59 @@ TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
# API Configuration # API Configuration
API_BASE_URL=http://localhost:3000/api 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

251
README.md
View File

@ -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 - **User Authentication** - Phone-based registration and login
- 🏠 **Property Management**: Browse, view, and add properties - **Property Management** - Browse, add, and manage property listings
- 👤 **User Registration**: Complete registration flow with validation - **Role-Based Access** - AGENT role restrictions
- 🔐 **Secure Login**: Password-based authentication for existing users - **Advanced Monitoring** - Topic-based alerts and health checks
- 📋 **Property Listings**: View all available properties - **Configurable Logging** - Flexible log retention and cleanup
- 🏘️ **My Properties**: Manage user's own property listings - **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
``` ## 🛠️ Installation
src/
├── shared/ # Shared utilities and services 1. **Clone the repository**
│ ├── types/ # Common TypeScript interfaces ```bash
│ └── services/ # Shared services (API, Session) git clone <repository-url>
├── features/ # Feature modules cd yaltopia-telegram-bot
│ ├── auth/ # Authentication feature
│ └── properties/ # Property management feature
└── bot/ # Bot orchestration
``` ```
## Setup 2. **Install dependencies**
1. **Install dependencies:**
```bash ```bash
npm install npm install
``` ```
2. **Environment setup:** 3. **Configure environment variables**
```bash ```bash
cp .env.example .env cp .env.example .env
# Edit .env with your actual values
``` ```
Edit `.env` and add your Telegram bot token: 4. **Build the project**
```
TELEGRAM_BOT_TOKEN=your_bot_token_here
API_BASE_URL=http://localhost:3000/api
```
3. **Build the project:**
```bash ```bash
npm run build npm run build
``` ```
4. **Start the bot:** 5. **Start the bot**
```bash ```bash
npm start npm start
``` ```
For development: ## ⚙️ Configuration
### Required Environment Variables
```bash ```bash
npm run dev # 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
``` ```
## Bot Flow ### Optional Configuration
### 1. Authentication Flow ```bash
- User starts with `/start` # Schedule Configuration
- Bot requests phone number DAILY_REPORT_TIME=09:00 # UTC time for daily reports
- If phone exists → Login with password HEALTH_CHECK_INTERVAL=5 # Minutes between health checks
- If new user → Registration flow (name, email, password, confirm) LOG_CLEANUP_INTERVAL=24 # Hours between log cleanup
LOG_RETENTION_DAYS=30 # Days to keep logs
### 2. Main Menu (After Login) # Alert Thresholds
- 🏘️ **Browse Properties**: View all available properties MEMORY_ALERT_THRESHOLD=90 # Memory usage percentage
- 🏠 **My Properties**: View user's own listings API_FAILURE_ALERT_THRESHOLD=5 # API failure rate percentage
- **Add Property**: Create new property listing INVALID_LOGIN_THRESHOLD=50 # Invalid logins per hour
- 👤 **Profile**: View user profile information ```
### 3. Property Creation Flow ## 🔧 Development
- Title → Description → Type (Rent/Sale) → Price → Area → Rooms → Toilets → Subcity → House Type
## API Integration ### Available Scripts
The bot integrates with your existing backend APIs: ```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
```
- `POST /telegram-auth/telegram-register` - User registration ### Setting Up Monitoring
- `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
## Development 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 get-group-info
```
5. **Update your .env file with the IDs**
6. **Test the monitoring:**
```bash
npm run test-monitoring
```
### Scripts ## 📊 Monitoring Features
- `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
### Adding New Features ### Topic-Based Alerts
- **Main Monitoring Topic** - Receives all alerts
- **Specific Topics** - Optional categorized alerts
- **Health Checks** - Automated system monitoring
- **Daily Reports** - Scheduled system summaries
1. Create feature directory in `src/features/` ### Alert Types
2. Add service class for API interactions - 🚀 **Startup Alerts** - Bot initialization
3. Add handler class for bot interactions - 💥 **Error Alerts** - System errors and exceptions
4. Integrate in main bot class (`src/bot/bot.ts`) - 🔥 **Backend Failures** - API and service issues
- 🏥 **Health Alerts** - Memory, performance issues
- 🔐 **Security Alerts** - Invalid login attempts
- 📊 **Daily Reports** - System statistics
## Getting Your Bot Token ## 🔒 Security Features
1. Message [@BotFather](https://t.me/botfather) on Telegram - **Environment Variable Protection** - No hardcoded secrets
2. Use `/newbot` command - **Role-Based Access Control** - AGENT-only access
3. Follow the setup process - **Rate Limiting** - Prevent abuse
4. Copy the token to your `.env` file - **Input Validation** - Secure data handling
- **Login Attempt Monitoring** - Brute force detection
- **Secure Logging** - No sensitive data in logs
## 🚀 Deployment
### Production Checklist
1. **Set production environment variables**
```bash
NODE_ENV=production
BOT_VERSION=1.0.0
```
2. **Configure monitoring thresholds**
```bash
MEMORY_ALERT_THRESHOLD=80
API_FAILURE_ALERT_THRESHOLD=2
INVALID_LOGIN_THRESHOLD=20
```
3. **Set up log management**
```bash
LOG_RETENTION_DAYS=90
LOG_CLEANUP_INTERVAL=24
```
4. **Test all systems**
```bash
npm run test-config
npm run test-monitoring
```
### Docker Deployment (Optional)
```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]

View File

@ -7,7 +7,12 @@
"build": "tsc", "build": "tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"dev": "ts-node src/index.ts", "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": { "dependencies": {
"node-telegram-bot-api": "^0.66.0", "node-telegram-bot-api": "^0.66.0",

81
scripts/get-group-info.ts Normal file
View File

@ -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');

View File

@ -8,6 +8,8 @@ import { PropertiesHandler } from '../features/properties/properties.handler';
import { SystemMonitor } from '../shared/monitoring/system-monitor'; import { SystemMonitor } from '../shared/monitoring/system-monitor';
import { Logger, LogLevel } from '../shared/monitoring/logger'; import { Logger, LogLevel } from '../shared/monitoring/logger';
import { AdminNotifier } from '../shared/monitoring/admin-notifier'; 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 { export class YaltopiaBot {
private bot: TelegramBot; private bot: TelegramBot;
@ -20,26 +22,37 @@ export class YaltopiaBot {
private systemMonitor: SystemMonitor; private systemMonitor: SystemMonitor;
private logger: Logger; private logger: Logger;
private adminNotifier: AdminNotifier; 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.bot = new TelegramBot(token, { polling: true });
this.apiService = new ApiService(apiBaseUrl); this.apiService = new ApiService(apiBaseUrl);
this.sessionService = new SessionService(); this.sessionService = new SessionService();
// Initialize monitoring // Initialize monitoring with topic-based configuration
this.logger = new Logger(LogLevel.INFO); this.logger = new Logger(LogLevel.INFO);
this.systemMonitor = new SystemMonitor({ adminChatId: adminChatIds[0] }); this.adminNotifier = new AdminNotifier(this.bot, monitoringConfig);
this.adminNotifier = new AdminNotifier(this.bot, { adminChatIds }); this.logger.setAdminNotifier(this.adminNotifier);
this.dailyReporter = new DailyReporter(this.logger, this.adminNotifier);
this.systemMonitor = new SystemMonitor({ adminChatId: monitoringConfig.adminChatIds[0] || 0 });
// Initialize services // Initialize services
this.authService = new AuthService(this.apiService); this.authService = new AuthService(this.apiService);
this.propertiesService = new PropertiesService(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.authHandler = new AuthHandler(this.bot, this.authService, this.sessionService, this.apiService);
this.propertiesHandler = new PropertiesHandler(this.bot, this.propertiesService, this.sessionService); this.propertiesHandler = new PropertiesHandler(this.bot, this.propertiesService, this.sessionService);
this.setupHandlers(); this.setupHandlers();
this.setupMonitoring();
} }
private setupHandlers(): void { 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<void> {
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<void> { private async handleTextMessage(chatId: number, text: string): Promise<void> {
const session = this.sessionService.getSession(chatId); const session = this.sessionService.getSession(chatId);

View File

@ -55,7 +55,7 @@ export class AuthHandler {
console.log(`🔐 User ${chatId} - AGENT account found, directing to login`); console.log(`🔐 User ${chatId} - AGENT account found, directing to login`);
this.sessionService.setSessionStep(chatId, 'LOGIN_PASSWORD', { phone: normalizedPhone }); this.sessionService.setSessionStep(chatId, 'LOGIN_PASSWORD', { phone: normalizedPhone });
await this.bot.sendMessage(chatId, await this.bot.sendMessage(chatId,
'<EFBFBD> AGENT aoccount found! Please enter your password:', '🔐 AGENT account found! Please enter your password:',
{ reply_markup: { remove_keyboard: true } } { reply_markup: { remove_keyboard: true } }
); );
} else if (phoneCheck.exists && !phoneCheck.isAgent) { } else if (phoneCheck.exists && !phoneCheck.isAgent) {

View File

@ -11,8 +11,23 @@ export interface AdminConfig {
pass: string; pass: string;
to: 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 { export class AdminNotifier {
private bot: TelegramBot; private bot: TelegramBot;
private config: AdminConfig; 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<void> {
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<void> {
await this.sendTopicAlert('start', 'info', '🚀 Bot Startup', message, data);
}
async sendErrorAlert(error: string, userId?: number, context?: any): Promise<void> {
await this.sendTopicAlert('error', 'critical', '❌ System Error', error, { userId, context });
}
async sendBackendFailAlert(service: string, error: string, data?: any): Promise<void> {
await this.sendTopicAlert('backendFails', 'critical', '🔥 Backend Failure', `${service}: ${error}`, data);
}
async sendHealthAlert(status: 'healthy' | 'warning' | 'critical', issues: string[]): Promise<void> {
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<void> {
await this.sendTopicAlert('dailyReport', 'info', '📊 Daily Report', report, metrics);
}
async sendInvalidLoginAlert(userId: number, attempts: number, ip?: string, userAgent?: string): Promise<void> {
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<void> { async sendSystemReport(report: string): Promise<void> {
const chunks = this.splitMessage(report, 4000); // Telegram message limit const chunks = this.splitMessage(report, 4000); // Telegram message limit
@ -119,6 +210,66 @@ export class AdminNotifier {
return formatted; 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 { private formatErrorSummary(errors: Array<{ timestamp: Date; error: string; userId?: number; context?: string }>): string {
const recentErrors = errors.slice(0, 10); // Show last 10 errors const recentErrors = errors.slice(0, 10); // Show last 10 errors

View File

@ -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<void> {
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<DailyMetrics> {
// 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<any> {
// 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)`);
}
}

View File

@ -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<void> {
// 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<DatabaseLogEntry[]> {
// 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<DatabaseLogEntry[]> {
// 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
});
}
}
}
*/

View File

@ -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<void> {
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<void> {
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)
});
}
}
}

View File

@ -1,5 +1,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { AdminNotifier } from './admin-notifier';
import { monitoringSchedule } from './monitoring-config';
export enum LogLevel { export enum LogLevel {
ERROR = 0, ERROR = 0,
@ -22,17 +24,20 @@ export class Logger {
private logDir: string; private logDir: string;
private maxFileSize: number; private maxFileSize: number;
private maxFiles: number; private maxFiles: number;
private adminNotifier?: AdminNotifier;
constructor( constructor(
logLevel: LogLevel = LogLevel.INFO, logLevel: LogLevel = LogLevel.INFO,
logDir: string = './logs', logDir: string = './logs',
maxFileSize: number = 10 * 1024 * 1024, // 10MB maxFileSize: number = 10 * 1024 * 1024, // 10MB
maxFiles: number = 5 maxFiles: number = 5,
adminNotifier?: AdminNotifier
) { ) {
this.logLevel = logLevel; this.logLevel = logLevel;
this.logDir = logDir; this.logDir = logDir;
this.maxFileSize = maxFileSize; this.maxFileSize = maxFileSize;
this.maxFiles = maxFiles; this.maxFiles = maxFiles;
this.adminNotifier = adminNotifier;
// Create logs directory if it doesn't exist // Create logs directory if it doesn't exist
if (!fs.existsSync(logDir)) { if (!fs.existsSync(logDir)) {
@ -40,6 +45,10 @@ export class Logger {
} }
} }
setAdminNotifier(adminNotifier: AdminNotifier): void {
this.adminNotifier = adminNotifier;
}
private shouldLog(level: LogLevel): boolean { private shouldLog(level: LogLevel): boolean {
return level <= this.logLevel; return level <= this.logLevel;
} }
@ -59,6 +68,21 @@ export class Logger {
return logLine; 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 { private writeToFile(entry: LogEntry): void {
const filename = `bot-${new Date().toISOString().split('T')[0]}.log`; const filename = `bot-${new Date().toISOString().split('T')[0]}.log`;
const filepath = path.join(this.logDir, filename); const filepath = path.join(this.logDir, filename);
@ -115,8 +139,14 @@ export class Logger {
stack: error?.stack stack: error?.stack
}; };
console.error(`🚨 ${this.formatLogEntry(entry)}`); console.error(`🚨 ${this.formatConsoleEntry(entry)}`);
this.writeToFile(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 { warn(message: string, userId?: number, context?: any): void {
@ -130,7 +160,7 @@ export class Logger {
context context
}; };
console.warn(`⚠️ ${this.formatLogEntry(entry)}`); console.warn(`⚠️ ${this.formatConsoleEntry(entry)}`);
this.writeToFile(entry); this.writeToFile(entry);
} }
@ -145,7 +175,7 @@ export class Logger {
context context
}; };
console.log(` ${this.formatLogEntry(entry)}`); console.log(` ${this.formatConsoleEntry(entry)}`);
this.writeToFile(entry); this.writeToFile(entry);
} }
@ -160,10 +190,51 @@ export class Logger {
context context
}; };
console.log(`🐛 ${this.formatLogEntry(entry)}`); console.log(`🐛 ${this.formatConsoleEntry(entry)}`);
this.writeToFile(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 // Structured logging methods
userAction(userId: number, action: string, success: boolean, details?: any): void { userAction(userId: number, action: string, success: boolean, details?: any): void {
this.info(`User action: ${action}`, userId, { success, ...details }); this.info(`User action: ${action}`, userId, { success, ...details });
@ -227,7 +298,7 @@ export class Logger {
} }
// Clean old log files // Clean old log files
cleanOldLogs(daysToKeep: number = 30): void { cleanOldLogs(daysToKeep: number = monitoringSchedule.logRetentionDays): void {
try { try {
const files = fs.readdirSync(this.logDir); const files = fs.readdirSync(this.logDir);
const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000; const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000;

View File

@ -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
*/

View File

@ -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}`));
}

View File

@ -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<void> {
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<void> {
// 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);
*/

View File

@ -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}%)`);
}
*/

View File

@ -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');

98
src/utils/test-config.ts Normal file
View File

@ -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();

View File

@ -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);