diff --git a/app.py b/app.py index 37bb21d..9a312fc 100644 --- a/app.py +++ b/app.py @@ -1979,95 +1979,192 @@ def admin_logs(): @app.route('/api/logs/recent') @admin_required def api_recent_logs(): - """API endpoint to get recent log entries""" + """Enhanced API endpoint to get recent log entries with filtering""" try: - days = request.args.get('days', 1, type=int) + # Get parameters + days = request.args.get('days', 7, type=int) limit = request.args.get('limit', 50, type=int) + category_filter = request.args.get('category', None) + severity_filter = request.args.get('severity', None) + search_term = request.args.get('search', None) - cutoff_date = datetime.now() - timedelta(days=days) + # Verify log table exists + if not logger_handler.verify_log_table_exists(): + return jsonify({ + 'success': False, + 'error': 'Log table not found or inaccessible', + 'logs': [], + 'total': 0 + }), 500 - logs_sql = """ - SELECT event_type, event_category, event_description, - severity_level, created_timestamp, username, ip_address - FROM log_events - WHERE created_timestamp >= :cutoff_date - ORDER BY created_timestamp DESC - LIMIT :limit - """ + # Get logs using the enhanced method + logs = logger_handler.get_recent_logs( + days=days, + limit=limit, + category_filter=category_filter, + severity_filter=severity_filter, + search_term=search_term + ) - result = db.session.execute(text(logs_sql), { - 'cutoff_date': cutoff_date, - 'limit': limit - }).fetchall() - - logs = [] - for row in result: - logs.append({ - 'event_type': row.event_type, - 'event_category': row.event_category, - 'description': row.event_description, - 'severity': row.severity_level, - 'timestamp': row.created_timestamp.isoformat(), - 'username': row.username, - 'ip_address': row.ip_address - }) + # Log the API access for audit purposes + logger_handler.log_security_event( + event_type="admin_logs_accessed", + description=f"Admin {session.get('username', 'unknown')} accessed log data (days={days}, limit={limit})", + severity="LOW", + additional_data={ + 'filters': { + 'category': category_filter, + 'severity': severity_filter, + 'search': search_term + } + } + ) return jsonify({ 'success': True, 'logs': logs, - 'total': len(logs) + 'total': len(logs), + 'filters_applied': { + 'days': days, + 'category': category_filter, + 'severity': severity_filter, + 'search': search_term + } }) except Exception as e: logger_handler.log_database_error('api_recent_logs', e) + print(f"Error in api_recent_logs: {e}") return jsonify({ 'success': False, - 'error': 'Failed to fetch recent logs' + 'error': f'Failed to fetch recent logs: {str(e)}', + 'logs': [], + 'total': 0 }), 500 @app.route('/api/logs/stats') @admin_required def api_log_stats(): - """API endpoint to get logging statistics""" + """Enhanced API endpoint to get logging statistics""" try: days = request.args.get('days', 7, type=int) + + # Verify log table exists + if not logger_handler.verify_log_table_exists(): + return jsonify({ + 'success': False, + 'error': 'Log table not found or inaccessible', + 'stats': {} + }), 500 + + # Get statistics stats = logger_handler.get_log_statistics(days=days) + # Add some additional metadata + enhanced_stats = { + 'total_events': stats.get('total_events', 0), + 'security_events': stats.get('security_events', 0), + 'database_errors': stats.get('database_errors', 0), + 'user_activities': stats.get('user_activities', 0), + 'system_events': stats.get('system_events', 0), + 'unique_users': stats.get('unique_users', 0), + 'severity_breakdown': { + 'high': stats.get('high_severity', 0), + 'medium': stats.get('medium_severity', 0), + 'low': stats.get('low_severity', 0), + 'info': stats.get('info_severity', 0) + }, + 'category_breakdown': { + 'security': stats.get('security_events', 0), + 'database': stats.get('database_errors', 0), + 'user_activity': stats.get('user_activities', 0), + 'system': stats.get('system_events', 0) + } + } + return jsonify({ 'success': True, - 'stats': stats, - 'days': days + 'stats': enhanced_stats, + 'days': days, + 'generated_at': datetime.now().isoformat() }) except Exception as e: logger_handler.log_database_error('api_log_stats', e) + print(f"Error in api_log_stats: {e}") return jsonify({ 'success': False, - 'error': 'Failed to fetch log statistics' + 'error': f'Failed to fetch log statistics: {str(e)}', + 'stats': {} }), 500 @app.route('/api/logs/cleanup', methods=['POST']) @admin_required def api_cleanup_logs(): - """API endpoint to cleanup old log entries""" + """Enhanced API endpoint to cleanup old log entries""" try: - days_to_keep = request.json.get('days_to_keep', 90) + # Get parameters from request + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'error': 'No JSON data provided' + }), 400 + + days_to_keep = data.get('days_to_keep', 90) + + # Validate input + if not isinstance(days_to_keep, int) or days_to_keep < 7: + return jsonify({ + 'success': False, + 'error': 'days_to_keep must be an integer >= 7' + }), 400 + + if days_to_keep > 365: + return jsonify({ + 'success': False, + 'error': 'days_to_keep cannot exceed 365 days' + }), 400 + + # Verify log table exists + if not logger_handler.verify_log_table_exists(): + return jsonify({ + 'success': False, + 'error': 'Log table not found or inaccessible' + }), 500 + + # Perform cleanup deleted_count = logger_handler.cleanup_old_logs(days_to_keep=days_to_keep) - # Log the cleanup operation - logger_handler.logger.info(f"Log cleanup completed: {deleted_count} entries removed") + # Enhanced logging for audit trail + admin_username = session.get('username', 'unknown') + logger_handler.log_security_event( + event_type="admin_log_cleanup", + description=f"Admin {admin_username} performed log cleanup: {deleted_count} entries removed (keeping last {days_to_keep} days)", + severity="HIGH", # High because this is a data deletion operation + additional_data={ + 'admin_user': admin_username, + 'days_to_keep': days_to_keep, + 'deleted_count': deleted_count, + 'ip_address': request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr) + } + ) return jsonify({ 'success': True, 'deleted_count': deleted_count, - 'message': f'Successfully cleaned up {deleted_count} old log entries' + 'days_to_keep': days_to_keep, + 'message': f'Successfully cleaned up {deleted_count} old log entries (keeping last {days_to_keep} days)', + 'performed_by': admin_username, + 'performed_at': datetime.now().isoformat() }) except Exception as e: logger_handler.log_database_error('api_cleanup_logs', e) + print(f"Error in api_cleanup_logs: {e}") return jsonify({ 'success': False, - 'error': 'Failed to cleanup old logs' + 'error': f'Failed to cleanup old logs: {str(e)}' }), 500 # PROJECT MANAGEMENT ROUTES diff --git a/logger_handler.py b/logger_handler.py index b57d05a..9a1d91e 100644 --- a/logger_handler.py +++ b/logger_handler.py @@ -600,43 +600,92 @@ class AppLogger: try: cutoff_date = datetime.now() - timedelta(days=days) - stats_sql = """ + # Get total events + total_sql = """ + SELECT COUNT(*) as total_events + FROM log_events + WHERE created_timestamp >= :cutoff_date + """ + + total_result = self.db.session.execute(text(total_sql), { + 'cutoff_date': cutoff_date + }).fetchone() + + # Get events by category + category_sql = """ SELECT event_category, - severity_level, COUNT(*) as event_count FROM log_events WHERE created_timestamp >= :cutoff_date - GROUP BY event_category, severity_level - ORDER BY event_count DESC + GROUP BY event_category """ - result = self.db.session.execute(text(stats_sql), { + category_result = self.db.session.execute(text(category_sql), { 'cutoff_date': cutoff_date }).fetchall() - stats = {} - for row in result: - category = row.event_category - if category not in stats: - stats[category] = {} - stats[category][row.severity_level] = row.event_count + # Build simple statistics dictionary (not nested) + stats = { + 'total_events': total_result.total_events if total_result else 0, + 'security_events': 0, + 'database_errors': 0, + 'user_activities': 0, + 'system_events': 0 + } + + # Process category results + for row in category_result: + if row.event_category == 'security': + stats['security_events'] = row.event_count + elif row.event_category == 'database': + stats['database_errors'] = row.event_count + elif row.event_category == 'user_activity': + stats['user_activities'] = row.event_count + elif row.event_category == 'system': + stats['system_events'] = row.event_count return stats except Exception as e: self.log_database_error('get_log_statistics', e) - return {} + print(f"Error in get_log_statistics: {e}") + # Return default stats structure + return { + 'total_events': 0, + 'security_events': 0, + 'database_errors': 0, + 'user_activities': 0, + 'system_events': 0 + } def cleanup_old_logs(self, days_to_keep=90): """Clean up old log entries from database""" try: cutoff_date = datetime.now() - timedelta(days=days_to_keep) + # First, count how many records will be deleted + count_sql = """ + SELECT COUNT(*) as count_to_delete + FROM log_events + WHERE created_timestamp < :cutoff_date + AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH') + """ + + count_result = self.db.session.execute(text(count_sql), { + 'cutoff_date': cutoff_date + }).fetchone() + + count_to_delete = count_result.count_to_delete if count_result else 0 + + if count_to_delete == 0: + return 0 + + # Perform the cleanup - exclude critical logs cleanup_sql = """ DELETE FROM log_events WHERE created_timestamp < :cutoff_date - AND severity_level NOT IN ('ERROR', 'CRITICAL') + AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH') """ result = self.db.session.execute(text(cleanup_sql), { @@ -646,14 +695,92 @@ class AppLogger: deleted_count = result.rowcount self.db.session.commit() - self.logger.info(f"Cleaned up {deleted_count} old log entries") + # Log the cleanup operation + self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries older than {days_to_keep} days)") + + # Also log to security log for audit + self.log_security_event( + event_type="log_cleanup", + description=f"Admin cleaned up {deleted_count} old log entries (keeping last {days_to_keep} days)", + severity="MEDIUM", + additional_data={ + 'days_to_keep': days_to_keep, + 'deleted_count': deleted_count, + 'cutoff_date': cutoff_date.isoformat() + } + ) + return deleted_count except Exception as e: + self.db.session.rollback() self.log_database_error('cleanup_old_logs', e) + print(f"Error in cleanup_old_logs: {e}") return 0 - +def get_recent_logs(self, days=7, limit=100, category_filter=None, severity_filter=None, search_term=None): + """Enhanced method to get recent logs with filtering options""" + try: + cutoff_date = datetime.now() - timedelta(days=days) + + # Build the base query + base_sql = """ + SELECT + event_id, + event_type, + event_category, + event_description, + severity_level, + created_timestamp, + username, + ip_address, + user_id + FROM log_events + WHERE created_timestamp >= :cutoff_date + """ + + # Add filters + params = {'cutoff_date': cutoff_date} + + if category_filter: + base_sql += " AND event_category = :category_filter" + params['category_filter'] = category_filter + + if severity_filter: + base_sql += " AND severity_level = :severity_filter" + params['severity_filter'] = severity_filter + + if search_term: + base_sql += " AND (event_description LIKE :search_term OR event_type LIKE :search_term OR username LIKE :search_term)" + params['search_term'] = f"%{search_term}%" + + # Add ordering and limit + base_sql += " ORDER BY created_timestamp DESC LIMIT :limit" + params['limit'] = limit + + result = self.db.session.execute(text(base_sql), params).fetchall() + + logs = [] + for row in result: + logs.append({ + 'event_id': row.event_id, + 'event_type': row.event_type, + 'event_category': row.event_category, + 'description': row.event_description, + 'severity': row.severity_level, + 'timestamp': row.created_timestamp.isoformat(), + 'username': row.username or 'System', + 'ip_address': row.ip_address or '-', + 'user_id': row.user_id + }) + + return logs + + except Exception as e: + self.log_database_error('get_recent_logs', e) + print(f"Error in get_recent_logs: {e}") + return [] + # DECORATOR FUNCTIONS FOR AUTOMATIC LOGGING def log_user_activity(activity_type): @@ -693,7 +820,6 @@ def log_user_activity(activity_type): return decorated_function return decorator - def log_database_operations(operation_name): """Decorator to automatically log database operations""" def decorator(f): @@ -716,9 +842,36 @@ def log_database_operations(operation_name): return decorated_function return decorator - +def verify_log_table_exists(self): + """Verify that the log_events table exists and has the correct structure""" + try: + # Check if table exists + check_table_sql = """ + SELECT COUNT(*) as table_exists + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = 'log_events' + """ + + result = self.db.session.execute(text(check_table_sql)).fetchone() + + if result.table_exists == 0: + print("⚠️ log_events table does not exist. Creating it now...") + self._create_log_table() + return True + + # Check if table has records + count_sql = "SELECT COUNT(*) as record_count FROM log_events" + count_result = self.db.session.execute(text(count_sql)).fetchone() + + print(f"✅ log_events table exists with {count_result.record_count} records") + return True + + except Exception as e: + print(f"❌ Error verifying log table: {e}") + return False + # INITIALIZATION FUNCTION - def init_logging(app, db): """Initialize the logging system with the Flask app""" logger_handler = AppLogger(app, db)