179 lines
6.1 KiB
Python
179 lines
6.1 KiB
Python
import os
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
|
|
from flask import (Blueprint, render_template, request, jsonify,
|
|
current_app, send_file, abort)
|
|
from flask_login import login_required
|
|
|
|
logs_bp = Blueprint('logs', __name__, url_prefix='/logs')
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
# ── Index ─────────────────────────────────────────────────────────────────────
|
|
|
|
@logs_bp.route('/')
|
|
@login_required
|
|
def index():
|
|
log_file = current_app.config.get('LOG_FILE_PATH', '')
|
|
file_exists = bool(log_file) and os.path.isfile(log_file)
|
|
file_size = os.path.getsize(log_file) if file_exists else 0
|
|
file_mtime = None
|
|
if file_exists:
|
|
ts = os.path.getmtime(log_file)
|
|
file_mtime = datetime.fromtimestamp(ts).strftime('%b %d, %H:%M')
|
|
|
|
def fmt_size(b):
|
|
if b < 1024: return f'{b} B'
|
|
if b < 1024**2: return f'{b/1024:.1f} KB'
|
|
return f'{b/1024**2:.1f} MB'
|
|
|
|
# DB row count
|
|
db_count = 0
|
|
try:
|
|
from app.models.app_log import AppLog
|
|
db_count = AppLog.query.count()
|
|
except Exception:
|
|
pass
|
|
|
|
return render_template('logs/index.html',
|
|
log_file=log_file,
|
|
file_exists=file_exists,
|
|
file_size=file_size,
|
|
file_size_fmt=fmt_size(file_size),
|
|
file_mtime=file_mtime,
|
|
db_count=db_count)
|
|
|
|
|
|
# ── API (reads from DB) ───────────────────────────────────────────────────────
|
|
|
|
@logs_bp.route('/api')
|
|
@login_required
|
|
def api():
|
|
try:
|
|
from app.models.app_log import AppLog
|
|
from app.extensions import db
|
|
except Exception as e:
|
|
return jsonify({'entries': [], 'error': str(e)})
|
|
|
|
level_filter = request.args.get('level', 'ALL').upper()
|
|
search = request.args.get('search', '').lower()
|
|
limit = min(int(request.args.get('limit', 200)), 2000)
|
|
module = request.args.get('module', '').lower()
|
|
|
|
try:
|
|
query = AppLog.query
|
|
|
|
if level_filter not in ('ALL', ''):
|
|
query = query.filter(AppLog.level == level_filter)
|
|
|
|
if module:
|
|
query = query.filter(AppLog.module.ilike(f'%{module}%'))
|
|
|
|
if search:
|
|
query = query.filter(
|
|
db.or_(
|
|
AppLog.message.ilike(f'%{search}%'),
|
|
AppLog.module.ilike(f'%{search}%'),
|
|
)
|
|
)
|
|
|
|
total = query.count()
|
|
rows = query.order_by(AppLog.timestamp.desc()).limit(limit).all()
|
|
|
|
entries = [
|
|
{
|
|
'ts': r.timestamp.strftime('%Y-%m-%d %H:%M:%S') if r.timestamp else '',
|
|
'level': r.level or 'INFO',
|
|
'name': r.module or '',
|
|
'message': r.message or '',
|
|
}
|
|
for r in rows
|
|
]
|
|
|
|
# Counts and modules across ALL unfiltered rows (capped for performance)
|
|
all_rows = AppLog.query.with_entities(AppLog.level, AppLog.module).all()
|
|
counts = {}
|
|
modules_seen = set()
|
|
for lvl, mod in all_rows:
|
|
counts[lvl] = counts.get(lvl, 0) + 1
|
|
if mod:
|
|
modules_seen.add(mod)
|
|
|
|
return jsonify({
|
|
'entries': entries,
|
|
'counts': counts,
|
|
'total_raw': total,
|
|
'modules': sorted(modules_seen),
|
|
})
|
|
except Exception as e:
|
|
log.error('logs api error: %s', e)
|
|
return jsonify({'entries': [], 'error': str(e), 'counts': {}, 'total_raw': 0, 'modules': []})
|
|
|
|
|
|
# ── Download (still from file) ────────────────────────────────────────────────
|
|
|
|
@logs_bp.route('/download')
|
|
@login_required
|
|
def download():
|
|
log_file = current_app.config.get('LOG_FILE_PATH', '')
|
|
if not log_file or not os.path.isfile(log_file):
|
|
abort(404)
|
|
return send_file(log_file, as_attachment=True, download_name='pfm-app.log')
|
|
|
|
|
|
# ── Clear (truncate DB table + file) ─────────────────────────────────────────
|
|
|
|
@logs_bp.route('/clear', methods=['POST'])
|
|
@login_required
|
|
def clear():
|
|
errors = []
|
|
|
|
# Clear DB table
|
|
try:
|
|
from app.models.app_log import AppLog
|
|
from app.extensions import db
|
|
AppLog.query.delete()
|
|
db.session.commit()
|
|
except Exception as exc:
|
|
errors.append(f'DB: {exc}')
|
|
|
|
# Clear log file
|
|
log_file = current_app.config.get('LOG_FILE_PATH', '')
|
|
if log_file:
|
|
try:
|
|
with open(log_file, 'w', encoding='utf-8'):
|
|
pass
|
|
except Exception as exc:
|
|
errors.append(f'file: {exc}')
|
|
|
|
if errors:
|
|
return jsonify({'error': '; '.join(errors)}), 500
|
|
log.info('App logs cleared by user')
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
# ── Purge (delete entries older than N days) ──────────────────────────────────
|
|
|
|
@logs_bp.route('/purge', methods=['POST'])
|
|
@login_required
|
|
def purge():
|
|
try:
|
|
days = int(request.form.get('days', 30))
|
|
if days not in (7, 30, 90):
|
|
return jsonify({'error': 'days must be 7, 30, or 90'}), 400
|
|
except (ValueError, TypeError):
|
|
return jsonify({'error': 'Invalid days parameter'}), 400
|
|
|
|
cutoff = datetime.utcnow() - timedelta(days=days)
|
|
try:
|
|
from app.models.app_log import AppLog
|
|
from app.extensions import db
|
|
deleted = AppLog.query.filter(AppLog.timestamp < cutoff).delete()
|
|
db.session.commit()
|
|
log.info('App logs purged: %d entries older than %d days deleted', deleted, days)
|
|
return jsonify({'status': 'ok', 'deleted': deleted})
|
|
except Exception as exc:
|
|
log.error('App log purge failed: %s', exc)
|
|
return jsonify({'error': str(exc)}), 500
|