Skip to content

Commit 965c8b9

Browse files
chores: add an example of how logging can be used across modules
1 parent d4b3306 commit 965c8b9

10 files changed

Lines changed: 2151 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ classifiers = [
2828
"Topic :: Scientific/Engineering :: Bio-Informatics"
2929
]
3030
dependencies = [
31+
"numpy>=2.2.6",
3132
"toml"
3233
]
3334
description = "This is session handler, configuration and logging handler for Saezlab packages and applications."

saezlab_core/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
# https://opensource.org/license/mit
1414
#
1515

16+
from .logger import get_logger
17+
from .session import Session
18+
19+
__all__ = ['get_logger', 'Session']
20+
1621
"""This is session handler, configuration and logging handler for Saezlab packages and applications."""
1722

1823
__all__ = [
1924
'__version__',
2025
'__author__',
2126
]
2227

23-
from ._metadata import __author__, __version__
28+
# from ._metadata import __author__, __version__

saezlab_core/logger.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import sys
2+
from typing import Optional
3+
import logging
4+
from datetime import datetime
5+
from logging.handlers import RotatingFileHandler
6+
7+
__all__ = [
8+
'DATE_FORMAT',
9+
'LOG_FORMAT',
10+
'get_logger',
11+
'get_timestamped_log_path',
12+
'setup_logging',
13+
]
14+
15+
# Default format as per the requirements, adding levelname and module name for context.
16+
LOG_FORMAT = '[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s'
17+
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
18+
19+
20+
def setup_logging(
21+
level: int = logging.INFO,
22+
log_file: Optional[str] = None,
23+
max_bytes: int = 10 * 1024 * 1024, # 10 MB
24+
backup_count: int = 5,
25+
):
26+
"""Configures the root logger for the application with log rotation.
27+
28+
This sets up handlers for console and optional file logging.
29+
It uses a standardized format for all log messages and rotates
30+
log files when they reach a specified size.
31+
32+
:param level: The minimum logging level to capture (e.g., logging.INFO).
33+
:param log_file: Optional path to a file for log output.
34+
:param max_bytes: The maximum size in bytes for a log file before it is rotated.
35+
:param backup_count: The number of backup log files to keep.
36+
"""
37+
formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT)
38+
39+
handlers = []
40+
41+
# Console handler
42+
console_handler = logging.StreamHandler(sys.stdout)
43+
console_handler.setFormatter(formatter)
44+
handlers.append(console_handler)
45+
46+
# Rotating file handler (if a path is provided)
47+
if log_file:
48+
# Use RotatingFileHandler for automatic log rotation.
49+
file_handler = RotatingFileHandler(
50+
log_file, maxBytes=max_bytes, backupCount=backup_count
51+
)
52+
file_handler.setFormatter(formatter)
53+
handlers.append(file_handler)
54+
55+
# The `force=True` argument removes any existing handlers
56+
# on the root logger, ensuring our configuration is the only one.
57+
logging.basicConfig(level=level, handlers=handlers, force=True)
58+
59+
# Set up a hook for unhandled exceptions to be logged automatically.
60+
# This addresses the "Log traceback" requirement.
61+
def handle_exception(exc_type, exc_value, exc_traceback):
62+
if issubclass(exc_type, KeyboardInterrupt):
63+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
64+
return
65+
logging.getLogger().critical(
66+
'Unhandled exception', exc_info=(exc_type, exc_value, exc_traceback)
67+
)
68+
69+
sys.excepthook = handle_exception
70+
71+
72+
def get_timestamped_log_path(log_dir: str, app_name: str) -> str:
73+
"""Generates a timestamped log file path.
74+
75+
:param log_dir: The directory where the log file should be stored.
76+
:param app_name: The base name for the log file.
77+
:return: A string representing the full path to the log file.
78+
"""
79+
timestamp = datetime.now().strftime('%Y-%m-%d')
80+
return f'{log_dir}/{app_name}_{timestamp}.log'
81+
82+
83+
def get_logger(name: str) -> logging.Logger:
84+
"""Returns a logger instance for the given name.
85+
86+
This is the primary function that developers will use to get a
87+
pre-configured logger within their modules.
88+
"""
89+
return logging.getLogger(name)

saezlab_core/module_a.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from saezlab_core import get_logger
2+
3+
__all__ = [
4+
'run_task_a',
5+
]
6+
7+
log = get_logger(__name__)
8+
9+
10+
def run_task_a():
11+
"""A dummy task that logs its progress."""
12+
log.info('Starting Task A.')
13+
log.debug('Performing some complex calculations for Task A...')
14+
# Simulate some work
15+
result = 2 + 2
16+
log.info(f'Task A finished with result: {result}')
17+
return result

saezlab_core/module_b.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from saezlab_core import get_logger
2+
3+
__all__ = [
4+
'run_task_b',
5+
]
6+
7+
log = get_logger(__name__)
8+
9+
10+
def run_task_b():
11+
"""Another dummy task that logs its progress and might encounter an issue."""
12+
log.info('Starting Task B.')
13+
log.warning(
14+
'This task is deprecated and will be removed in a future version.'
15+
)
16+
# Simulate a potential issue
17+
data = None
18+
if data is None:
19+
log.error('Input data for Task B is missing.')
20+
return None
21+
22+
log.info('Task B finished.')
23+
return True

saezlab_core/module_c.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
__all__ = [
2+
'utility_function',
3+
]
4+
5+
def utility_function():
6+
"""A simple utility function that does not perform any logging.
7+
Its operations are silent.
8+
"""
9+
# This function is simple and does not need to report anything.
10+
# For example, a pure calculation.
11+
12+
try:
13+
# Perform some calculations
14+
10 / 0
15+
except Exception:
16+
# Even in case of error, we do not log anything.
17+
print('An error occurred, but we are not logging it.')
18+
19+
return 'This is a silent utility.'

saezlab_core/module_d.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from saezlab_core import get_logger
2+
3+
__all__ = [
4+
'run_task_division',
5+
]
6+
7+
log = get_logger(__name__)
8+
9+
10+
def run_task_division():
11+
"""A task that performs division and handles division by zero."""
12+
log.info('Starting Division Task.')
13+
numerator = 10
14+
denominator = 0 # This will cause a division by zero error
15+
16+
try:
17+
result = numerator / denominator
18+
log.info(f'Division result: {result}')
19+
return result
20+
except ZeroDivisionError:
21+
log.exception('Division by zero encountered in Division Task.')
22+
return None

saezlab_core/session.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import os
2+
from typing import Optional
3+
import logging
4+
5+
from . import logger
6+
7+
__all__ = [
8+
'Session',
9+
]
10+
11+
12+
class Session:
13+
"""Manages the application session, primarily for initializing the logger.
14+
This class ensures that logging is configured only once.
15+
"""
16+
17+
_initialized = False
18+
19+
@classmethod
20+
def initialize(
21+
cls,
22+
log_level: str = 'INFO',
23+
log_path: Optional[str] = None,
24+
app_name: str = 'application',
25+
):
26+
"""Initializes the session by setting up the logging system.
27+
28+
This method should be called once at the application's entry point.
29+
If `log_path` is a directory, a timestamped log file will be created within it.
30+
If `log_path` is a file path, it will be used directly.
31+
If `log_path` is None, logs will only be sent to the console.
32+
33+
:param log_level: The logging level as a string (e.g., "DEBUG", "INFO").
34+
:param log_path: Optional path to a log file or a directory for logs.
35+
:param app_name: The base name for the application, used for timestamped logs.
36+
"""
37+
if cls._initialized:
38+
# Using the logger itself to warn about re-initialization.
39+
logging.getLogger(__name__).warning(
40+
'Session.initialize() called more than once.'
41+
)
42+
return
43+
44+
# Convert string level to logging constant
45+
level = getattr(logging, log_level.upper(), logging.INFO)
46+
47+
log_file_to_use = None
48+
if log_path:
49+
# If the path is a directory, create a timestamped log file inside it.
50+
# Otherwise, assume it's a full file path.
51+
if os.path.isdir(log_path) or not os.path.splitext(log_path)[1]:
52+
os.makedirs(log_path, exist_ok=True)
53+
log_file_to_use = logger.get_timestamped_log_path(
54+
log_path, app_name
55+
)
56+
else:
57+
# Ensure the directory for the file exists.
58+
log_dir = os.path.dirname(log_path)
59+
if log_dir:
60+
os.makedirs(log_dir, exist_ok=True)
61+
log_file_to_use = log_path
62+
63+
logger.setup_logging(level=level, log_file=log_file_to_use)
64+
logging.getLogger(__name__).info(
65+
f'Logging session initialized with level {log_level.upper()}.'
66+
)
67+
68+
cls._initialized = True

scripts/main.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import numpy as np
2+
3+
from saezlab_core import ( # TODO: Every package should have this import
4+
Session,
5+
module_a,
6+
module_b,
7+
module_c,
8+
module_d,
9+
get_logger,
10+
)
11+
12+
__all__ = [
13+
'main',
14+
]
15+
16+
17+
def main():
18+
"""A more complex main function to demonstrate logging from multiple modules."""
19+
# Initialize the session once at the start.
20+
# The user only needs to provide the log directory and an application name.
21+
Session.initialize(
22+
log_level='ERROR', log_path='./log', app_name='saezlab_core'
23+
)
24+
25+
# Get a logger for the main application entry point.
26+
log = get_logger(__name__) # TODO: Every package should have this import
27+
28+
log.info('Main application process starting.')
29+
30+
# --- Run Task A ---
31+
log.info('Calling module A...')
32+
result_a = module_a.run_task_a()
33+
log.debug(f'Module A returned: {result_a}')
34+
35+
# --- Run Task B ---
36+
log.info('Calling module B...')
37+
result_b = module_b.run_task_b()
38+
if result_b is None:
39+
log.warning('Module B indicated a problem, but we are continuing.')
40+
41+
# --- Use Module C ---
42+
log.info('Calling module C (the silent one)...')
43+
result_c = module_c.utility_function()
44+
# Note: We log about module C from main.py, but module_c.py itself is silent.
45+
log.debug(f"Module C returned: '{result_c}'")
46+
47+
# --- Additional Complex Operation ---
48+
log.info('Performing a complex operation in main...')
49+
array = np.array([1, 2, 3, 4, 5])
50+
mean_value = np.mean(array)
51+
log.debug(f'Computed mean of array {array}: {mean_value}')
52+
53+
# --- Handle an Exception ---
54+
log.info('Attempting a risky operation...')
55+
result = module_d.run_task_division()
56+
if result is None:
57+
log.error('Risky operation failed due to an error.')
58+
59+
log.info('Main application process finished.')
60+
61+
62+
if __name__ == '__main__':
63+
main()

0 commit comments

Comments
 (0)