06/01 Add log viewer

This commit is contained in:
2026-06-01 11:09:06 -04:00
parent 82ea589b41
commit acf4d9bb43
5 changed files with 454 additions and 24 deletions
+42 -24
View File
@@ -8,38 +8,54 @@ from app.utils.formatters import format_currency, format_percent, format_large_n
def _setup_logging(app):
"""
Ensure app.* module loggers emit at INFO level.
Configure the 'app' namespace logger to write to a rotating file AND stderr.
Under Gunicorn the root logger already has handlers (pointing to Gunicorn's
error log / stderr) but its level is WARNING, so INFO records are dropped
before they reach any handler. We fix that by:
1. Reusing Gunicorn's handlers on the 'app' namespace logger so records
go to the same destination as Gunicorn's own logs.
2. Falling back to a plain stderr StreamHandler in dev / direct-run mode.
Uses a pipe-delimited format so the log viewer can parse each field easily:
2026-06-01 12:00:00|INFO|app.services.teller_service|message text
The file path comes from LOG_FILE_PATH config (defaults to logs/app.log
next to the project root). The directory is created automatically.
"""
import sys
from logging.handlers import RotatingFileHandler
app_log = logging.getLogger('app')
app_log.setLevel(logging.INFO)
if app_log.handlers:
return # already configured (e.g. running tests)
return # already configured (avoids duplicate handlers on reload)
gunicorn_handlers = logging.getLogger('gunicorn.error').handlers
if gunicorn_handlers:
# Running under Gunicorn — attach its handlers so our logs land in the
# same error log file that Gunicorn writes to.
for h in gunicorn_handlers:
app_log.addHandler(h)
else:
# Dev / direct python run — stderr is fine.
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s %(name)s: %(message)s'
))
app_log.addHandler(handler)
app_log.setLevel(logging.INFO)
app_log.propagate = False # don't double-emit through the root logger
app_log.propagate = False # avoid double-printing via root
fmt = logging.Formatter(
'%(asctime)s|%(levelname)s|%(name)s|%(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
# --- rotating file handler (primary — always on) ---
log_file = app.config.get('LOG_FILE_PATH', '')
if log_file:
try:
os.makedirs(os.path.dirname(log_file), exist_ok=True)
fh = RotatingFileHandler(
log_file,
maxBytes=app.config.get('LOG_MAX_BYTES', 10 * 1024 * 1024),
backupCount=app.config.get('LOG_BACKUP_COUNT', 5),
encoding='utf-8',
)
fh.setLevel(logging.INFO)
fh.setFormatter(fmt)
app_log.addHandler(fh)
except Exception as exc:
# Can't open file (permissions, bad path) — fall through to stderr only
print(f'[pfm] WARNING: could not open log file {log_file!r}: {exc}', file=sys.stderr)
# --- stderr handler (secondary — also always on so Gunicorn captures it) ---
sh = logging.StreamHandler(sys.stderr)
sh.setLevel(logging.INFO)
sh.setFormatter(fmt)
app_log.addHandler(sh)
app_log.info('Logging initialised — file=%s', log_file or '(none)')
def create_app(config_name=None):
@@ -73,6 +89,7 @@ def create_app(config_name=None):
from app.routes.reports import reports_bp
from app.routes.settings import settings_bp
from app.routes.teller import teller_bp
from app.routes.logs import logs_bp
app.register_blueprint(auth_bp)
app.register_blueprint(dashboard_bp)
@@ -86,6 +103,7 @@ def create_app(config_name=None):
app.register_blueprint(reports_bp)
app.register_blueprint(settings_bp)
app.register_blueprint(teller_bp)
app.register_blueprint(logs_bp)
with app.app_context():
from app.models import (