Add owner-only logging and Brickset API call tracking
LIM configured no logging at all, so the instruction fetcher had no trail to follow. Adds a rotating file handler on the root logger plus two owner-only admin pages. - Admin > Logs: filter by level, source and text; tracebacks kept with their error; auto-refresh, raw download, rotated-file browsing and rotate-now - Admin > Logs > Brickset API calls: every outbound call recorded with method, params, caller, duration, result count and error, plus daily quota accounting (only getSets counts) and a comparison against getKeyUsageStats - New owner concept (services/ownership.py): a single account, distinct from is_admin, since these pages expose other admins activity. Defaults to the first admin; flask owner show/set to change - Services now log through module loggers; retention setting and pruning for call records - New CLI: flask owner show/set, flask logs path/prune - New table brickset_api_calls, created automatically at boot; LOG_* env vars, /data/logs in Docker
This commit is contained in:
1 parent
89f65e2d74
commit
f7c20de5ac
27 files changed
+1914
-35
No files matched your search
+87
@@ -12,6 +12,11 @@ Flask CLI commands for LIM backup and restore.
|
||||
flask instructions run [--count N] [--force]
|
||||
flask instructions status
|
||||
|
||||
flask owner show
|
||||
flask owner set USERNAME
|
||||
flask logs path [--tail N]
|
||||
flask logs prune [--days N]
|
||||
|
||||
`restore` is the path to use on a fresh Docker instance: no login required,
|
||||
and it works before any admin user exists.
|
||||
|
||||
@@ -267,6 +272,88 @@ def instructions_status():
|
||||
f"{summary['sets_missing_pdfs']} without a PDF")
|
||||
|
||||
|
||||
owner_cli = AppGroup('owner', help='Who owns this instance.')
|
||||
|
||||
|
||||
@owner_cli.command('show')
|
||||
def owner_show():
|
||||
"""Show the owner account (the only one who can read the logs)."""
|
||||
from app.services.ownership import get_owner
|
||||
|
||||
owner = get_owner()
|
||||
if owner is None:
|
||||
click.secho('No users exist yet.', fg='yellow')
|
||||
return
|
||||
click.secho(f'Owner: {owner.username} <{owner.email}> (id {owner.id})',
|
||||
fg='green')
|
||||
|
||||
|
||||
@owner_cli.command('set')
|
||||
@click.argument('username')
|
||||
def owner_set(username):
|
||||
"""Hand ownership to USERNAME (promotes them to admin if needed)."""
|
||||
from app.models.user import User
|
||||
from app.services.ownership import set_owner
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
click.secho(f'No user called {username!r}.', fg='red')
|
||||
raise SystemExit(1)
|
||||
|
||||
set_owner(user)
|
||||
click.secho(f'{user.username} is now the owner.', fg='green')
|
||||
|
||||
|
||||
logs_cli = AppGroup('logs', help='Application log file.')
|
||||
|
||||
|
||||
@logs_cli.command('path')
|
||||
@click.option('--tail', default=0, type=int,
|
||||
help='Also print the last N lines.')
|
||||
def logs_path(tail):
|
||||
"""Where the log lives, and optionally its tail."""
|
||||
import os
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from app.services import logging_setup
|
||||
|
||||
path = logging_setup.log_path(current_app)
|
||||
exists = os.path.isfile(path)
|
||||
click.echo(f' {path}')
|
||||
click.echo(f' exists: {exists}'
|
||||
+ (f' size: {os.path.getsize(path)} bytes' if exists else ''))
|
||||
click.echo(f' handler active: {logging_setup.get_handler() is not None}')
|
||||
|
||||
if tail and exists:
|
||||
entries, total = logging_setup.read_entries(path, max_entries=tail)
|
||||
click.echo(f'\n last {len(entries)} of {total} entries:\n')
|
||||
for entry in entries:
|
||||
click.echo(f" {entry['time']} {entry['level']:<8} "
|
||||
f"{entry['logger']} | {entry['message']}")
|
||||
|
||||
|
||||
@logs_cli.command('prune')
|
||||
@click.option('--days', default=None, type=int,
|
||||
help='Override the configured retention.')
|
||||
def logs_prune(days):
|
||||
"""Delete old Brickset API call records."""
|
||||
from app.models.api_call import BricksetApiCall
|
||||
from app.models.site_setting import SiteSetting
|
||||
|
||||
if days is None:
|
||||
try:
|
||||
days = int(SiteSetting.get('api_log_retention_days', '90'))
|
||||
except (TypeError, ValueError):
|
||||
days = 90
|
||||
|
||||
removed = BricksetApiCall.prune(days)
|
||||
click.secho(f'Removed {removed} record(s) older than {days} days.',
|
||||
fg='green')
|
||||
|
||||
|
||||
def register_cli(app):
|
||||
app.cli.add_command(backup_cli)
|
||||
app.cli.add_command(instructions_cli)
|
||||
app.cli.add_command(owner_cli)
|
||||
app.cli.add_command(logs_cli)
|
||||
Reference in new issue
Block a user