Minor changes

This commit is contained in:
Nguyen Ngo
2025-08-12 17:35:43 -04:00
parent bcfaf5f6f5
commit cad78e6f60
2 changed files with 307 additions and 57 deletions
+136 -39
View File
@@ -1979,95 +1979,192 @@ def admin_logs():
@app.route('/api/logs/recent') @app.route('/api/logs/recent')
@admin_required @admin_required
def api_recent_logs(): def api_recent_logs():
"""API endpoint to get recent log entries""" """Enhanced API endpoint to get recent log entries with filtering"""
try: 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) 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 = """ # Get logs using the enhanced method
SELECT event_type, event_category, event_description, logs = logger_handler.get_recent_logs(
severity_level, created_timestamp, username, ip_address days=days,
FROM log_events limit=limit,
WHERE created_timestamp >= :cutoff_date category_filter=category_filter,
ORDER BY created_timestamp DESC severity_filter=severity_filter,
LIMIT :limit search_term=search_term
""" )
result = db.session.execute(text(logs_sql), { # Log the API access for audit purposes
'cutoff_date': cutoff_date, logger_handler.log_security_event(
'limit': limit event_type="admin_logs_accessed",
}).fetchall() description=f"Admin {session.get('username', 'unknown')} accessed log data (days={days}, limit={limit})",
severity="LOW",
logs = [] additional_data={
for row in result: 'filters': {
logs.append({ 'category': category_filter,
'event_type': row.event_type, 'severity': severity_filter,
'event_category': row.event_category, 'search': search_term
'description': row.event_description, }
'severity': row.severity_level, }
'timestamp': row.created_timestamp.isoformat(), )
'username': row.username,
'ip_address': row.ip_address
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'logs': logs, '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: except Exception as e:
logger_handler.log_database_error('api_recent_logs', e) logger_handler.log_database_error('api_recent_logs', e)
print(f"Error in api_recent_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Failed to fetch recent logs' 'error': f'Failed to fetch recent logs: {str(e)}',
'logs': [],
'total': 0
}), 500 }), 500
@app.route('/api/logs/stats') @app.route('/api/logs/stats')
@admin_required @admin_required
def api_log_stats(): def api_log_stats():
"""API endpoint to get logging statistics""" """Enhanced API endpoint to get logging statistics"""
try: try:
days = request.args.get('days', 7, type=int) 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) 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({ return jsonify({
'success': True, 'success': True,
'stats': stats, 'stats': enhanced_stats,
'days': days 'days': days,
'generated_at': datetime.now().isoformat()
}) })
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_log_stats', e) logger_handler.log_database_error('api_log_stats', e)
print(f"Error in api_log_stats: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Failed to fetch log statistics' 'error': f'Failed to fetch log statistics: {str(e)}',
'stats': {}
}), 500 }), 500
@app.route('/api/logs/cleanup', methods=['POST']) @app.route('/api/logs/cleanup', methods=['POST'])
@admin_required @admin_required
def api_cleanup_logs(): def api_cleanup_logs():
"""API endpoint to cleanup old log entries""" """Enhanced API endpoint to cleanup old log entries"""
try: 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) deleted_count = logger_handler.cleanup_old_logs(days_to_keep=days_to_keep)
# Log the cleanup operation # Enhanced logging for audit trail
logger_handler.logger.info(f"Log cleanup completed: {deleted_count} entries removed") 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({ return jsonify({
'success': True, 'success': True,
'deleted_count': deleted_count, '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: except Exception as e:
logger_handler.log_database_error('api_cleanup_logs', e) logger_handler.log_database_error('api_cleanup_logs', e)
print(f"Error in api_cleanup_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Failed to cleanup old logs' 'error': f'Failed to cleanup old logs: {str(e)}'
}), 500 }), 500
# PROJECT MANAGEMENT ROUTES # PROJECT MANAGEMENT ROUTES
+169 -16
View File
@@ -600,43 +600,92 @@ class AppLogger:
try: try:
cutoff_date = datetime.now() - timedelta(days=days) 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 SELECT
event_category, event_category,
severity_level,
COUNT(*) as event_count COUNT(*) as event_count
FROM log_events FROM log_events
WHERE created_timestamp >= :cutoff_date WHERE created_timestamp >= :cutoff_date
GROUP BY event_category, severity_level GROUP BY event_category
ORDER BY event_count DESC
""" """
result = self.db.session.execute(text(stats_sql), { category_result = self.db.session.execute(text(category_sql), {
'cutoff_date': cutoff_date 'cutoff_date': cutoff_date
}).fetchall() }).fetchall()
stats = {} # Build simple statistics dictionary (not nested)
for row in result: stats = {
category = row.event_category 'total_events': total_result.total_events if total_result else 0,
if category not in stats: 'security_events': 0,
stats[category] = {} 'database_errors': 0,
stats[category][row.severity_level] = row.event_count '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 return stats
except Exception as e: except Exception as e:
self.log_database_error('get_log_statistics', 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): def cleanup_old_logs(self, days_to_keep=90):
"""Clean up old log entries from database""" """Clean up old log entries from database"""
try: try:
cutoff_date = datetime.now() - timedelta(days=days_to_keep) 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 = """ cleanup_sql = """
DELETE FROM log_events DELETE FROM log_events
WHERE created_timestamp < :cutoff_date 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), { result = self.db.session.execute(text(cleanup_sql), {
@@ -646,13 +695,91 @@ class AppLogger:
deleted_count = result.rowcount deleted_count = result.rowcount
self.db.session.commit() 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 return deleted_count
except Exception as e: except Exception as e:
self.db.session.rollback()
self.log_database_error('cleanup_old_logs', e) self.log_database_error('cleanup_old_logs', e)
print(f"Error in cleanup_old_logs: {e}")
return 0 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 # DECORATOR FUNCTIONS FOR AUTOMATIC LOGGING
@@ -693,7 +820,6 @@ def log_user_activity(activity_type):
return decorated_function return decorated_function
return decorator return decorator
def log_database_operations(operation_name): def log_database_operations(operation_name):
"""Decorator to automatically log database operations""" """Decorator to automatically log database operations"""
def decorator(f): def decorator(f):
@@ -716,9 +842,36 @@ def log_database_operations(operation_name):
return decorated_function return decorated_function
return decorator 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 # INITIALIZATION FUNCTION
def init_logging(app, db): def init_logging(app, db):
"""Initialize the logging system with the Flask app""" """Initialize the logging system with the Flask app"""
logger_handler = AppLogger(app, db) logger_handler = AppLogger(app, db)