try to fix logging
This commit is contained in:
@@ -10,6 +10,7 @@ venv
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
|
log.txt
|
||||||
|
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,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'):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user