Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion csp_gateway/server/config/hydra/job_logging/custom.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ handlers:
stream: ext://sys.stdout
file:
level: DEBUG
class: logging.FileHandler
class: csp_gateway.server.shared.symlink_file_handler.SymlinkFileHandler
formatter: whenAndWhere
filename: ${hydra.runtime.output_dir}/csp-gateway.log
log_links_dir: ${hydra.runtime.cwd}/${hydra.job.name}_log_links
symlink_filename: ${hydra.job.name}_${now:%Y%m%d_%H%M%S}.log
root:
handlers: [console, file]
level: DEBUG
Expand Down
1 change: 1 addition & 0 deletions csp_gateway/server/shared/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .adapters import *
from .channel_selection import *
from .json_converter import *
from .symlink_file_handler import *
58 changes: 58 additions & 0 deletions csp_gateway/server/shared/symlink_file_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import logging
from pathlib import Path
from typing import Any

__all__ = ["SymlinkFileHandler"]

log = logging.getLogger(__name__)


class SymlinkFileHandler(logging.FileHandler):
def __init__(
self,
filename: str,
log_links_dir: str,
symlink_filename: str,
*args: Any,
**kwargs: Any,
) -> None:
self.log_links_dir = Path(log_links_dir)
self.symlink_filename = symlink_filename

super().__init__(filename, *args, **kwargs)

try:
self.log_links_dir.mkdir(parents=True, exist_ok=True)
log.debug("Ensured symlink directory exists: %s", self.log_links_dir)
except (OSError, PermissionError) as e:
log.error("Failed to create symlink directory %s: %s", self.log_links_dir, e, exc_info=True)
return

target = Path(self.baseFilename).resolve()

# Timestamped symlink
named_link = self.log_links_dir / self.symlink_filename
self._create_file_symlink(target, named_link)

# Latest symlink
self._create_file_symlink(target, self.log_links_dir / "latest.log")

@staticmethod
def _create_file_symlink(target: Path, symlink_path: Path) -> None:
if not target.exists():
log.error("Target for symlink does not exist: %s", target)
return

tmp = symlink_path.with_suffix(symlink_path.suffix + ".tmp")

try:
if tmp.exists() or tmp.is_symlink():
tmp.unlink()

tmp.symlink_to(target)
tmp.replace(symlink_path)

log.info("Created symlink %s -> %s", symlink_path, target)

except (OSError, PermissionError) as e:
log.error("Failed to create symlink %s: %s", symlink_path, e, exc_info=True)
127 changes: 127 additions & 0 deletions csp_gateway/tests/server/shared/test_symlink_file_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import logging

from csp_gateway.server.shared.symlink_file_handler import SymlinkFileHandler


def test_handler_creates_symlinks(tmp_path):
"""
SymlinkFileHandler should create:
- a timestamped symlink
- a latest.log symlink
"""

# Arrange
output_dir = tmp_path / "hydra_output"
log_links_dir = tmp_path / "job_log_links"

output_dir.mkdir()
log_links_dir.mkdir()

log_file = output_dir / "csp-gateway.log"

symlink_name = "csp-gateway_20260101_120000.log"

# Act
handler = SymlinkFileHandler(
filename=str(log_file),
log_links_dir=str(log_links_dir),
symlink_filename=symlink_name,
)

# Assert
assert log_file.exists()

timestamped_link = log_links_dir / symlink_name
latest_link = log_links_dir / "latest.log"

assert timestamped_link.is_symlink()
assert latest_link.is_symlink()

assert timestamped_link.resolve() == log_file.resolve()
assert latest_link.resolve() == log_file.resolve()

handler.close()


def test_handler_creates_log_links_dir_if_missing(tmp_path):
output_dir = tmp_path / "hydra_output"
output_dir.mkdir()

log_file = output_dir / "csp-gateway.log"
log_file.write_text("log")

log_links_dir = tmp_path / "missing_log_links"

handler = SymlinkFileHandler(
filename=str(log_file),
log_links_dir=str(log_links_dir),
symlink_filename="test.log",
)

assert log_links_dir.exists()
assert log_links_dir.is_dir()

handler.close()


def test_handler_writes_logs_through_symlink(tmp_path):
output_dir = tmp_path / "hydra_output"
output_dir.mkdir()

log_file = output_dir / "csp-gateway.log"
log_links_dir = tmp_path / "log_links"

handler = SymlinkFileHandler(
filename=str(log_file),
log_links_dir=str(log_links_dir),
symlink_filename="test.log",
)

test_logger = logging.getLogger("test_symlink_logger")
test_logger.addHandler(handler)
test_logger.setLevel(logging.INFO)

test_logger.info("Test log message")
handler.flush()

latest_symlink = log_links_dir / "latest.log"
assert latest_symlink.exists()
assert latest_symlink.is_symlink()

content = latest_symlink.read_text()
assert "Test log message" in content

handler.close()
test_logger.removeHandler(handler)


def test_latest_symlink_updates_on_new_handler(tmp_path):
output_dir1 = tmp_path / "run1"
output_dir2 = tmp_path / "run2"
output_dir1.mkdir()
output_dir2.mkdir()

log_links_dir = tmp_path / "log_links"

handler1 = SymlinkFileHandler(
filename=str(output_dir1 / "csp-gateway.log"),
log_links_dir=str(log_links_dir),
symlink_filename="run1.log",
)

latest = log_links_dir / "latest.log"
assert latest.is_symlink()
assert latest.resolve() == (output_dir1 / "csp-gateway.log").resolve()

handler1.close()

handler2 = SymlinkFileHandler(
filename=str(output_dir2 / "csp-gateway.log"),
log_links_dir=str(log_links_dir),
symlink_filename="run2.log",
)

assert latest.is_symlink()
assert latest.resolve() == (output_dir2 / "csp-gateway.log").resolve()

handler2.close()
Loading