feat: added retention_days deletion

This commit is contained in:
2025-01-20 13:04:18 +01:00
parent 263d962912
commit 4e7f352a15
4 changed files with 114 additions and 45 deletions

View File

@@ -4,4 +4,4 @@ from sqlalchemy import Column, Integer, String
class Application(Base): class Application(Base):
name = Column(String(512), nullable=False, unique=True) name = Column(String(512), nullable=False, unique=True)
retention_days = Column(Integer, nullable=True, default=30) retention_days = Column(Integer, nullable=True)

View File

@@ -1,12 +1,32 @@
import os import os
from datetime import datetime, timedelta
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, name from apscheduler.schedulers.background import BackgroundScheduler
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, get_db, name
from sqlalchemy.orm import Session
from alembic import command from alembic import command
from alembic.config import Config from alembic.config import Config
from app.models.app import Application
from app.models.entry import LogEntry
from app.services.db.session import create_if_not_exists from app.services.db.session import create_if_not_exists
def delete_old_logs(sess: Session | None = None):
session = sess or next(get_db())
for app in session.query(Application).filter(Application.retention_days.isnot(None)):
cutoff = datetime.now() - timedelta(days=app.retention_days)
print(
f"Deleting logs older than {app.retention_days} days (cutoff: {cutoff}) for {app.name}",
)
session.query(LogEntry).filter(
LogEntry.application == app.id, LogEntry.created_at < cutoff
).delete()
session.commit()
def setup(db_name=name): def setup(db_name=name):
# Create Database # Create Database
create_if_not_exists(db_name) create_if_not_exists(db_name)
@@ -18,3 +38,17 @@ def setup(db_name=name):
"script_location", os.path.join(os.path.dirname(os.path.dirname(__file__)), "alembic") "script_location", os.path.join(os.path.dirname(os.path.dirname(__file__)), "alembic")
) )
command.upgrade(config, "head") command.upgrade(config, "head")
# Start retention deletion
scheduler = BackgroundScheduler()
scheduler.add_job(
delete_old_logs,
"interval",
id="deletor",
days=1,
max_instances=1,
replace_existing=True,
next_run_time=datetime.now(),
)
scheduler.start()
print("Deletion scheduler started")

View File

@@ -1,3 +1,6 @@
import contextlib
from datetime import datetime, timedelta
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, get_db from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, get_db
from creyPY.fastapi.models.base import Base from creyPY.fastapi.models.base import Base
from creyPY.fastapi.testing import GenericClient from creyPY.fastapi.testing import GenericClient
@@ -5,16 +8,55 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy_utils import create_database, database_exists, drop_database from sqlalchemy_utils import create_database, database_exists, drop_database
from app.models.entry import LogEntry
from app.services.auth import verify from app.services.auth import verify
import contextlib from app.setup import delete_old_logs
from .main import app from .main import app
CURRENT_USER = "api-key|testing" CURRENT_USER = "api-key|testing"
ENTRY_EXAMPLES = [
{
"l_type": "info",
"t_type": "create",
"message": "User Max Mustermann created",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "update",
"message": "User Max Mustermann updated",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "create",
"author": "auth|max_muster",
"message": "User Max Mustermann created a Unit",
"object_reference": "1",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "update",
"author": "auth|max_muster",
"message": "User Max Mustermann updated Unit 1",
"object_reference": "1",
"previous_object": {"name": "Unit 1"},
"environment": "prod",
},
{
"l_type": "warning",
"t_type": "delete",
"message": "User Max Mustermann deleted",
"environment": "prod",
},
]
@contextlib.contextmanager @contextlib.contextmanager
def app_context(self, name: str = "Testing"): def app_context(self, name: str = "Testing", retention_days: int | None = None):
app_id = self.create_app(name) app_id = self.create_app(name, retention_days)
try: try:
yield app_id yield app_id
finally: finally:
@@ -23,45 +65,8 @@ def app_context(self, name: str = "Testing"):
@contextlib.contextmanager @contextlib.contextmanager
def log_examples(self): def log_examples(self):
LOG_EXAMPLES = [
{
"l_type": "info",
"t_type": "create",
"message": "User Max Mustermann created",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "update",
"message": "User Max Mustermann updated",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "create",
"author": "auth|max_muster",
"message": "User Max Mustermann created a Unit",
"object_reference": "1",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "update",
"author": "auth|max_muster",
"message": "User Max Mustermann updated Unit 1",
"object_reference": "1",
"previous_object": {"name": "Unit 1"},
"environment": "prod",
},
{
"l_type": "warning",
"t_type": "delete",
"message": "User Max Mustermann deleted",
"environment": "prod",
},
]
with app_context(self) as app_id: with app_context(self) as app_id:
for entry in LOG_EXAMPLES: for entry in ENTRY_EXAMPLES:
self.log_message({"application": app_id, **entry}) self.log_message({"application": app_id, **entry})
yield app_id yield app_id
@@ -86,6 +91,7 @@ class TestAPI:
global CURRENT_USER global CURRENT_USER
return CURRENT_USER return CURRENT_USER
self.db_instance = get_db_test()
app.dependency_overrides[get_db] = get_db_test app.dependency_overrides[get_db] = get_db_test
app.dependency_overrides[verify] = get_test_sub app.dependency_overrides[verify] = get_test_sub
self.c = GenericClient(app) self.c = GenericClient(app)
@@ -94,8 +100,8 @@ class TestAPI:
drop_database(self.engine.url) drop_database(self.engine.url)
# HELPERS # HELPERS
def create_app(self, name: str = "Testing"): def create_app(self, name: str = "Testing", retention_days: int | None = None):
re = self.c.post("/app/", {"name": name}) re = self.c.post("/app/", {"name": name, "retention_days": retention_days})
return re["id"] return re["id"]
def destroy_app(self, app_id): def destroy_app(self, app_id):
@@ -260,3 +266,29 @@ class TestAPI:
re = self.c.get("/log/?application=" + str(app_id)) re = self.c.get("/log/?application=" + str(app_id))
assert re["total"] == 0 assert re["total"] == 0
def test_retention_delete(self):
sess = next(self.db_instance)
with app_context(self, retention_days=2) as app_id:
for i in range(5):
sess.add(
LogEntry(
application=app_id,
created_at=datetime.now() - timedelta(days=i),
created_by_id=CURRENT_USER,
)
)
sess.commit()
assert sess.query(LogEntry).count() == 5
re = self.c.get("/log/?application=" + str(app_id))
assert re["total"] == 5
delete_old_logs(sess)
assert sess.query(LogEntry).count() == 2
# delete all logs
re = self.c.delete("/log/?application=" + str(app_id), r_code=200)

View File

@@ -34,3 +34,6 @@ pluggy==1.5.0 # pytest
pytest==8.3.4 # pytest pytest==8.3.4 # pytest
fastapi-filters==0.2.9 # Filters fastapi-filters==0.2.9 # Filters
APScheduler==3.11.0 # Scheduler for deletion
tzlocal==5.2 # Scheduler for deletion