added logging

This commit is contained in:
2026-07-17 19:57:08 +02:00
parent 64f1993a84
commit d9325fce98
4 changed files with 73 additions and 6 deletions
+14 -3
View File
@@ -10,11 +10,11 @@ Amounts and worked/vacation/sick statuses selected for dates are stored persiste
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
waitress-serve --host=0.0.0.0 --port=9009 fatima:app
waitress-serve --host=0.0.0.0 --port=9009 flask_fatima:app
```
To use a different port, substitute it in the command (for example,
`--port=8080`). The application exposes its WSGI callable as `fatima:app`, so it
`--port=8080`). The application exposes its WSGI callable as `flask_fatima:app`, so it
can also be used by platforms that ask for an application entry point.
Set the username, password, and Flask signing key in `config.yaml` before starting. This file is excluded from Git. Use `config.example.yaml` as the documented template when setting up another machine.
@@ -38,6 +38,7 @@ The app is served below `/fatima` by default. Configure the external path in
```yaml
flask:
url_prefix: "/fatima"
log_file: "/tmp/fatima.log"
```
Set `URL_PREFIX` in the environment to override this value at deployment time.
@@ -77,4 +78,14 @@ On the iPhone, visit `http://<your-computer-on-the-local-network>:9009/fatima/`.
Waitress listens on all network interfaces with the command above. For anything
exposed beyond a trusted home network, put it behind an HTTPS reverse proxy.
For deployments, `APP_CONFIG` can point to a YAML file elsewhere. `SECRET_KEY`, `APP_USERNAME`, and `APP_PASSWORD` environment variables optionally override the corresponding YAML values.
For deployments, `APP_CONFIG` can point to a YAML file elsewhere. `SECRET_KEY`,
`APP_USERNAME`, `APP_PASSWORD`, `DATABASE`, and `LOG_FILE` environment variables optionally
override the corresponding values. `DATABASE` should be an absolute path to the
SQLite file used in production.
At startup, the app prints a database summary to the Waitress log containing the
resolved path, amount and status totals, distinct recorded-date total, and date
range. Compare that path and those totals with the importer's output to confirm
that both processes use the same database. The timestamped summary is also
appended to the configured `flask.log_file` each time the app starts. It defaults
to `/tmp/fatima.log` and can be overridden at deployment time with `LOG_FILE`.
+2
View File
@@ -3,6 +3,8 @@ flask:
secret_key: "replace-with-a-long-random-value"
# External path used by the reverse proxy. Use "" to serve from the domain root.
url_prefix: "/fatima"
# Startup database summary log.
log_file: "/tmp/fatima.log"
auth:
username: "your-username"
+42 -2
View File
@@ -1,7 +1,7 @@
import os
import secrets
import sqlite3
from datetime import date
from datetime import date, datetime
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from functools import wraps
from pathlib import Path
@@ -57,6 +57,7 @@ def load_yaml_config(path):
"APP_USERNAME": auth.get("username"),
"APP_PASSWORD": auth.get("password"),
"URL_PREFIX": normalize_url_prefix(flask_config.get("url_prefix", "/fatima")),
"LOG_FILE": flask_config.get("log_file", "/tmp/fatima.log"),
}
missing = [
name for name, value in required.items()
@@ -80,6 +81,40 @@ def load_yaml_config(path):
return required
def database_summary(database_path):
with sqlite3.connect(database_path) as database:
amount_count = database.execute("SELECT COUNT(*) FROM date_choices").fetchone()[0]
status_count = database.execute("SELECT COUNT(*) FROM date_statuses").fetchone()[0]
recorded_count, first_date, last_date = database.execute(
"""
SELECT COUNT(*), MIN(recorded_date), MAX(recorded_date) FROM (
SELECT choice_date AS recorded_date FROM date_choices
UNION
SELECT status_date AS recorded_date FROM date_statuses
)
"""
).fetchone()
return amount_count, status_count, recorded_count, first_date, last_date
def log_database_summary(database_path, log_path):
amounts, statuses, recorded_dates, first_date, last_date = database_summary(database_path)
date_range = f"{first_date} through {last_date}" if first_date else "no recorded dates"
message = (
"Fatima database: "
f"path={database_path} | "
f"amounts={amounts} | statuses={statuses} | "
f"recorded_dates={recorded_dates} | range={date_range}"
)
print(message, flush=True)
try:
with log_path.open("a", encoding="utf-8") as log_file:
timestamp = datetime.now().astimezone().isoformat(timespec="seconds")
log_file.write(f"{timestamp} | {message}\n")
except OSError as error:
print(f"Could not write database summary to {log_path}: {error}", flush=True)
def create_app(test_config=None):
app = Flask(__name__)
config_path = os.environ.get("APP_CONFIG", Path(app.root_path) / "config.yaml")
@@ -87,7 +122,7 @@ def create_app(test_config=None):
app.config.setdefault("DATABASE", str(Path(app.instance_path) / "calendar.sqlite"))
# Environment variables remain useful when deploying without a local file.
for name in ("SECRET_KEY", "APP_USERNAME", "APP_PASSWORD"):
for name in ("SECRET_KEY", "APP_USERNAME", "APP_PASSWORD", "DATABASE", "LOG_FILE"):
if os.environ.get(name):
app.config[name] = os.environ[name]
if "URL_PREFIX" in os.environ:
@@ -95,6 +130,8 @@ def create_app(test_config=None):
if test_config:
app.config.update(test_config)
app.config["URL_PREFIX"] = normalize_url_prefix(app.config["URL_PREFIX"])
app.config["DATABASE"] = str(Path(app.config["DATABASE"]).expanduser().resolve())
app.config["LOG_FILE"] = str(Path(app.config["LOG_FILE"]).expanduser().resolve())
app.wsgi_app = UrlPrefixMiddleware(app.wsgi_app, app.config["URL_PREFIX"])
Path(app.config["DATABASE"]).parent.mkdir(parents=True, exist_ok=True)
@@ -119,6 +156,9 @@ def create_app(test_config=None):
"""
)
if not app.config.get("TESTING"):
log_database_summary(app.config["DATABASE"], Path(app.config["LOG_FILE"]))
def get_database():
connection = sqlite3.connect(app.config["DATABASE"])
connection.row_factory = sqlite3.Row
+15 -1
View File
@@ -1,6 +1,7 @@
import tempfile
from pathlib import Path
from flask_fatima import create_app
from flask_fatima import create_app, log_database_summary
URL_PREFIX = "/fatima"
@@ -30,6 +31,19 @@ def test_calendar_requires_login():
assert response.headers["Location"].endswith(url("/login"))
def test_database_summary_is_appended_to_log(capsys):
test_client = client()
with tempfile.TemporaryDirectory() as directory:
log_path = Path(directory) / "fatima.log"
log_database_summary(test_client.application.config["DATABASE"], log_path)
logged = log_path.read_text(encoding="utf-8")
assert "Fatima database:" in logged
assert f"path={test_client.application.config['DATABASE']}" in logged
assert "amounts=0 | statuses=0 | recorded_dates=0" in logged
assert "Fatima database:" in capsys.readouterr().out
def test_prefix_applies_to_generated_links_and_unprefixed_routes_are_rejected():
test_client = client()
test_client.post(url("/login"), data={"username": "user", "password": "pass"})