try to fix logging

This commit is contained in:
Ross
2026-06-03 20:48:00 +01:00
parent 2e854c43b6
commit 3d445fe12e
7 changed files with 194 additions and 53 deletions
+1
View File
@@ -10,6 +10,7 @@ venv
.env .env
*.log *.log
log.txt
temp/ temp/
+58
View File
@@ -0,0 +1,58 @@
from __future__ import annotations
import pytest
from django.urls import reverse
from atlas.models import SeriesImage, UncategorisedDicom
@pytest.mark.django_db
def test_user_uploads_partial_import_button_prefers_case_for_hash_matched_series(
client,
admin_user,
create_series,
create_case,
make_case,
):
client.force_login(admin_user)
primary_case = create_case
primary_case.author.add(admin_user)
secondary_case = make_case(title="Secondary Case")
secondary_case.author.add(admin_user)
existing_series = create_series
existing_series.series_instance_uid = "atlas-series-1"
existing_series.study_instance_uid = "study-1"
existing_series.save(update_fields=["series_instance_uid", "study_instance_uid"])
primary_case.series.add(existing_series)
secondary_case.series.add(existing_series)
SeriesImage.objects.create(series=existing_series, image_blake3_hash="shared-hash")
UncategorisedDicom.objects.bulk_create(
[
UncategorisedDicom(
image="pending-upload.dcm",
user=admin_user,
series_instance_uid="uploaded-series-1",
image_blake3_hash="shared-hash",
basic_dicom_tags={
"SeriesInstanceUID": "uploaded-series-1",
"StudyInstanceUID": "study-1",
"StudyDescription": "Shared study",
"SeriesDescription": "Uploaded series",
"Modality": "CT",
},
)
]
)
response = client.get(reverse("atlas:user_uploads"))
assert response.status_code == 200
content = response.content.decode("utf-8")
assert 'class="btn btn-sm btn-warning import-partial-series-btn"' in content
assert f'data-case-id="{primary_case.pk}"' in content
+22 -2
View File
@@ -3357,6 +3357,7 @@ def user_uploads(
for series_uid, _, tags, _ in series_list for series_uid, _, tags, _ in series_list
} }
uploaded_uid_to_existing_series_ids: dict[str, set[int]] = defaultdict(set) uploaded_uid_to_existing_series_ids: dict[str, set[int]] = defaultdict(set)
uploaded_uid_to_case_ids: dict[str, set[int]] = defaultdict(set)
existing_series_lookup: dict[int, Series] = {} existing_series_lookup: dict[int, Series] = {}
case_to_study_uids = defaultdict(set) case_to_study_uids = defaultdict(set)
uploaded_hashes = { uploaded_hashes = {
@@ -3394,6 +3395,7 @@ def user_uploads(
continue continue
case_lookup[case_obj.pk] = case_obj case_lookup[case_obj.pk] = case_obj
case_to_series_uids[case_obj.pk].add(existing.series_instance_uid) case_to_series_uids[case_obj.pk].add(existing.series_instance_uid)
uploaded_uid_to_case_ids[existing.series_instance_uid].add(case_obj.pk)
if existing.study_instance_uid: if existing.study_instance_uid:
case_to_study_uids[case_obj.pk].add(existing.study_instance_uid) case_to_study_uids[case_obj.pk].add(existing.study_instance_uid)
@@ -3435,6 +3437,7 @@ def user_uploads(
continue continue
case_lookup[case_obj.pk] = case_obj case_lookup[case_obj.pk] = case_obj
case_to_series_uids[case_obj.pk].add(uploaded_uid) case_to_series_uids[case_obj.pk].add(uploaded_uid)
uploaded_uid_to_case_ids[uploaded_uid].add(case_obj.pk)
study_uid = uploaded_series_uid_to_study_uid.get(uploaded_uid) study_uid = uploaded_series_uid_to_study_uid.get(uploaded_uid)
if study_uid: if study_uid:
case_to_study_uids[case_obj.pk].add(study_uid) case_to_study_uids[case_obj.pk].add(study_uid)
@@ -3469,6 +3472,22 @@ def user_uploads(
if series_id in existing_series_lookup if series_id in existing_series_lookup
] ]
matched_case_counts = defaultdict(int)
for uploaded_uid in study_partial_imported_uids:
for case_pk in uploaded_uid_to_case_ids.get(uploaded_uid, set()):
matched_case_counts[case_pk] += 1
if matched_case_counts:
preferred_case_id, _ = max(
matched_case_counts.items(),
key=lambda item: (
item[1],
len(case_to_series_uids.get(item[0], set()) & study_uploaded_uids),
-item[0],
),
)
study_data["preferred_case_id"] = preferred_case_id
related_case_ids = set(case_to_series_uids.keys()) | set(case_to_study_uids.keys()) related_case_ids = set(case_to_series_uids.keys()) | set(case_to_study_uids.keys())
for case_pk in related_case_ids: for case_pk in related_case_ids:
imported_uids = case_to_series_uids.get(case_pk, set()) imported_uids = case_to_series_uids.get(case_pk, set())
@@ -3492,7 +3511,9 @@ def user_uploads(
} }
) )
if len(case_candidates_for_study) == 1: if study_data.get("preferred_case_id"):
pass
elif len(case_candidates_for_study) == 1:
study_data["preferred_case_id"] = next(iter(case_candidates_for_study)) study_data["preferred_case_id"] = next(iter(case_candidates_for_study))
elif study_data.get("import_suggestions"): elif study_data.get("import_suggestions"):
sorted_suggestions = sorted( sorted_suggestions = sorted(
@@ -3576,7 +3597,6 @@ def uploads_import_htmx(request, case_id: int | None = None):
imported = import_dicoms_helper(request, case_id=case_id, dicoms=dicoms) imported = import_dicoms_helper(request, case_id=case_id, dicoms=dicoms)
except Exception as exc: except Exception as exc:
logger.exception("Error during uploads import") logger.exception("Error during uploads import")
logging.getLogger(__name__).exception("Error during uploads import")
return HttpResponse( return HttpResponse(
f'<div class="alert alert-danger mb-0">Import failed: {escape(str(exc))}</div>', f'<div class="alert alert-danger mb-0">Import failed: {escape(str(exc))}</div>',
status=500, status=500,
+3
View File
@@ -3,9 +3,12 @@ import os
import sys import sys
from django.conf import settings from django.conf import settings
from rad.logging_setup import configure_loguru
if __name__ == "__main__": if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rad.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rad.settings")
configure_loguru()
# Set up debugger if we are running in a container # Set up debugger if we are running in a container
if os.environ.get('RUN_MAIN'): if os.environ.get('RUN_MAIN'):
+91
View File
@@ -0,0 +1,91 @@
from __future__ import annotations
import logging
import os
import sys
from pathlib import Path
from django.conf import settings
from loguru import logger
_CONFIGURED = False
def _is_truthy(value: str | None) -> bool:
if value is None:
return False
return value.lower() in {"1", "true", "yes", "on"}
def _get_stdout_level() -> str:
env_level = os.environ.get("LOGURU_STDOUT_LEVEL")
if env_level:
return env_level
debug_flag = os.environ.get("DEBUG")
if debug_flag is None:
try:
debug_flag = "1" if getattr(settings, "DEBUG", False) else "0"
except Exception:
debug_flag = "0"
return "DEBUG" if _is_truthy(debug_flag) else "INFO"
def _get_log_file_path() -> Path:
override = os.environ.get("LOGURU_FILE")
if override:
return Path(override)
try:
logging_config = getattr(settings, "LOGGING", None) or {}
handler = (logging_config.get("handlers") or {}).get("file") or {}
filename = handler.get("filename")
if filename:
path = Path(filename)
if path.is_absolute():
return path
return Path(getattr(settings, "BASE_DIR", Path.cwd())) / path
except Exception:
pass
try:
return Path(getattr(settings, "BASE_DIR", Path.cwd())) / "log.txt"
except Exception:
return Path.cwd() / "log.txt"
def configure_loguru() -> Path:
global _CONFIGURED
if _CONFIGURED:
return _get_log_file_path()
log_file_path = _get_log_file_path()
log_file_path.parent.mkdir(parents=True, exist_ok=True)
stdout_level = _get_stdout_level()
log_format = "[{time:DD/MMM/YYYY HH:mm:ss}] {level} [{name}:{line}] {message}"
logger.remove()
logger.add(
sys.stdout,
level=stdout_level,
enqueue=True,
backtrace=False,
diagnose=False,
format=log_format,
)
logger.add(
str(log_file_path),
level="DEBUG",
enqueue=True,
backtrace=True,
diagnose=False,
format=log_format + "\n{exception}",
rotation="10 MB",
retention="10 days",
)
logging.root.setLevel(logging.DEBUG)
_CONFIGURED = True
logger.debug("Loguru logging configured with stdout and file sinks (stdout={})", stdout_level)
return log_file_path
+16
View File
@@ -3,6 +3,22 @@ from __future__ import annotations
import pytest import pytest
from django.urls import reverse from django.urls import reverse
from rad import logging_setup
def test_loguru_uses_django_file_handler_path(settings, tmp_path, monkeypatch):
monkeypatch.setattr(logging_setup, "_CONFIGURED", False)
settings.BASE_DIR = str(tmp_path)
settings.LOGGING = {
"handlers": {
"file": {
"filename": "log.txt",
}
}
}
assert logging_setup._get_log_file_path() == tmp_path / "log.txt"
@pytest.mark.django_db @pytest.mark.django_db
def test_logs_view_filters_by_date_and_can_reset_log_file(client, admin_user, monkeypatch, tmp_path): def test_logs_view_filters_by_date_and_can_reset_log_file(client, admin_user, monkeypatch, tmp_path):
+3 -51
View File
@@ -1,60 +1,12 @@
""" """WSGI config for rad project."""
WSGI config for rad project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
from loguru import logger
import sys
from pathlib import Path
import logging
# Configure Loguru sinks early so app imports see consistent logging. from .logging_setup import configure_loguru
# Log to stdout (so `docker logs` shows messages) and to a file at
# `/var/log/rad/app.log` for persistent collection / host tailing.
try:
# ensure directory exists
log_dir = Path("/var/log/rad")
log_dir.mkdir(parents=True, exist_ok=True)
# Determine stdout level: allow overriding with LOGURU_STDOUT_LEVEL env var,
# otherwise expose DEBUG when DEBUG env var is truthy, fall back to INFO.
raw_debug = os.environ.get("DEBUG", "0")
env_level = os.environ.get("LOGURU_STDOUT_LEVEL")
if env_level:
stdout_level = env_level
else:
stdout_level = "DEBUG" if raw_debug.lower() in ("1", "true", "yes") else "INFO"
# Add stdout sink (if not already added by other code)
logger.remove() # remove default handlers to avoid duplicate lines
logger.add(sys.stdout, level=stdout_level, enqueue=True, backtrace=False, diagnose=False)
# Add rotating file sink for persistence in container. Use JSON lines
# (serialize=True) to make logs easy to query in Loki/Grafana.
logger.add(
str(log_dir / "app.log"),
rotation="10 MB",
retention="10 days",
level="DEBUG",
enqueue=True,
serialize=True,
)
logger.debug("Loguru logging configured with stdout and file sinks (stdout=%s)", stdout_level)
logging.root.setLevel(logging.DEBUG)
except Exception:
# Best-effort only — don't crash WSGI startup if logging can't be configured
try:
logger.add(sys.stderr, level="DEBUG", enqueue=True, backtrace=False, diagnose=False)
except Exception:
pass
logger.debug("Failed to configure Loguru logging sinks, continuing without file logging")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rad.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rad.settings")
configure_loguru()
application = get_wsgi_application() application = get_wsgi_application()