Skip to content

Commit

Permalink
Merge pull request #26 from ONSdigital/dev_advanced_logging
Browse files Browse the repository at this point in the history
Advanced Logging Function
  • Loading branch information
pricemg authored Sep 18, 2023
2 parents 5c22469 + 78739ca commit b0f3405
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [semantic versioning](https://semver.org/spec/v2.0.0
- Add the helpers_spark.py and test_helpers_spark.py modules from cprices-utils.
- Add logging.py and test_logging.py module from cprices-utils.
- Add the helpers_python.py and test_helpers_python.py modules from cprices-utils.
- Add `init_logger_advanced` in `helpers/logging.py` module.
- Add in the general validation functions from cprices-utils.

### Changed
Expand Down
99 changes: 98 additions & 1 deletion rdsa_utils/helpers/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from functools import partial
import logging
from textwrap import dedent
from typing import Callable, Dict, Optional
from typing import Callable, Dict, List, Optional

from humanfriendly import format_timespan
import pandas as pd
Expand Down Expand Up @@ -64,6 +64,103 @@ def init_logger_basic(log_level: int) -> None:
""")


def init_logger_advanced(
log_level: int,
handlers: Optional[List[logging.Handler]] = None,
log_format: str = None,
date_format: str = None,
) -> None:
"""Instantiate a logger with provided handlers.
This function allows the logger to be used across modules. Logs can be
handled by any number of handlers, e.g., FileHandler, StreamHandler, etc.,
provided in the `handlers` list.
Parameters
----------
log_level
The level of logging to be recorded. Can be defined either as the
integer level or the logging.<LEVEL> values in line with the definitions
of the logging module.
(see - https://docs.python.org/3/library/logging.html#levels)
handlers
List of handler instances to be added to the logger. Each handler
instance must be a subclass of `logging.Handler`. Default is an
empty list, and in this case, basicConfig with `log_level`,
`log_format`, and `date_format` is used.
log_format
The format of the log message. If not provided, a default format
`'%(asctime)s %(levelname)s %(name)s: %(message)s'` is used.
date_format
The format of the date in the log message. If not provided, a default
format `'%Y-%m-%d %H:%M:%S'` is used.
Returns
-------
None
The logger created by this function is available in any other modules
by using `logging.getLogger(__name__)` at the global scope level in a
module (i.e., below imports, not in a function).
Raises
------
ValueError
If any item in the `handlers` list is not an instance of
`logging.Handler`.
Examples
--------
>>> file_handler = logging.FileHandler('logfile.log')
>>> rich_handler = RichHandler()
>>> init_logger_advanced(
... logging.DEBUG,
... [file_handler, rich_handler],
... "%(levelname)s: %(message)s",
... "%H:%M:%S"
... )
"""
# Set default log format and date format if not provided
if log_format is None:
log_format = '%(asctime)s %(levelname)s %(name)s: %(message)s'
if date_format is None:
date_format = '%Y-%m-%d %H:%M:%S'

# Prepare a formatter
formatter = logging.Formatter(log_format, date_format)

# Create a logger
logger = logging.getLogger(__name__)
logger.setLevel(log_level)

# Check if handlers is None, if so assign an empty list to it
if handlers is None:
handlers = []

# Validate each handler
for handler in handlers:
if not isinstance(handler, logging.Handler):
msg = (
f'Handler {handler} is not an instance of '
f'logging.Handler or its subclasses'
)
raise ValueError(
msg,
)

handler.setFormatter(formatter)
logger.addHandler(handler)

# If no handlers provided, use basicConfig
if not handlers:
logging.basicConfig(
level=log_level,
format=log_format,
datefmt=date_format,
)

logger.debug('Initialised logger for pipeline.')


def timer_args(
name: str,
logger: Optional[Callable[[str], None]] = logger.info,
Expand Down
35 changes: 35 additions & 0 deletions tests/helpers/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)

from rdsa_utils.helpers.logging import (
init_logger_advanced,
timer_args,
print_full_table_and_raise_error,
)
Expand Down Expand Up @@ -152,3 +153,37 @@ class TestAddWarningMessageToFunction:
def test_expected(self):
"""Test expected behaviour."""
pass


class TestInitLoggerAdvanced:
"""Tests for init_logger_advanced function."""

def test_logger_with_no_handler(self, caplog):
"""Test whether a logger is properly initialized with no handlers."""
caplog.set_level(logging.DEBUG)
init_logger_advanced(logging.DEBUG)
assert caplog.records[0].levelname == 'DEBUG'

def test_logger_with_handlers(self, caplog):
"""Test whether a logger is properly initialized with a valid handler."""
caplog.set_level(logging.DEBUG)
handler = logging.FileHandler('logfile.log')
handlers = [handler]
init_logger_advanced(logging.DEBUG, handlers)

logger = logging.getLogger('rdsa_utils.helpers.logging')

assert caplog.records[0].levelname == 'DEBUG'
assert any(isinstance(h, type(handler)) for h in logger.handlers)

def test_logger_with_invalid_handler(self):
"""Test whether a ValueError is raised when an invalid handler is provided."""
log_level = logging.DEBUG
invalid_handler = 'I am not a handler'
handlers = [invalid_handler]
with pytest.raises(ValueError) as exc_info:
init_logger_advanced(log_level, handlers)
assert (
str(exc_info.value)
== f'Handler {invalid_handler} is not an instance of logging.Handler or its subclasses'
)

0 comments on commit b0f3405

Please sign in to comment.