Skip to content

Commit ead84f5

Browse files
authored
Implement advanced logging (#44)
* feat: implement advanced logging with session and event logging * chore: bump version to 0.5.0 * chore: remove linting and scanning steps from Docker image build process * ADD: replace tox commands with ruff * ADD: fix linting, doc style and type errors * ADD: fix errors and python version in actions workflow * FIX: tests * ADD: add tests and minor fixes * FIX: noqa annotation * FIX: remove ip logging * UPDATE: help text with logging info * UPDATE: docstring naming * ADD: introducing uv, update Dockerfile accordingly, set flac as standard download format * UPDATE: fix tox issues * ADD: uv.lock file * UPDATE: tox testenv * UPDATE: pyproject.toml * UPDATE: project dependencies
1 parent 0abea7e commit ead84f5

28 files changed

Lines changed: 3276 additions & 159 deletions

.github/workflows/docker-image.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up Python
1818
uses: actions/setup-python@v4
1919
with:
20-
python-version: '3.11'
20+
python-version: '3.12'
2121

2222
- name: Install dependencies
2323
run: |
@@ -37,7 +37,7 @@ jobs:
3737
- name: Set up Python
3838
uses: actions/setup-python@v4
3939
with:
40-
python-version: '3.11'
40+
python-version: '3.12'
4141

4242
- name: Install dependencies
4343
run: |
@@ -46,7 +46,7 @@ jobs:
4646
4747
- name: Run tests with coverage
4848
run: |
49-
tox -e py311 -- --cov=src --cov-report=xml
49+
tox -e py312 -- --cov=src --cov-report=xml
5050
5151
- name: Upload Coverage Report
5252
uses: actions/upload-artifact@v4

Dockerfile

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
1-
FROM python:3.11-slim
1+
# ---------- Stage 1: Builder (Debian-based) ----------
2+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
3+
4+
ENV UV_COMPILE_BYTECODE=1
5+
ENV UV_LINK_MODE=copy
6+
7+
WORKDIR /app
8+
9+
# Copy lockfiles
10+
COPY pyproject.toml uv.lock ./
11+
12+
# Install dependencies into .venv
13+
RUN --mount=type=cache,target=/root/.cache/uv \
14+
uv sync --frozen --no-install-project --no-dev
15+
16+
# ---------- Stage 2: Runtime (Debian-based) ----------
17+
FROM python:3.12-slim
218

319
WORKDIR /app
420

21+
# Copy the venv from the builder
22+
COPY --from=builder /app/.venv /app/.venv
23+
24+
# Copy source code
525
COPY ./src /app/src
26+
27+
# Set environment variables
28+
ENV PATH="/app/.venv/bin:$PATH"
629
ENV PYTHONPATH=/app
730

31+
# Ensure logs dir exists
832
RUN mkdir -p /app/logs && chmod -R 777 /app/logs
9-
RUN pip install --no-cache-dir -r /app/src/requirements.txt
1033

1134
EXPOSE 8080
1235

13-
CMD ["python", "/app/src/__main__.py"]
36+
CMD ["python", "src/__main__.py"]

pyproject.toml

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "musicapi"
3-
version = "0.4.3"
3+
version = "0.5.0"
44
description = "Web interface for automatic music donwload and tracking."
55
authors = [
66
{ name="fsdmu"}
@@ -10,7 +10,7 @@ maintainers = [
1010
]
1111
readme = "README.md"
1212
license = { text = "GPL-3.0-or-later" }
13-
requires-python = ">=3.11"
13+
requires-python = ">=3.12"
1414

1515
dependencies = [
1616
"aiofiles>=25.1.0",
@@ -36,28 +36,28 @@ dependencies = [
3636
"idna>=3.11",
3737
"ifaddr>=0.2.0",
3838
"itsdangerous>=2.2.0",
39-
"Jinja2>=3.1.6",
39+
"jinja2>=3.1.6",
4040
"markdown2>=2.5.4",
41-
"MarkupSafe>=3.0.3",
41+
"markupsafe>=3.0.3",
4242
"multidict>=6.7.0",
4343
"nicegui>=3.4.1",
4444
"orjson>=3.11.5",
4545
"propcache>=0.4.1",
4646
"pydantic>=2.12.5",
47-
"pydantic_core>=2.41.5",
48-
"Pygments>=2.19.2",
47+
"pydantic-core>=2.41.5",
48+
"pygments>=2.19.2",
4949
"python-dotenv>=1.2.1",
5050
"python-engineio>=4.13.0",
5151
"python-multipart>=0.0.21",
5252
"python-socketio>=5.16.0",
53-
"PyYAML>=6.0.3",
53+
"pyyaml>=6.0.3",
5454
"requests>=2.32.5",
5555
"simple-websocket>=1.1.0",
5656
"sniffio>=1.3.1",
57-
"SQLAlchemy>=2.0.45",
57+
"sqlalchemy>=2.0.45",
5858
"starlette>=0.50.0",
5959
"typing-inspection>=0.4.2",
60-
"typing_extensions>=4.15.0",
60+
"typing-extensions>=4.15.0",
6161
"urllib3>=2.6.2",
6262
"uvicorn>=0.40.0",
6363
"uvloop>=0.22.1",
@@ -66,5 +66,32 @@ dependencies = [
6666
"wsproto>=1.3.2",
6767
"yarl>=1.22.0",
6868
"ytmusicapi>=1.11.4",
69-
"mysql-connector>=2.2.9"
70-
]
69+
"mysql-connector>=2.2.9",
70+
"psycopg2-binary>=2.9.11",
71+
]
72+
73+
[tool.ruff]
74+
line-length = 88
75+
target-version = "py312"
76+
77+
[tool.ruff.lint]
78+
select = ["E", "W", "F", "I", "D", "UP"]
79+
ignore = [
80+
"E203",
81+
"D212",
82+
]
83+
84+
[tool.ruff.lint.pydocstyle]
85+
convention = "google"
86+
87+
[tool.uv.workspace]
88+
members = [
89+
"example",
90+
]
91+
92+
[tool.setuptools.packages.find]
93+
where = ["src"]
94+
95+
[tool.pytest.ini_options]
96+
pythonpath = ["."]
97+
testpaths = ["tests"]

src/__main__.py

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
"""User interface for the MusicAPi."""
22

33
import logging
4-
import src.logging_config # noqa: F401
5-
from nicegui import ui
4+
import uuid
65

7-
from src.ui.theme import apply_theme
8-
from src.ui.components import SettingsDrawer, HelpDialog
6+
from nicegui import app, ui
7+
from starlette.requests import Request
8+
from starlette.responses import Response
9+
10+
from src.logging.event_logger import log_event
11+
from src.logging.log_database_connector import LogDatabaseConnector
12+
from src.logging_config import setup_logging, stop_logging
13+
from src.ui.components import HelpDialog, SettingsDrawer
914
from src.ui.logic import process_submission
15+
from src.ui.theme import apply_theme
1016

11-
logger = logging.getLogger(__name__)
17+
logger = logging.getLogger("app.music_api_ui")
1218
logger.setLevel(logging.INFO)
1319

20+
latest_url_value = ""
21+
22+
THRESHOLD = 4
23+
1424

1525
class MusicApiApp:
1626
"""Class for the MusicAPI user interface."""
1727

18-
def __init__(self):
28+
def __init__(self, session_id: str | None = None):
1929
"""Initialize the MusicApiApp."""
2030
apply_theme()
2131

32+
self.session_id = session_id
33+
2234
self.settings = SettingsDrawer()
2335
self.help = HelpDialog()
2436

@@ -31,6 +43,7 @@ def build_header(self):
3143
ui.button(icon="help", on_click=self.help.open).props("round flat")
3244
ui.button(icon="code", on_click=self.settings.toggle).props("round flat")
3345

46+
# noqa: D401
3447
def build_main_content(self):
3548
"""Build the main content for the MusicAPI user interface."""
3649
with ui.column().classes(
@@ -54,22 +67,85 @@ def build_main_content(self):
5467
.classes("mt-2")
5568
)
5669

57-
ui.button("Submit", on_click=self.handle_click).props(
58-
"color=#CB69C1"
59-
).classes("pink-btn w-full h-[50px] font-bold text-lg mt-4")
70+
# ensure submit handler logs and receives session_id
71+
ui.button(
72+
"Submit",
73+
on_click=lambda: self.handle_click(session_id=self.session_id),
74+
).props("color=#CB69C1").classes(
75+
"pink-btn w-full h-[50px] font-bold text-lg mt-4"
76+
)
77+
78+
@log_event("submit.click")
79+
async def handle_click(self, session_id: str | None = None):
80+
"""Handle a download submission. session_id is passed to logging wrapper.
6081
61-
async def handle_click(self):
62-
"""Handle a download submission."""
82+
Args:
83+
session_id: The session ID for logging context.
84+
85+
"""
6386
await process_submission(
64-
self.url_input, self.auto_dl.value, self.settings.audio_format.value
87+
self.url_input,
88+
self.auto_dl.value,
89+
self.settings.audio_format.value,
90+
session_id=session_id,
6591
)
6692

6793

94+
@log_event("page.view")
95+
def _log_page_view(session_id: str | None, user_agent: str, created: bool):
96+
"""Emit a structured page.view event via the log_event wrapper.
97+
98+
Args:
99+
session_id: The session ID for logging context.
100+
user_agent: The client's user agent string.
101+
created: Whether the session was newly created.
102+
103+
Returns:
104+
A dict containing client and session information.
105+
106+
"""
107+
return {
108+
"client": {"user_agent": user_agent},
109+
"session": {"id": session_id, "created": created},
110+
}
111+
112+
68113
@ui.page("/")
69-
def main_page():
70-
"""Start user interface for the MusicAPi."""
71-
MusicApiApp()
114+
def main_page(request: Request, response: Response) -> None:
115+
"""Start user interface for the MusicAPi.
116+
117+
Args:
118+
request: The incoming HTTP request.
119+
response: The HTTP response to be sent.
120+
121+
"""
122+
# create or reuse session id cookie
123+
session_id = request.cookies.get("session_id")
124+
created = False
125+
if not session_id:
126+
session_id = str(uuid.uuid4())
127+
response.set_cookie(
128+
"session_id",
129+
session_id,
130+
httponly=True,
131+
samesite="lax",
132+
max_age=400 * 24 * 3600,
133+
)
134+
created = True
135+
136+
user_agent = request.headers.get("user-agent", "")
137+
138+
_log_page_view(
139+
session_id=session_id,
140+
user_agent=user_agent,
141+
created=created,
142+
)
143+
144+
MusicApiApp(session_id=session_id)
145+
72146

147+
app.on_shutdown(stop_logging)
73148

74149
if __name__ in {"__main__", "__mp_main__"}:
75-
ui.run(host="0.0.0.0", port=8080, title="MusicAPI")
150+
setup_logging(LogDatabaseConnector())
151+
ui.run(host="0.0.0.0", port=8080, title="MusicAPI", uvicorn_logging_level="warning")

src/auto_download_artists.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
"""Automatically download albums from artists marked for auto-download."""
22

33
import logging
4-
import src.logging_config # initialize logging # noqa: F401
54

6-
from src.youtube_handler.me_tube_connector import MeTubeConnector
5+
import src.logging_config # initialize logging # noqa: F401
76
from src.database_connector import DatabaseConnector
7+
from src.youtube_handler.me_tube_connector import MeTubeConnector
88
from src.youtube_handler.youtube_album_fetcher import YoutubeAlbumFetcher
99

10-
logger = logging.getLogger(__name__)
10+
logger = logging.getLogger("app.auto_download_artists")
1111
logger.setLevel(logging.INFO)
1212

1313

1414
def main():
15-
"""Automatically download albums from artists marked for auto-download."""
15+
"""Automatically downloads albums from artists marked for auto-download."""
1616
db = DatabaseConnector()
1717
mt = MeTubeConnector(base_url=None)
1818

0 commit comments

Comments
 (0)