mirror of
https://github.com/creyD/apilog.git
synced 2026-04-12 19:30:29 +02:00
Compare commits
92 Commits
1.3.3-reno
...
1.4.79
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f64d5e96 | ||
|
|
b6eaa7c1e7 | ||
|
|
38f7c9234d | ||
|
|
ca6a2a3eaa | ||
|
|
055b8af28a | ||
|
|
abe103dc0f | ||
|
|
8da0f31b16 | ||
| d5907074c9 | |||
|
|
06e5a3af30 | ||
|
|
f42ce7abe1 | ||
|
|
a743969689 | ||
|
|
6fc6117400 | ||
|
|
d8057db909 | ||
|
|
c8872ca437 | ||
|
|
e4a903cd61 | ||
|
|
de7ad24b49 | ||
|
|
da4ebd0a8b | ||
|
|
9f3a7f55db | ||
|
|
f315369ff8 | ||
|
|
86c8e4fc4e | ||
|
|
1c3c2347ee | ||
|
|
d28507ca15 | ||
|
|
191268af6a | ||
|
|
96afee2cf4 | ||
|
|
995bdc0600 | ||
|
|
f2f572ec9a | ||
|
|
61bc957a2c | ||
|
|
235c07466e | ||
|
|
2f70ab2dcf | ||
|
|
076267ec3a | ||
|
|
0af16a831f | ||
|
|
ecf784054f | ||
|
|
50ad31f331 | ||
|
|
493492cd4f | ||
|
|
2df1a18f70 | ||
|
|
9ab291afb5 | ||
|
|
797f869e87 | ||
|
|
3d1597a4d4 | ||
|
|
4c737620bb | ||
|
|
e1f0d012dd | ||
|
|
b6a23f66b4 | ||
|
|
3e4e5d9cf1 | ||
|
|
e3ae8ae526 | ||
|
|
a69b2341d5 | ||
|
|
9a2655d225 | ||
|
|
f6d27c543f | ||
|
|
c53c2db27a | ||
|
|
9e888b9e96 | ||
|
|
f4973dd612 | ||
|
|
b590b4de6a | ||
|
|
7c38e09ac3 | ||
|
|
36d59101fd | ||
|
|
fee836f20e | ||
|
|
85649f94f2 | ||
|
|
a025b3994c | ||
|
|
20dc2c6fa0 | ||
|
|
2c6b35e3dd | ||
|
|
fadc83e19b | ||
|
|
cdc0ac249a | ||
|
|
015d5eb13d | ||
|
|
608ec8de6f | ||
|
|
a21947fe02 | ||
|
|
11e5ff9646 | ||
|
|
971d7743f1 | ||
|
|
1d34b32179 | ||
|
|
59abb07721 | ||
|
|
7fc2a7a1a9 | ||
|
|
ae570acc6a | ||
|
|
24aa470d6d | ||
|
|
112b2def80 | ||
|
|
cc7be6e4ac | ||
|
|
8410fc886e | ||
|
|
13496b0cbd | ||
|
|
e88a617e30 | ||
|
|
76d0866595 | ||
|
|
b194e583fb | ||
|
|
5f45d3aeb8 | ||
|
|
20a107a49c | ||
|
|
087ea50c4e | ||
|
|
f47b44ba95 | ||
| 9b1343b90d | |||
|
|
15d754f68d | ||
|
|
9e0c8f0173 | ||
|
|
0c769ba843 | ||
| 18cce99967 | |||
| 4e7f352a15 | |||
| 263d962912 | |||
|
|
3d4e5e3f4b | ||
|
|
6cdae87f42 | ||
|
|
de36e60710 | ||
|
|
bcec3079d3 | ||
|
|
cf033298ce |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -91,7 +91,7 @@ jobs:
|
||||
git config --local user.name "creyD"
|
||||
|
||||
- name: Git Version
|
||||
uses: codacy/git-version@2.8.0
|
||||
uses: codacy/git-version@2.8.3
|
||||
id: git_version
|
||||
with:
|
||||
minor-identifier: "feat:"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.13-slim
|
||||
ARG VERSION=unknown
|
||||
|
||||
# Create a non-root user and group
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# apilog
|
||||
|
||||
Tiny logging API server, for taking logs via HTTP POST requests.
|
||||
|
||||
## TODO
|
||||
|
||||
[ ] Application Patch
|
||||
[ ] Team CRUD
|
||||
|
||||
29
alembic/versions/1e695b024786_.py
Normal file
29
alembic/versions/1e695b024786_.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 1e695b024786
|
||||
Revises: 21dc1dc045b8
|
||||
Create Date: 2025-01-20 11:36:14.692849
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "1e695b024786"
|
||||
down_revision: Union[str, None] = "21dc1dc045b8"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
with op.batch_alter_table("application", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("retention_days", sa.Integer(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("application", schema=None) as batch_op:
|
||||
batch_op.drop_column("retention_days")
|
||||
@@ -1,6 +1,7 @@
|
||||
from creyPY.fastapi.models.base import Base
|
||||
from sqlalchemy import Column, String
|
||||
from sqlalchemy import Column, Integer, String
|
||||
|
||||
|
||||
class Application(Base):
|
||||
name = Column(String(512), nullable=False, unique=True)
|
||||
retention_days = Column(Integer, nullable=True)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from creyPY.fastapi.models.base import Base
|
||||
from sqlalchemy import Column, String, ForeignKey, Enum, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from enum import Enum as pyenum
|
||||
|
||||
from creyPY.fastapi.models.base import Base
|
||||
from sqlalchemy import JSON, Column, Enum, ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
|
||||
class TransactionType(pyenum):
|
||||
CREATE = "create"
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
from creyPY.fastapi.crud import (
|
||||
create_obj_from_data,
|
||||
)
|
||||
from creyPY.fastapi.db.session import get_db
|
||||
from fastapi import APIRouter, Depends, Security, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
from app.services.auth import verify
|
||||
from app.schema.app import AppIN, AppOUT
|
||||
from app.models.app import Application
|
||||
from creyPY.fastapi.pagination import Page
|
||||
from uuid import UUID
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
|
||||
from creyPY.fastapi.crud import create_obj_from_data
|
||||
from creyPY.fastapi.db.session import get_db
|
||||
from creyPY.fastapi.pagination import Page, paginate
|
||||
from fastapi import APIRouter, Depends, HTTPException, Security
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.app import Application
|
||||
from app.schema.app import AppIN, AppOUT
|
||||
from app.services.auth import verify
|
||||
|
||||
router = APIRouter(prefix="/app", tags=["apps"])
|
||||
|
||||
@@ -26,7 +25,7 @@ async def create_app(
|
||||
data,
|
||||
Application,
|
||||
db,
|
||||
additonal_data={"created_by_id": sub},
|
||||
additional_data={"created_by_id": sub},
|
||||
)
|
||||
return AppOUT.model_validate(obj)
|
||||
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
from creyPY.fastapi.crud import (
|
||||
create_obj_from_data,
|
||||
)
|
||||
from creyPY.fastapi.order_by import order_by
|
||||
from datetime import datetime
|
||||
from typing import Callable
|
||||
from sqlalchemy.sql.selectable import Select
|
||||
from creyPY.fastapi.db.session import get_db
|
||||
from fastapi import APIRouter, Depends, Security, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from app.services.auth import verify
|
||||
from app.schema.entry import LogIN, LogOUT
|
||||
from app.models.entry import LogEntry
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
from creyPY.fastapi.pagination import Page
|
||||
from uuid import UUID
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from creyPY.fastapi.crud import create_obj_from_data
|
||||
from creyPY.fastapi.db.session import get_db
|
||||
from creyPY.fastapi.order_by import order_by
|
||||
from creyPY.fastapi.pagination import Page, paginate
|
||||
from fastapi import APIRouter, Depends, HTTPException, Security
|
||||
from fastapi_filters import FilterValues, create_filters
|
||||
from fastapi_filters.ext.sqlalchemy import apply_filters
|
||||
from app.models.entry import LogType, TransactionType
|
||||
from datetime import datetime
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.sql.selectable import Select
|
||||
|
||||
from app.models.entry import LogEntry, LogType, TransactionType
|
||||
from app.schema.entry import LogIN, LogOUT
|
||||
from app.services.auth import verify
|
||||
|
||||
router = APIRouter(prefix="/log", tags=["logging"])
|
||||
|
||||
@@ -33,7 +31,7 @@ async def create_log(
|
||||
data,
|
||||
LogEntry,
|
||||
db,
|
||||
additonal_data={"created_by_id": sub},
|
||||
additional_data={"created_by_id": sub},
|
||||
)
|
||||
return LogOUT.model_validate(obj)
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from app.schema.common import BaseSchemaModelIN, BaseSchemaModelOUT
|
||||
|
||||
|
||||
class AppIN(BaseSchemaModelIN):
|
||||
name: str
|
||||
retention_days: int | SkipJsonSchema[None] = 30
|
||||
|
||||
|
||||
class AppOUT(BaseSchemaModelOUT, AppIN):
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from sqlalchemy_utils import create_database, database_exists
|
||||
|
||||
|
||||
def create_if_not_exists(db_name: str):
|
||||
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL
|
||||
|
||||
if not database_exists(SQLALCHEMY_DATABASE_URL + db_name):
|
||||
create_database(SQLALCHEMY_DATABASE_URL + db_name)
|
||||
38
app/setup.py
38
app/setup.py
@@ -1,10 +1,30 @@
|
||||
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.helpers import create_if_not_exists
|
||||
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, get_db, name
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from app.services.db.session import create_if_not_exists
|
||||
from app.models.app import Application
|
||||
from app.models.entry import LogEntry
|
||||
|
||||
|
||||
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):
|
||||
@@ -18,3 +38,17 @@ def setup(db_name=name):
|
||||
"script_location", os.path.join(os.path.dirname(os.path.dirname(__file__)), "alembic")
|
||||
)
|
||||
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")
|
||||
|
||||
118
app/test_main.py
118
app/test_main.py
@@ -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.models.base import Base
|
||||
from creyPY.fastapi.testing import GenericClient
|
||||
@@ -5,16 +8,55 @@ from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy_utils import create_database, database_exists, drop_database
|
||||
|
||||
from app.models.entry import LogEntry
|
||||
from app.services.auth import verify
|
||||
import contextlib
|
||||
from app.setup import delete_old_logs
|
||||
|
||||
from .main import app
|
||||
|
||||
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
|
||||
def app_context(self, name: str = "Testing"):
|
||||
app_id = self.create_app(name)
|
||||
def app_context(self, name: str = "Testing", retention_days: int | None = None):
|
||||
app_id = self.create_app(name, retention_days)
|
||||
try:
|
||||
yield app_id
|
||||
finally:
|
||||
@@ -23,45 +65,8 @@ def app_context(self, name: str = "Testing"):
|
||||
|
||||
@contextlib.contextmanager
|
||||
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:
|
||||
for entry in LOG_EXAMPLES:
|
||||
for entry in ENTRY_EXAMPLES:
|
||||
self.log_message({"application": app_id, **entry})
|
||||
yield app_id
|
||||
|
||||
@@ -86,6 +91,7 @@ class TestAPI:
|
||||
global CURRENT_USER
|
||||
return CURRENT_USER
|
||||
|
||||
self.db_instance = get_db_test()
|
||||
app.dependency_overrides[get_db] = get_db_test
|
||||
app.dependency_overrides[verify] = get_test_sub
|
||||
self.c = GenericClient(app)
|
||||
@@ -94,8 +100,8 @@ class TestAPI:
|
||||
drop_database(self.engine.url)
|
||||
|
||||
# HELPERS
|
||||
def create_app(self, name: str = "Testing"):
|
||||
re = self.c.post("/app/", {"name": name})
|
||||
def create_app(self, name: str = "Testing", retention_days: int | None = None):
|
||||
re = self.c.post("/app/", {"name": name, "retention_days": retention_days})
|
||||
return re["id"]
|
||||
|
||||
def destroy_app(self, app_id):
|
||||
@@ -260,3 +266,29 @@ class TestAPI:
|
||||
|
||||
re = self.c.get("/log/?application=" + str(app_id))
|
||||
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)
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.6.2.post1
|
||||
certifi==2024.8.30
|
||||
creyPY==1.2.5
|
||||
fastapi==0.115.5
|
||||
fastapi-pagination==0.12.31
|
||||
anyio==4.9.0
|
||||
certifi==2025.7.14
|
||||
creyPY[postgres]==3.0.0
|
||||
fastapi==0.116.1
|
||||
fastapi-pagination==0.13.3
|
||||
h11==0.14.0
|
||||
httpcore==1.0.6
|
||||
httpx==0.27.2
|
||||
httpcore==1.0.8
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
psycopg==3.2.4
|
||||
psycopg-binary==3.2.3
|
||||
psycopg-pool==3.2.3
|
||||
psycopg==3.2.9
|
||||
psycopg-binary==3.2.9
|
||||
psycopg-pool==3.2.6
|
||||
pydantic==2.9.2
|
||||
pydantic_core==2.23.4
|
||||
python-dotenv==1.0.1
|
||||
python-dotenv==1.1.1
|
||||
sniffio==1.3.1
|
||||
SQLAlchemy==2.0.35
|
||||
starlette==0.40.0
|
||||
typing_extensions==4.12.2
|
||||
SQLAlchemy==2.0.42
|
||||
starlette==0.47.2
|
||||
typing_extensions==4.14.1
|
||||
|
||||
Mako==1.3.5 # Alembic
|
||||
Mako==1.3.10 # Alembic
|
||||
MarkupSafe==3.0.1 # Alembic
|
||||
alembic==1.13.3 # Alembic
|
||||
alembic==1.16.4 # Alembic
|
||||
|
||||
SQLAlchemy-Utils==0.41.2 # SQLAlchemy
|
||||
|
||||
click==8.1.8 # Uvicorn
|
||||
uvicorn==0.31.1 # Uvicorn
|
||||
click==8.2.1 # Uvicorn
|
||||
uvicorn==0.35.0 # Uvicorn
|
||||
|
||||
iniconfig==2.0.0 # pytest
|
||||
packaging==24.1 # pytest
|
||||
pluggy==1.5.0 # pytest
|
||||
pytest==8.3.3 # pytest
|
||||
iniconfig==2.1.0 # pytest
|
||||
packaging==25.0 # pytest
|
||||
pluggy==1.6.0 # pytest
|
||||
pytest==8.4.1 # pytest
|
||||
|
||||
fastapi-filters==0.2.9 # Filters
|
||||
fastapi-filters==0.3.0 # Filters
|
||||
|
||||
APScheduler==3.11.0 # Scheduler for deletion
|
||||
tzlocal==5.3.1 # Scheduler for deletion
|
||||
|
||||
Reference in New Issue
Block a user