b4c3f65636
Moves the Recent Activity card into the main column, directly beneath
Playlist Overview, so the two cards that describe "what the server and
its players are doing" sit together. System Status stays full-width below
the grid.
While moving it, the card was showing almost nothing useful: player
feedback ("Feedback received from ...") is a ~15s heartbeat that makes up
84% of all log rows (423/506) and 18 of the newest 20, so the list was a
wall of repeated one-line status reports that pushed real events off the
screen.
The card is now tabbed:
- "Activity" (default) - uploads, logins, HTTPS changes, deployments...
- "Player heartbeats" - the raw status stream, on demand
get_recent_logs() gains exclude_prefix / include_prefix so the split
happens in SQL. Filtering a single wide window in Python instead gave only
7 useful entries, because the number returned depends on heartbeat volume
and would fall further as more players are added. Both queries now return
25. Prefixes are passed with autoescape=True so a literal '%' or '_' in a
prefix matches itself rather than acting as a LIKE wildcard.
UI: the list is capped at 320px and scrolls, so a long log cannot make
the card dominate the page. Long messages wrap instead of pushing the
timestamp off-screen (previously float:right), and the debug level now
has its own colour instead of rendering as "info" green.
Verified: 25/25 entries per tab, no cross-contamination, no horizontal
overflow at 500-1400px, tabs wrap below the title under ~600px, and dark
mode uses the theme variables.
100 lines
3.1 KiB
Python
100 lines
3.1 KiB
Python
"""Logging utility for tracking system events."""
|
|
from typing import Optional
|
|
from datetime import datetime, timedelta
|
|
|
|
from app.extensions import db
|
|
from app.models.server_log import ServerLog
|
|
|
|
|
|
def log_action(level: str, message: str) -> None:
|
|
"""Log an action to the database with specified level.
|
|
|
|
Args:
|
|
level: Log level (info, warning, error)
|
|
message: Log message content
|
|
"""
|
|
try:
|
|
new_log = ServerLog(level=level, message=message)
|
|
db.session.add(new_log)
|
|
db.session.commit()
|
|
print(f"[{level.upper()}] {message}")
|
|
except Exception as e:
|
|
print(f"Error logging action: {e}")
|
|
db.session.rollback()
|
|
|
|
|
|
def get_recent_logs(limit: int = 20, level: Optional[str] = None,
|
|
exclude_prefix: Optional[str] = None,
|
|
include_prefix: Optional[str] = None) -> list:
|
|
"""Get the most recent log entries.
|
|
|
|
Args:
|
|
limit: Maximum number of logs to return
|
|
level: Optional filter by log level
|
|
exclude_prefix: Drop entries whose message starts with this text.
|
|
Useful for high-frequency noise: player feedback alone is ~84% of
|
|
all rows, so over-fetching a window and filtering in Python makes
|
|
the number of *useful* entries depend on heartbeat volume. Filtering
|
|
in SQL keeps a busy fleet from starving the result.
|
|
include_prefix: Keep only entries whose message starts with this text.
|
|
|
|
Returns:
|
|
List of ServerLog instances, newest first
|
|
|
|
Note:
|
|
Prefixes are escaped before reaching SQL LIKE (via ``autoescape``), so a
|
|
literal '%' or '_' inside the prefix matches itself rather than acting
|
|
as a wildcard.
|
|
"""
|
|
query = ServerLog.query
|
|
|
|
if level:
|
|
query = query.filter_by(level=level)
|
|
|
|
if exclude_prefix:
|
|
query = query.filter(
|
|
~ServerLog.message.startswith(exclude_prefix, autoescape=True))
|
|
|
|
if include_prefix:
|
|
query = query.filter(
|
|
ServerLog.message.startswith(include_prefix, autoescape=True))
|
|
|
|
return query.order_by(ServerLog.timestamp.desc()).limit(limit).all()
|
|
|
|
|
|
def clear_old_logs(days: int = 30) -> int:
|
|
"""Delete logs older than specified days.
|
|
|
|
Args:
|
|
days: Number of days to keep
|
|
|
|
Returns:
|
|
Number of logs deleted
|
|
"""
|
|
try:
|
|
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
|
deleted = ServerLog.query.filter(ServerLog.timestamp < cutoff_date).delete()
|
|
db.session.commit()
|
|
log_action('info', f'Deleted {deleted} old log entries (older than {days} days)')
|
|
return deleted
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
print(f"Error clearing old logs: {e}")
|
|
return 0
|
|
|
|
|
|
# Convenience functions for specific log levels
|
|
def log_info(message: str) -> None:
|
|
"""Log an info level message."""
|
|
log_action('info', message)
|
|
|
|
|
|
def log_warning(message: str) -> None:
|
|
"""Log a warning level message."""
|
|
log_action('warning', message)
|
|
|
|
|
|
def log_error(message: str) -> None:
|
|
"""Log an error level message."""
|
|
log_action('error', message)
|