Skip to content

Commit 0ba8b50

Browse files
feat: Add logger (#2)
* feat: Add logger * feat: Add logger * feat: Add logger * fix: pre-commit * feat: general enhancements --------- Co-authored-by: Roberto Pastor Muela <[email protected]>
1 parent a71ce0b commit 0ba8b50

File tree

5 files changed

+206
-4
lines changed

5 files changed

+206
-4
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ repos:
2525
- id: add-license-headers
2626
files: '(src|examples|tests|docker)/.*\.(py)|\.(proto)'
2727
args:
28-
- --start_year=2024
28+
- --start_year=2025
2929

3030
# this validates our github workflow files
3131
- repo: https://github.com/python-jsonschema/check-jsonschema

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2024 - 2025 Ansys Internal
3+
Copyright (c) 2025 Ansys Internal
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

src/ansys/tools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates.
1+
# Copyright (C) 2025 ANSYS, Inc. and/or its affiliates.
22
# SPDX-License-Identifier: MIT
33
#
44
#

src/ansys/tools/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates.
1+
# Copyright (C) 2025 ANSYS, Inc. and/or its affiliates.
22
# SPDX-License-Identifier: MIT
33
#
44
#

src/ansys/tools/logger.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Copyright (C) 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Provides the singleton helper class for the logger."""
24+
25+
# logger from https://gist.github.com/huklee/cea20761dd05da7c39120084f52fcc7c
26+
import datetime
27+
import logging
28+
from pathlib import Path
29+
30+
31+
class SingletonType(type):
32+
"""Provides the singleton helper class for the logger."""
33+
34+
_instances = {}
35+
36+
def __call__(cls, *args, **kwargs):
37+
"""Call to redirect new instances to the singleton instance."""
38+
if cls not in cls._instances:
39+
cls._instances[cls] = super(SingletonType, cls).__call__(*args, **kwargs)
40+
return cls._instances[cls]
41+
42+
43+
class CustomFormatter(logging.Formatter):
44+
"""Custom formatter to truncate long columns."""
45+
46+
def set_column_width(self, width: int):
47+
"""Set the maximum column width for module and function names."""
48+
# at least 8
49+
if width < 8:
50+
raise ValueError("Column width must be at least 8 characters.")
51+
self._max_column_width = width
52+
53+
@property
54+
def max_column_width(self):
55+
"""Get the maximum column length."""
56+
if not hasattr(self, "_max_column_width"):
57+
self._max_column_width = 15
58+
return self._max_column_width
59+
60+
def format(self, record):
61+
"""Format the log record, truncating the module and function names if necessary."""
62+
if len(record.module) > self.max_column_width:
63+
record.module = record.module[: self.max_column_width - 3] + "..."
64+
if len(record.funcName) > self.max_column_width:
65+
record.funcName = record.funcName[: self.max_column_width - 3] + "..."
66+
67+
# Fill the module and function names with spaces to align them
68+
record.module = record.module.ljust(self.max_column_width)
69+
record.funcName = record.funcName.ljust(self.max_column_width)
70+
71+
return super().format(record)
72+
73+
74+
class Logger(object, metaclass=SingletonType):
75+
"""Provides the singleton logger.
76+
77+
Parameters
78+
----------
79+
level : int, default: ``logging.ERROR``
80+
Output Level of the logger.
81+
logger_name : str, default: ``"Logger"``
82+
Name of the logger.
83+
column_width : int, default: ``15``
84+
Maximum width of the module and function names in the log output.
85+
86+
"""
87+
88+
_logger = None
89+
90+
def __init__(self, level: int = logging.ERROR, logger_name: str = "Logger", column_width: int = 15):
91+
"""Initialize the logger."""
92+
self._logger = logging.getLogger(logger_name)
93+
self._logger.setLevel(level)
94+
self._formatter = CustomFormatter(
95+
"%(asctime)s [%(levelname)-8s | %(module)s | %(funcName)s:%(lineno)-4d] > %(message)s"
96+
)
97+
self._formatter.set_column_width(column_width)
98+
99+
def get_logger(self):
100+
"""Get the logger.
101+
102+
Returns
103+
-------
104+
Logger
105+
Logger.
106+
107+
"""
108+
return self._logger
109+
110+
def set_level(self, level: int):
111+
"""Set the logger output level.
112+
113+
Parameters
114+
----------
115+
level : int
116+
Output Level of the logger.
117+
118+
"""
119+
self._logger.setLevel(level=level)
120+
121+
def enable_output(self, stream=None):
122+
"""Enable logger output to a given stream.
123+
124+
If a stream is not specified, ``sys.stderr`` is used.
125+
126+
Parameters
127+
----------
128+
stream: TextIO, default: ``sys.stderr``
129+
Stream to output the log output to.
130+
131+
"""
132+
# stdout
133+
stream_handler = logging.StreamHandler(stream)
134+
stream_handler.setFormatter(self._formatter)
135+
self._logger.addHandler(stream_handler)
136+
137+
def add_file_handler(self, logs_dir: str | Path = ".log"):
138+
"""Save logs to a file in addition to printing them to the standard output.
139+
140+
Parameters
141+
----------
142+
logs_dir : str or Path, default: ``".log"``
143+
Directory where the log file will be saved. If it does not exist, it will be created.
144+
"""
145+
now = datetime.datetime.now()
146+
logs_dir = Path(logs_dir) if isinstance(logs_dir, str) else logs_dir
147+
if not logs_dir.is_dir():
148+
logs_dir.mkdir(parents=True)
149+
file_handler = logging.FileHandler(logs_dir / f"log_{now.strftime('%Y%m%d_%H%M%S')}.log")
150+
file_handler.setFormatter(self._formatter)
151+
self._logger.addHandler(file_handler)
152+
153+
header = (
154+
"-" * (70 + self._formatter.max_column_width)
155+
+ "\n"
156+
+ f"Timestamp [Level | Module{' ' * (self._formatter.max_column_width - 6)} | Function{' ' * (self._formatter.max_column_width - 8)}:Line] > Message\n" # noqa: E501
157+
+ "-" * (70 + self._formatter.max_column_width)
158+
+ "\n"
159+
)
160+
161+
# Log the header to the file
162+
file_handler.stream.write(header)
163+
164+
def debug(self, *args, **kwargs):
165+
"""Log a message with level DEBUG."""
166+
return self._logger.debug(*args, **kwargs, stacklevel=2)
167+
168+
def info(self, *args, **kwargs):
169+
"""Log a message with level INFO."""
170+
return self._logger.info(*args, **kwargs, stacklevel=2)
171+
172+
def warning(self, *args, **kwargs):
173+
"""Log a message with level WARNING."""
174+
return self._logger.warning(*args, **kwargs, stacklevel=2)
175+
176+
def warn(self, *args, **kwargs):
177+
"""Log a message with level WARNING."""
178+
return self._logger.warning(*args, **kwargs, stacklevel=2)
179+
180+
def error(self, *args, **kwargs):
181+
"""Log a message with level ERROR."""
182+
return self._logger.error(*args, **kwargs, stacklevel=2)
183+
184+
def critical(self, *args, **kwargs):
185+
"""Log a message with level CRITICAL."""
186+
return self._logger.critical(*args, **kwargs, stacklevel=2)
187+
188+
def fatal(self, *args, **kwargs):
189+
"""Log a message with level FATAL."""
190+
return self._logger.fatal(*args, **kwargs, stacklevel=2)
191+
192+
def log(self, level, msg, *args, **kwargs):
193+
"""Log a message with a specified level."""
194+
return self._logger.log(level, msg, *args, **kwargs, stacklevel=2)
195+
196+
197+
LOGGER = Logger()
198+
"""Global logger instance.
199+
200+
This is a global instance of the Logger class that can be used throughout the application.
201+
It is initialized with default settings and can be configured as needed.
202+
"""

0 commit comments

Comments
 (0)