Skip to content

Commit

Permalink
Merge pull request #376 from datamel/rotate-logging
Browse files Browse the repository at this point in the history
Add logging_util and rotating log archive for uiserver logs
  • Loading branch information
wxtim authored Aug 19, 2022
2 parents 4d14920 + e95e263 commit 549afbd
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 4 deletions.
10 changes: 10 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ creating a new release entry be sure to copy & paste the span tag with the
`actions:bind` attribute, which is used by a regex to find the text to be
updated. Only the first match gets replaced, so it's fine to leave the old
ones in. -->

-------------------------------------------------------------------------------
## __cylc-uiserver-1.2.0 (<span actions:bind='release-date'>Upcoming</span>)__

### Enhancements

[#376](https://github.com/cylc/cylc-uiserver/pull/376) -
UIServer logs are now archived. The five most recent logs are retained (located
in `~/.cylc/uiserver/log/`). A new log is created with each UIServer instance.

-------------------------------------------------------------------------------
## __cylc-uiserver-1.1.0 (<span actions:bind='release-date'>Released 2022-07-28</span>)__

Expand Down
9 changes: 8 additions & 1 deletion cylc/uiserver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@

import os
from typing import Dict

from cylc.uiserver.app import CylcUIServer

from cylc.uiserver.logging_util import RotatingUISFileHandler


def init_log():
LOG = RotatingUISFileHandler()
# set up uiserver log
LOG.on_start()


def _jupyter_server_extension_points():
"""
Expand Down
5 changes: 2 additions & 3 deletions cylc/uiserver/jupyter_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@

from cylc.uiserver import (
__file__ as uis_pkg,
getenv,
)
getenv)
from cylc.uiserver.app import USER_CONF_ROOT


Expand Down Expand Up @@ -72,7 +71,7 @@
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'INFO',
'filename': str(USER_CONF_ROOT / 'uiserver.log'),
'filename': str(USER_CONF_ROOT / 'log' / 'log'),
'mode': 'a',
'backupCount': 5,
'maxBytes': 10485760,
Expand Down
72 changes: 72 additions & 0 deletions cylc/uiserver/logging_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from contextlib import suppress
from glob import glob
import logging
import os
from pathlib import Path

from typing import List

from cylc.uiserver.app import USER_CONF_ROOT


class RotatingUISFileHandler(logging.handlers.RotatingFileHandler):
"""Rotate logs on ui-server restart"""

LOG_NAME_EXTENSION = "-uiserver.log"

def __init__(self):
self.file_path = Path(USER_CONF_ROOT / "log").expanduser()

def on_start(self):
"""Set up logging"""
self.file_path.mkdir(parents=True, exist_ok=True)
self.delete_symlink()
self.update_log_archive()
self.setup_new_log()

def update_log_archive(self):
"""Ensure log archive retains only 5 logs"""
log_files = sorted(glob(os.path.join(
self.file_path, f"[0-9]*{self.LOG_NAME_EXTENSION}")), reverse=True)
while len(log_files) > 4:
os.unlink(log_files.pop(0))
# rename logs, logs sent in descending order to prevent conflicts
self.rename_logs(log_files)

def rename_logs(self, log_files: List[str]):
"""Increment the log number by one for each log"""
for file in log_files:
log_num = int(Path(file).name.partition('-')[0]) + 1
new_file_name = Path(
f"{self.file_path}/{log_num:02d}{self.LOG_NAME_EXTENSION}"
)
Path(file).rename(new_file_name)

def delete_symlink(self):
"""Deletes an existing log symlink."""
symlink_path = Path(self.file_path / 'log')
if symlink_path.exists() and symlink_path.is_symlink():
symlink_path.unlink()

def setup_new_log(self):
"""Create log"""
log = Path(self.file_path / f'01{self.LOG_NAME_EXTENSION}')
log.touch()
symlink_path = Path(self.file_path / 'log')
with suppress(OSError):
symlink_path.symlink_to(log)
2 changes: 2 additions & 0 deletions cylc/uiserver/scripts/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
For a multi-user system see `cylc hub`.
"""

from cylc.uiserver import init_log
from cylc.uiserver.app import CylcUIServer


def main(*argv):
init_log()
return CylcUIServer.launch_instance(argv or None)
2 changes: 2 additions & 0 deletions cylc/uiserver/scripts/hubapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
c.Spawner.cmd = ['cylc', 'hubapp']
"""

from cylc.uiserver import init_log
from cylc.uiserver.hubapp import CylcHubApp

INTERNAL = True


def main(*argv):
init_log()
return CylcHubApp.launch_instance(argv or None)
94 changes: 94 additions & 0 deletions cylc/uiserver/tests/test_logging_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from glob import glob
import os
import pytest

from pathlib import Path

from cylc.uiserver.logging_util import RotatingUISFileHandler


def test_update_log_archive(tmp_path):
"""Test update log archive retains log count at 5"""
LOG = RotatingUISFileHandler()
LOG.file_path = Path(tmp_path/'.cylc'/'uiserver'/'log')
LOG.file_path.mkdir(parents=True, exist_ok=True)
for file in [
'03-uiserver.log',
'01-uiserver.log',
'04-uiserver.log',
'02-uiserver.log',
'05-uiserver.log',
'07-uiserver.log',
'06-uiserver.log'
]:
log_file = LOG.file_path.joinpath(file)
log_file.touch()

LOG.update_log_archive()
log_files = glob(os.path.join(LOG.file_path, f"[0-9]*.log"))
expected_files = [
f'{LOG.file_path}/05-uiserver.log',
f'{LOG.file_path}/04-uiserver.log',
f'{LOG.file_path}/03-uiserver.log',
f'{LOG.file_path}/02-uiserver.log',
]
assert len(log_files) == 4
assert sorted(log_files) == sorted(expected_files)


def test_setup_new_log(tmp_path):
"""Checks new log is set up correctly."""
LOG = RotatingUISFileHandler()
LOG.file_path = Path(tmp_path/'.cylc'/'uiserver'/'log')
LOG.file_path.mkdir(parents=True, exist_ok=True)
LOG.setup_new_log()
new_log = Path(LOG.file_path / '01-uiserver.log')
symlink_log = Path(LOG.file_path / 'log')
assert new_log.exists()
assert symlink_log.is_symlink()
assert symlink_log.resolve() == new_log


def test_first_init_log(tmp_path):
"""Check initial setup, no previous logs present"""
LOG = RotatingUISFileHandler()
LOG.file_path = Path(tmp_path/'.cylc'/'uiserver'/'log')
LOG.on_start()
assert LOG.file_path.exists()
assert Path(LOG.file_path / 'log').is_symlink()
assert Path(LOG.file_path / 'log').resolve() == Path(
LOG.file_path / '01-uiserver.log')


def test_init_log_with_established_setup(tmp_path):
"""Tests the entire logging init with a typically full logging dir"""
LOG = RotatingUISFileHandler()
LOG.file_path = Path(tmp_path/'.cylc'/'uiserver'/'log')
LOG.file_path.mkdir(parents=True, exist_ok=True)
for i in range(1, 5):
file = (LOG.file_path / f'{i:02d}-uiserver.log')
file.touch()
file.write_text(f"This file started as: {file}")
symlink_log = Path(LOG.file_path / 'log')
symlink_log.symlink_to(LOG.file_path / f'01-uiserver.log')
LOG.on_start()
log_files = glob(os.path.join(LOG.file_path, f"*.log"))
assert len(log_files) == 5
actual_output = Path(LOG.file_path/'05-uiserver.log').read_text()
expected_output = f"This file started as: {LOG.file_path}/04-uiserver.log"
assert actual_output == expected_output

0 comments on commit 549afbd

Please sign in to comment.