Log page changes

This commit is contained in:
2025-08-12 21:27:59 -04:00
parent cad78e6f60
commit 2456a1e43f
5 changed files with 1289 additions and 938 deletions
+79 -88
View File
@@ -1979,57 +1979,64 @@ 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():
"""Enhanced API endpoint to get recent log entries with filtering""" """API endpoint to get recent log entries with full details"""
try: try:
# Get parameters days = request.args.get('days', 1, type=int)
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)
# Verify log table exists cutoff_date = datetime.now() - timedelta(days=days)
if not logger_handler.verify_log_table_exists():
return jsonify({
'success': False,
'error': 'Log table not found or inaccessible',
'logs': [],
'total': 0
}), 500
# Get logs using the enhanced method # Enhanced SQL to get all available fields
logs = logger_handler.get_recent_logs( logs_sql = """
days=days, SELECT
limit=limit, event_id,
category_filter=category_filter, event_type,
severity_filter=severity_filter, event_category,
search_term=search_term event_description,
) event_data,
severity_level,
created_timestamp,
username,
user_id,
ip_address
FROM log_events
WHERE created_timestamp >= :cutoff_date
ORDER BY created_timestamp DESC
LIMIT :limit
"""
# Log the API access for audit purposes result = db.session.execute(text(logs_sql), {
logger_handler.log_security_event( 'cutoff_date': cutoff_date,
event_type="admin_logs_accessed", 'limit': limit
description=f"Admin {session.get('username', 'unknown')} accessed log data (days={days}, limit={limit})", }).fetchall()
severity="LOW",
additional_data={ logs = []
'filters': { for row in result:
'category': category_filter, # Parse event_data if it's JSON
'severity': severity_filter, event_data = None
'search': search_term if row.event_data:
} try:
} event_data = json.loads(row.event_data) if isinstance(row.event_data, str) else row.event_data
) except (json.JSONDecodeError, TypeError):
event_data = row.event_data
logs.append({
'event_id': row.event_id,
'event_type': row.event_type,
'event_category': row.event_category,
'description': row.event_description,
'event_data': event_data,
'severity': row.severity_level,
'timestamp': row.created_timestamp.isoformat(),
'username': row.username or 'System',
'user_id': row.user_id,
'ip_address': row.ip_address or '-'
})
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:
@@ -2037,111 +2044,95 @@ def api_recent_logs():
print(f"Error in api_recent_logs: {e}") print(f"Error in api_recent_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to fetch recent logs: {str(e)}', '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():
"""Enhanced API endpoint to get logging statistics""" """API endpoint to get logging statistics"""
try: try:
days = request.args.get('days', 7, type=int) days = request.args.get('days', 7, type=int)
print(f"📊 Getting log statistics for last {days} days")
# Verify log table exists # Get statistics from logger handler
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)
print(f"📈 Retrieved stats: {stats}")
# Add some additional metadata # Ensure all expected keys exist
enhanced_stats = { expected_stats = {
'total_events': stats.get('total_events', 0), 'total_events': stats.get('total_events', 0),
'security_events': stats.get('security_events', 0), 'security_events': stats.get('security_events', 0),
'database_errors': stats.get('database_errors', 0), 'database_errors': stats.get('database_errors', 0),
'user_activities': stats.get('user_activities', 0), 'user_activities': stats.get('user_activities', 0),
'system_events': stats.get('system_events', 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': enhanced_stats, 'stats': expected_stats,
'days': days, 'days': days,
'generated_at': datetime.now().isoformat() 'timestamp': 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}") print(f"Error in api_log_stats: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to fetch log statistics: {str(e)}', 'error': f'Failed to fetch log statistics: {str(e)}',
'stats': {} 'stats': {
'total_events': 0,
'security_events': 0,
'database_errors': 0,
'user_activities': 0,
'system_events': 0
}
}), 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():
"""Enhanced API endpoint to cleanup old log entries""" """API endpoint to cleanup old log entries"""
try: try:
# Get parameters from request # Get JSON data
data = request.get_json() data = request.get_json()
if not data: if not data:
print("❌ No JSON data provided")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'No JSON data provided' 'error': 'No JSON data provided'
}), 400 }), 400
days_to_keep = data.get('days_to_keep', 90) days_to_keep = data.get('days_to_keep', 90)
print(f"🧹 Cleanup request: keep last {days_to_keep} days")
# Validate input # Validate input
if not isinstance(days_to_keep, int) or days_to_keep < 7: if not isinstance(days_to_keep, int) or days_to_keep < 7:
print(f"❌ Invalid days_to_keep: {days_to_keep}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'days_to_keep must be an integer >= 7' 'error': 'days_to_keep must be an integer >= 7'
}), 400 }), 400
if days_to_keep > 365: if days_to_keep > 365:
print(f"❌ days_to_keep too large: {days_to_keep}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'days_to_keep cannot exceed 365 days' 'error': 'days_to_keep cannot exceed 365 days'
}), 400 }), 400
# Verify log table exists # Perform cleanup using logger handler
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)
# Enhanced logging for audit trail
admin_username = session.get('username', 'unknown') admin_username = session.get('username', 'unknown')
print(f"✅ Cleanup completed by {admin_username}: {deleted_count} records deleted")
# Log the admin action
logger_handler.log_security_event( logger_handler.log_security_event(
event_type="admin_log_cleanup", event_type="admin_log_cleanup",
description=f"Admin {admin_username} performed log cleanup: {deleted_count} entries removed (keeping last {days_to_keep} days)", 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 severity="HIGH",
additional_data={ additional_data={
'admin_user': admin_username, 'admin_user': admin_username,
'days_to_keep': days_to_keep, 'days_to_keep': days_to_keep,
@@ -2161,7 +2152,7 @@ def api_cleanup_logs():
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}") print(f"Error in api_cleanup_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to cleanup old logs: {str(e)}' 'error': f'Failed to cleanup old logs: {str(e)}'
+114 -85
View File
@@ -23,7 +23,7 @@ import logging.handlers
import json import json
import os import os
import traceback import traceback
from datetime import datetime, date from datetime import datetime, date, timedelta
from functools import wraps from functools import wraps
from flask import request, session, g from flask import request, session, g
from sqlalchemy import text from sqlalchemy import text
@@ -594,63 +594,82 @@ class AppLogger:
) )
# UTILITY METHODS # UTILITY METHODS
def get_log_statistics(self, days=7): def get_log_statistics(self, days=7):
"""Get logging statistics for the specified number of days""" """Get logging statistics for the specified number of days"""
try: try:
from datetime import datetime, timedelta # Import here as backup
cutoff_date = datetime.now() - timedelta(days=days) cutoff_date = datetime.now() - timedelta(days=days)
# Get total events # Initialize default stats
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,
COUNT(*) as event_count
FROM log_events
WHERE created_timestamp >= :cutoff_date
GROUP BY event_category
"""
category_result = self.db.session.execute(text(category_sql), {
'cutoff_date': cutoff_date
}).fetchall()
# Build simple statistics dictionary (not nested)
stats = { stats = {
'total_events': total_result.total_events if total_result else 0, 'total_events': 0,
'security_events': 0, 'security_events': 0,
'database_errors': 0, 'database_errors': 0,
'user_activities': 0, 'user_activities': 0,
'system_events': 0 'system_events': 0
} }
# Process category results # Check if table exists first
for row in category_result: try:
if row.event_category == 'security': table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone()
stats['security_events'] = row.event_count if not table_check:
elif row.event_category == 'database': print("⚠️ log_events table does not exist")
stats['database_errors'] = row.event_count return stats
elif row.event_category == 'user_activity': except Exception as table_error:
stats['user_activities'] = row.event_count print(f"⚠️ Cannot check if log_events table exists: {table_error}")
elif row.event_category == 'system': return stats
stats['system_events'] = row.event_count
# Get total events count
try:
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()
if total_result:
stats['total_events'] = total_result.total_events
print(f"✅ Found {stats['total_events']} total events in last {days} days")
except Exception as total_error:
print(f"⚠️ Error getting total events: {total_error}")
# Get events by category
try:
category_sql = """
SELECT
event_category,
COUNT(*) as event_count
FROM log_events
WHERE created_timestamp >= :cutoff_date
GROUP BY event_category
"""
category_result = self.db.session.execute(text(category_sql), {'cutoff_date': cutoff_date}).fetchall()
for row in category_result:
category = row.event_category
count = row.event_count
print(f"✅ Found {count} events in category: {category}")
if category == 'security':
stats['security_events'] = count
elif category == 'database':
stats['database_errors'] = count
elif category == 'user_activity':
stats['user_activities'] = count
elif category == 'system':
stats['system_events'] = count
except Exception as category_error:
print(f"⚠️ Error getting category stats: {category_error}")
print(f"📊 Final stats: {stats}")
return stats return stats
except Exception as e: except Exception as e:
print(f"❌ Error in get_log_statistics: {e}")
self.log_database_error('get_log_statistics', e) self.log_database_error('get_log_statistics', e)
print(f"Error in get_log_statistics: {e}")
# Return default stats structure
return { return {
'total_events': 0, 'total_events': 0,
'security_events': 0, 'security_events': 0,
@@ -662,60 +681,70 @@ class AppLogger:
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:
from datetime import datetime, timedelta # Import here as backup
cutoff_date = datetime.now() - timedelta(days=days_to_keep) cutoff_date = datetime.now() - timedelta(days=days_to_keep)
print(f"🧹 Starting log cleanup: removing entries older than {cutoff_date}")
# Check if table exists first
try:
table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone()
if not table_check:
print("⚠️ log_events table does not exist")
return 0
except Exception as table_error:
print(f"⚠️ Cannot check if log_events table exists: {table_error}")
return 0
# First, count how many records will be deleted # First, count how many records will be deleted
count_sql = """ try:
SELECT COUNT(*) as count_to_delete count_sql = """
FROM log_events SELECT COUNT(*) as count_to_delete
WHERE created_timestamp < :cutoff_date FROM log_events
AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH') 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 count_result = self.db.session.execute(text(count_sql), {'cutoff_date': cutoff_date}).fetchone()
}).fetchone() count_to_delete = count_result.count_to_delete if count_result else 0
count_to_delete = count_result.count_to_delete if count_result else 0 print(f"📊 Found {count_to_delete} records to delete")
if count_to_delete == 0: if count_to_delete == 0:
print("✅ No old records found to cleanup")
return 0
except Exception as count_error:
print(f"⚠️ Error counting records to delete: {count_error}")
return 0 return 0
# Perform the cleanup - exclude critical logs # Perform the cleanup - exclude critical logs
cleanup_sql = """ try:
DELETE FROM log_events cleanup_sql = """
WHERE created_timestamp < :cutoff_date DELETE FROM log_events
AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH') WHERE created_timestamp < :cutoff_date
""" AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH')
"""
result = self.db.session.execute(text(cleanup_sql), {
'cutoff_date': cutoff_date result = self.db.session.execute(text(cleanup_sql), {'cutoff_date': cutoff_date})
}) deleted_count = result.rowcount
self.db.session.commit()
deleted_count = result.rowcount
self.db.session.commit() print(f"🗑️ Successfully deleted {deleted_count} old log entries")
# Log the cleanup operation # Log the cleanup operation
self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries older than {days_to_keep} days)") self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries newer than {days_to_keep} days)")
# Also log to security log for audit return deleted_count
self.log_security_event(
event_type="log_cleanup", except Exception as delete_error:
description=f"Admin cleaned up {deleted_count} old log entries (keeping last {days_to_keep} days)", print(f"❌ Error during deletion: {delete_error}")
severity="MEDIUM", self.db.session.rollback()
additional_data={ return 0
'days_to_keep': days_to_keep,
'deleted_count': deleted_count,
'cutoff_date': cutoff_date.isoformat()
}
)
return deleted_count
except Exception as e: except Exception as e:
print(f"❌ Error in cleanup_old_logs: {e}")
self.db.session.rollback() 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): def get_recent_logs(self, days=7, limit=100, category_filter=None, severity_filter=None, search_term=None):
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1093 -760
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -24,7 +24,7 @@
<link <link
rel="icon" rel="icon"
type="image/x-icon" type="image/x-icon"
href="{{ url_for('static', filename='favicon.ico') }}" href="{{ url_for('static', filename='images/favicon.ico') }}"
/> />
</head> </head>
<body class="login-layout"> <body class="login-layout">
@@ -52,9 +52,7 @@
{% endif %} {% endwith %} {% endif %} {% endwith %}
<!-- Page Content --> <!-- Page Content -->
<div class="container"> <div class="container">{% block content %}{% endblock %}</div>
{% block content %}{% endblock %}
</div>
</main> </main>
<!-- Footer --> <!-- Footer -->
@@ -88,4 +86,4 @@
}; };
</script> </script>
</body> </body>
</html> </html>