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
@@ -21,6 +21,7 @@ Created by: Jess Rogerson (yelling commands at Claude.AI)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
@@ -30,6 +31,8 @@ from datetime import datetime, timedelta
|
||||
import requests
|
||||
from flask import current_app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from app import db
|
||||
from app.models.instruction import Instruction
|
||||
from app.models.instruction_fetch import InstructionFetchJob
|
||||
@@ -375,8 +378,11 @@ def fetch_for_set(lego_set, delay_between_files=None):
|
||||
pause = (file_delay_seconds() if delay_between_files is None
|
||||
else delay_between_files)
|
||||
|
||||
api = BricksetAPI()
|
||||
api = BricksetAPI(context='fetcher')
|
||||
entries, source, note = _instruction_urls(api, lego_set)
|
||||
logger.info('Set %s: Brickset lists %d instruction file(s)%s',
|
||||
lego_set.set_number, len(entries),
|
||||
f' ({note})' if note else '')
|
||||
|
||||
if not entries:
|
||||
return 0, 0, source, note or 'no instructions listed on Brickset'
|
||||
@@ -395,6 +401,8 @@ def fetch_for_set(lego_set, delay_between_files=None):
|
||||
temp_path, size, sha = _download_pdf(entry['url'], max_bytes,
|
||||
timeout)
|
||||
except Exception as exc:
|
||||
logger.warning('Set %s: download failed for %s - %s',
|
||||
lego_set.set_number, entry['url'], exc)
|
||||
problems.append(f'{entry["description"] or entry["url"]}: {exc}')
|
||||
continue
|
||||
|
||||
@@ -427,6 +435,8 @@ def fetch_for_set(lego_set, delay_between_files=None):
|
||||
known.add(sha)
|
||||
page += 1
|
||||
added += 1
|
||||
logger.info('Set %s: saved %s (%d bytes)',
|
||||
lego_set.set_number, filename, file_size or size)
|
||||
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
@@ -519,8 +529,8 @@ def run_one(force=False):
|
||||
job.status = 'empty'
|
||||
|
||||
except Exception as exc:
|
||||
current_app.logger.exception('Instruction fetch failed for %s',
|
||||
lego_set.set_number)
|
||||
logger.exception('Instruction fetch failed for %s',
|
||||
lego_set.set_number)
|
||||
job.message = str(exc)[:500]
|
||||
if job.attempts >= max_attempts():
|
||||
job.status = 'failed'
|
||||
@@ -533,8 +543,8 @@ def run_one(force=False):
|
||||
job.finished_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info('Instruction fetch %s for %s: %s', job.status,
|
||||
lego_set.set_number, job.message)
|
||||
logger.info('Job #%s %s for %s: %s', job.id, job.status,
|
||||
lego_set.set_number, job.message)
|
||||
return job.to_dict()
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user