added logging
This commit is contained in:
@@ -10,11 +10,11 @@ Amounts and worked/vacation/sick statuses selected for dates are stored persiste
|
|||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
pip install -r requirements.txt
|
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,
|
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.
|
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.
|
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
|
```yaml
|
||||||
flask:
|
flask:
|
||||||
url_prefix: "/fatima"
|
url_prefix: "/fatima"
|
||||||
|
log_file: "/tmp/fatima.log"
|
||||||
```
|
```
|
||||||
|
|
||||||
Set `URL_PREFIX` in the environment to override this value at deployment time.
|
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
|
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.
|
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`.
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ flask:
|
|||||||
secret_key: "replace-with-a-long-random-value"
|
secret_key: "replace-with-a-long-random-value"
|
||||||
# External path used by the reverse proxy. Use "" to serve from the domain root.
|
# External path used by the reverse proxy. Use "" to serve from the domain root.
|
||||||
url_prefix: "/fatima"
|
url_prefix: "/fatima"
|
||||||
|
# Startup database summary log.
|
||||||
|
log_file: "/tmp/fatima.log"
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
username: "your-username"
|
username: "your-username"
|
||||||
|
|||||||
+42
-2
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -57,6 +57,7 @@ def load_yaml_config(path):
|
|||||||
"APP_USERNAME": auth.get("username"),
|
"APP_USERNAME": auth.get("username"),
|
||||||
"APP_PASSWORD": auth.get("password"),
|
"APP_PASSWORD": auth.get("password"),
|
||||||
"URL_PREFIX": normalize_url_prefix(flask_config.get("url_prefix", "/fatima")),
|
"URL_PREFIX": normalize_url_prefix(flask_config.get("url_prefix", "/fatima")),
|
||||||
|
"LOG_FILE": flask_config.get("log_file", "/tmp/fatima.log"),
|
||||||
}
|
}
|
||||||
missing = [
|
missing = [
|
||||||
name for name, value in required.items()
|
name for name, value in required.items()
|
||||||
@@ -80,6 +81,40 @@ def load_yaml_config(path):
|
|||||||
return required
|
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):
|
def create_app(test_config=None):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
config_path = os.environ.get("APP_CONFIG", Path(app.root_path) / "config.yaml")
|
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"))
|
app.config.setdefault("DATABASE", str(Path(app.instance_path) / "calendar.sqlite"))
|
||||||
|
|
||||||
# Environment variables remain useful when deploying without a local file.
|
# 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):
|
if os.environ.get(name):
|
||||||
app.config[name] = os.environ[name]
|
app.config[name] = os.environ[name]
|
||||||
if "URL_PREFIX" in os.environ:
|
if "URL_PREFIX" in os.environ:
|
||||||
@@ -95,6 +130,8 @@ def create_app(test_config=None):
|
|||||||
if test_config:
|
if test_config:
|
||||||
app.config.update(test_config)
|
app.config.update(test_config)
|
||||||
app.config["URL_PREFIX"] = normalize_url_prefix(app.config["URL_PREFIX"])
|
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"])
|
app.wsgi_app = UrlPrefixMiddleware(app.wsgi_app, app.config["URL_PREFIX"])
|
||||||
|
|
||||||
Path(app.config["DATABASE"]).parent.mkdir(parents=True, exist_ok=True)
|
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():
|
def get_database():
|
||||||
connection = sqlite3.connect(app.config["DATABASE"])
|
connection = sqlite3.connect(app.config["DATABASE"])
|
||||||
connection.row_factory = sqlite3.Row
|
connection.row_factory = sqlite3.Row
|
||||||
|
|||||||
+15
-1
@@ -1,6 +1,7 @@
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from flask_fatima import create_app
|
from flask_fatima import create_app, log_database_summary
|
||||||
|
|
||||||
URL_PREFIX = "/fatima"
|
URL_PREFIX = "/fatima"
|
||||||
|
|
||||||
@@ -30,6 +31,19 @@ def test_calendar_requires_login():
|
|||||||
assert response.headers["Location"].endswith(url("/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():
|
def test_prefix_applies_to_generated_links_and_unprefixed_routes_are_rejected():
|
||||||
test_client = client()
|
test_client = client()
|
||||||
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
test_client.post(url("/login"), data={"username": "user", "password": "pass"})
|
||||||
|
|||||||
Reference in New Issue
Block a user