feat(monitoring): Add comprehensive monitoring and alerting system
This commit is contained in:
parent
23a455db95
commit
2f24310c24
56
.env.example
56
.env.example
|
|
@ -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
251
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
|
- **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]
|
||||||
|
|
@ -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
81
scripts/get-group-info.ts
Normal 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');
|
||||||
129
src/bot/bot.ts
129
src/bot/bot.ts
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
269
src/shared/monitoring/daily-reporter.ts
Normal file
269
src/shared/monitoring/daily-reporter.ts
Normal 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)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/shared/monitoring/database-logger.example.ts
Normal file
67
src/shared/monitoring/database-logger.example.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
129
src/shared/monitoring/log-backup.ts
Normal file
129
src/shared/monitoring/log-backup.ts
Normal 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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
53
src/shared/monitoring/monitoring-config.example.ts
Normal file
53
src/shared/monitoring/monitoring-config.example.ts
Normal 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
|
||||||
|
*/
|
||||||
112
src/shared/monitoring/monitoring-config.ts
Normal file
112
src/shared/monitoring/monitoring-config.ts
Normal 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}`));
|
||||||
|
}
|
||||||
177
src/shared/monitoring/monitoring-setup.example.ts
Normal file
177
src/shared/monitoring/monitoring-setup.example.ts
Normal 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);
|
||||||
|
*/
|
||||||
56
src/utils/monitoring-thresholds.example.ts
Normal file
56
src/utils/monitoring-thresholds.example.ts
Normal 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}%)`);
|
||||||
|
}
|
||||||
|
*/
|
||||||
71
src/utils/setup-main-topic.ts
Normal file
71
src/utils/setup-main-topic.ts
Normal 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
98
src/utils/test-config.ts
Normal 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();
|
||||||
45
src/utils/test-single-alert.ts
Normal file
45
src/utils/test-single-alert.ts
Normal 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);
|
||||||
Loading…
Reference in New Issue
Block a user