Skip to content

Commit b463aae

Browse files
committed
Add logging documentation and integrate Python logging with IRIS; implement LogManager and update logging methods in _Common class
1 parent b50159c commit b463aae

File tree

5 files changed

+223
-26
lines changed

5 files changed

+223
-26
lines changed

docs/logging.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Logging
2+
3+
InterSystems IRIS Interoperability framework implements its own logging system. The Python API provides a way to use Python's logging module integrated with IRIS logging.
4+
5+
## Basic Usage
6+
7+
The logging system is available through the component base class. You can access it via the `logger` property or use the convenience methods:
8+
9+
```python
10+
def on_init(self):
11+
# Using convenience methods
12+
self.log_info("Component initialized")
13+
self.log_error("An error occurred")
14+
self.log_warning("Warning message")
15+
self.log_alert("Critical alert")
16+
self.trace("Debug trace message")
17+
18+
# Using logger property
19+
self.logger.info("Info via logger")
20+
self.logger.error("Error via logger")
21+
```
22+
23+
## Console Logging
24+
25+
You can direct logs to the console instead of IRIS in two ways:
26+
27+
1. Set the component-wide setting:
28+
```python
29+
def on_init(self):
30+
self.log_to_console = True
31+
self.log_info("This will go to console")
32+
```
33+
34+
2. Per-message console logging:
35+
```python
36+
def on_message(self, request):
37+
# Log specific message to console
38+
self.log_info("Debug info", to_console=True)
39+
40+
# Other logs still go to IRIS
41+
self.log_info("Production info")
42+
```
43+
44+
## Log Levels
45+
46+
The following log levels are available:
47+
48+
- `trace()` - Debug level logging (maps to IRIS LogTrace)
49+
- `log_info()` - Information messages (maps to IRIS LogInfo)
50+
- `log_warning()` - Warning messages (maps to IRIS LogWarning)
51+
- `log_error()` - Error messages (maps to IRIS LogError)
52+
- `log_alert()` - Critical/Alert messages (maps to IRIS LogAlert)
53+
- `log_assert()` - Assert messages (maps to IRIS LogAssert)
54+
55+
## Integration with IRIS
56+
57+
The Python logging is automatically mapped to the appropriate IRIS logging methods:
58+
59+
- Python `DEBUG` → IRIS `LogTrace`
60+
- Python `INFO` → IRIS `LogInfo`
61+
- Python `WARNING` → IRIS `LogWarning`
62+
- Python `ERROR` → IRIS `LogError`
63+
- Python `CRITICAL` → IRIS `LogAlert`
64+
65+
## Legacy Methods
66+
67+
The following methods are deprecated but maintained for backwards compatibility:
68+
69+
- `LOGINFO()` - Use `log_info()` instead
70+
- `LOGALERT()` - Use `log_alert()` instead
71+
- `LOGWARNING()` - Use `log_warning()` instead
72+
- `LOGERROR()` - Use `log_error()` instead
73+
- `LOGASSERT()` - Use `log_assert()` instead

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ nav:
1313
- Command Line Interface: command-line.md
1414
- Python API: python-api.md
1515
- DTL Support: dtl.md
16+
- Logging: logging.md
1617
- Reference:
1718
- Examples: example.md
1819
- Useful Links: useful-links.md

src/iop/_common.py

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import iris
55
import traceback
66
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type
7+
from iop._log_manager import LogManager
8+
import logging
79

810
class _Common(metaclass=abc.ABCMeta):
911
"""Base class that defines common methods for all component types.
@@ -15,6 +17,7 @@ class _Common(metaclass=abc.ABCMeta):
1517
INFO_URL: ClassVar[str]
1618
ICON_URL: ClassVar[str]
1719
iris_handle: Any = None
20+
log_to_console: bool = False
1821

1922
def on_init(self) -> None:
2023
"""Initialize component when started.
@@ -245,56 +248,67 @@ def _log(self) -> Tuple[str, Optional[str]]:
245248
current_class = self.__class__.__name__
246249
current_method = None
247250
try:
248-
frame = traceback.extract_stack()[-3]
251+
frame = traceback.extract_stack()[-4]
249252
current_method = frame.name
250253
except:
251254
pass
252255
return current_class, current_method
253-
254-
def trace(self, message: str) -> None:
256+
257+
@property
258+
def logger(self) -> logging.Logger:
259+
"""Get a logger instance for this component.
260+
261+
Returns:
262+
Logger configured for IRIS integration
263+
"""
264+
class_name, method_name = self._log()
265+
return LogManager.get_logger(class_name, method_name, self.log_to_console)
266+
267+
def trace(self, message: str, to_console: Optional[bool] = None) -> None:
255268
"""Write trace log entry.
256269
257270
Args:
258271
message: Message to log
272+
to_console: If True, log to console instead of IRIS
259273
"""
260-
current_class, current_method = self._log()
261-
iris.cls("Ens.Util.Log").LogTrace(current_class, current_method, message,1)
274+
self.logger.debug(message, extra={'to_console': to_console})
275+
262276

263-
def log_info(self, message: str) -> None:
277+
def log_info(self, message: str, to_console: Optional[bool] = None) -> None:
264278
"""Write info log entry.
265279
266280
Args:
267281
message: Message to log
282+
to_console: If True, log to console instead of IRIS
268283
"""
269-
current_class, current_method = self._log()
270-
iris.cls("Ens.Util.Log").LogInfo(current_class, current_method, message)
284+
self.logger.info(message, extra={'to_console': to_console})
271285

272-
def log_alert(self, message: str) -> None:
273-
"""Write a log entry of type "alert". Log entries can be viewed in the management portal.
286+
def log_alert(self, message: str, to_console: Optional[bool] = None) -> None:
287+
"""Write alert log entry.
274288
275-
Parameters:
276-
message: a string that is written to the log.
289+
Args:
290+
message: Message to log
291+
to_console: If True, log to console instead of IRIS
277292
"""
278-
current_class, current_method = self._log()
279-
iris.cls("Ens.Util.Log").LogAlert(current_class, current_method, message)
293+
self.logger.critical(message, extra={'to_console': to_console})
280294

281-
def log_warning(self, message: str) -> None:
282-
"""Write a log entry of type "warning". Log entries can be viewed in the management portal.
295+
def log_warning(self, message: str, to_console: Optional[bool] = None) -> None:
296+
"""Write warning log entry.
283297
284-
Parameters:
285-
message: a string that is written to the log.
298+
Args:
299+
message: Message to log
300+
to_console: If True, log to console instead of IRIS
286301
"""
287-
current_class, current_method = self._log()
288-
iris.cls("Ens.Util.Log").LogWarning(current_class, current_method, message)
302+
self.logger.warning(message, extra={'to_console': to_console})
289303

290-
def log_error(self, message: str) -> None:
291-
"""Write a log entry of type "error". Log entries can be viewed in the management portal.
304+
def log_error(self, message: str, to_console: Optional[bool] = None) -> None:
305+
"""Write error log entry.
292306
293-
Parameters:
294-
message: a string that is written to the log.
307+
Args:
308+
message: Message to log
309+
to_console: If True, log to console instead of IRIS
295310
"""
296-
current_class, current_method = self._log()
297-
iris.cls("Ens.Util.Log").LogError(current_class, current_method, message)
311+
self.logger.error(message, extra={'to_console': to_console})
298312

299313
def log_assert(self, message: str) -> None:
300314
"""Write a log entry of type "assert". Log entries can be viewed in the management portal.

src/iop/_log_manager.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import iris
2+
import logging
3+
from typing import Optional, Tuple
4+
5+
class LogManager:
6+
"""Manages logging integration between Python's logging module and IRIS."""
7+
8+
@staticmethod
9+
def get_logger(class_name: str, method_name: Optional[str] = None, console: bool = False) -> logging.Logger:
10+
"""Get a logger instance configured for IRIS integration.
11+
12+
Args:
13+
class_name: Name of the class logging the message
14+
method_name: Optional name of the method logging the message
15+
console: If True, log to the console instead of IRIS
16+
17+
Returns:
18+
Logger instance configured for IRIS integration
19+
"""
20+
logger = logging.getLogger(f"{class_name}.{method_name}" if method_name else class_name)
21+
22+
# Only add handler if none exists
23+
if not logger.handlers:
24+
handler = IRISLogHandler(class_name, method_name, console)
25+
formatter = logging.Formatter('%(message)s')
26+
handler.setFormatter(formatter)
27+
logger.addHandler(handler)
28+
# Set the log level to the lowest level to ensure all messages are sent to IRIS
29+
logger.setLevel(logging.DEBUG)
30+
31+
return logger
32+
33+
class IRISLogHandler(logging.Handler):
34+
"""Custom logging handler that routes Python logs to IRIS logging system."""
35+
36+
def __init__(self, class_name: str, method_name: Optional[str] = None, console: bool = False):
37+
"""Initialize the handler with context information.
38+
39+
Args:
40+
class_name: Name of the class logging the message
41+
method_name: Optional name of the method logging the message
42+
console: If True, log to the console instead of IRIS
43+
"""
44+
super().__init__()
45+
self.class_name = class_name
46+
self.method_name = method_name
47+
self.console = console
48+
49+
def format(self, record: logging.LogRecord) -> str:
50+
"""Format the log record into a string.
51+
52+
Args:
53+
record: The logging record to format
54+
55+
Returns:
56+
Formatted log message
57+
"""
58+
if self.console:
59+
return f"{record}"
60+
return record.getMessage()
61+
62+
def emit(self, record: logging.LogRecord) -> None:
63+
"""Route the log record to appropriate IRIS logging method.
64+
65+
Args:
66+
record: The logging record to emit
67+
"""
68+
# Map Python logging levels to IRIS logging methods
69+
level_map = {
70+
logging.DEBUG: iris.cls("Ens.Util.Log").LogTrace,
71+
logging.INFO: iris.cls("Ens.Util.Log").LogInfo,
72+
logging.WARNING: iris.cls("Ens.Util.Log").LogWarning,
73+
logging.ERROR: iris.cls("Ens.Util.Log").LogError,
74+
logging.CRITICAL: iris.cls("Ens.Util.Log").LogAlert,
75+
}
76+
77+
log_func = level_map.get(record.levelno, iris.cls("Ens.Util.Log").LogInfo)
78+
if self.console or (hasattr(record, "to_console") and record.to_console):
79+
iris.cls("%SYS.System").WriteToConsoleLog(self.format(record),0,0,"IoP.Log")
80+
else:
81+
log_func(self.class_name, self.method_name, self.format(record))

src/tests/test_iop_commun.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ def test_is_iris_object_instance():
3838
result = _Common._is_iris_object_instance(msg)
3939
assert result == False
4040

41+
def test_log_info_to_console():
42+
commun = _Common()
43+
commun.log_to_console = True
44+
# generate a random string of 10 characters
45+
import random, string
46+
letters = string.ascii_lowercase
47+
random_string = ''.join(random.choice(letters) for i in range(10))
48+
commun.log_info(random_string)
49+
# check $IRISINSTALLDIR/mgr/messages.log last line
50+
with open(os.path.join(os.environ['IRISINSTALLDIR'], 'mgr', 'messages.log'), 'r') as file:
51+
lines = file.readlines()
52+
last_line = lines[-1]
53+
assert random_string in last_line
54+
55+
def test_log_info_to_console_from_method():
56+
commun = _Common()
57+
# generate a random string of 10 characters
58+
import random, string
59+
letters = string.ascii_lowercase
60+
random_string = ''.join(random.choice(letters) for i in range(10))
61+
commun.trace(message=random_string, to_console=True)
62+
# check $IRISINSTALLDIR/mgr/messages.log last line
63+
with open(os.path.join(os.environ['IRISINSTALLDIR'], 'mgr', 'messages.log'), 'r') as file:
64+
lines = file.readlines()
65+
last_line = lines[-1]
66+
assert random_string in last_line
67+
68+
4169
def test_log_info():
4270
commun = _Common()
4371
# generate a random string of 10 characters

0 commit comments

Comments
 (0)