Coverage for /Users/rik/github/cgse/libs/cgse-common/src/egse/log.py: 67%
71 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 11:57 +0200
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 11:57 +0200
1"""
2Configure local logging for the CGSE.
4I use 'local logging' here because the CGSE also has a Logger server which stores
5all CGSE log messages in a rotated file. That server is part of the `cgse-core` package.
7Environment variables that affect logging:
9 - LOG_FORMAT: full | FULL
10 - LOG_LEVEL: an integer [10, 50] or a level name DEBUG, INFO, WARNING, CRITICAL, ERROR
12"""
14__all__ = [
15 "LOG_FORMAT_FULL",
16 "LOG_FORMAT_CLEAN",
17 "LOG_FORMAT_STYLE",
18 "LOG_DATE_FORMAT_FULL",
19 "LOG_DATE_FORMAT_CLEAN",
20 "logger",
21 "root_logger",
22 "egse_logger",
23 "get_log_level_from_env",
24 "PackageFilter",
25]
27import logging
28import os
29import textwrap
30from pathlib import Path
32import rich
34# The format for the log messages.
35# The log record attributes are listed: https://docs.python.org/3.12/library/logging.html#logrecord-attributes
37LOG_FORMAT_STYLE = "{"
38LOG_FORMAT_FULL = (
39 "{asctime:19s}.{msecs:03.0f} : {processName:20s} : {levelname:8s} : {name:^25s} : {lineno:6d} : {filename:20s} : {"
40 "message}"
41)
42LOG_FORMAT_CLEAN = (
43 "{asctime} [{levelname:>8s}] {message} ({processName}[{process}]:{package_name}:{filename}:{lineno:d})"
44)
46LOG_DATE_FORMAT_FULL = "%Y-%m-%d %H:%M:%S"
47LOG_DATE_FORMAT_CLEAN = "%Y-%m-%d %H:%M:%S"
50class PackageFilter(logging.Filter):
51 """Adds 'package_name' to the log record.
53 When this filter is added to a handler of a logger, the formatter of that
54 logger can use the 'package_name' attribute.
56 When the package name can not be determined, is will contain 'n/a'.
58 NOTE: this filer assumes the root package is 'egse'.
59 """
61 def filter(self, record):
62 if hasattr(record, "pathname"): 62 ↛ 72line 62 didn't jump to line 72 because the condition on line 62 was always true
63 parts = Path(record.pathname).parent.parts
64 try:
65 egse_index = parts.index("egse")
66 package_name = ".".join(parts[egse_index:])
67 except ValueError:
68 package_name = "n/a"
70 record.package_name = package_name
71 else:
72 record.package_name = "n/a"
74 return True
77class EGSEFilter(logging.Filter):
78 def filter(self, record):
79 return record.name.startswith("egse")
82class NonEGSEFilter(logging.Filter):
83 def filter(self, record):
84 return not record.name.startswith("egse")
87def get_log_level_from_env(env_var: str = "LOG_LEVEL", default: int = logging.INFO):
88 """Read the log level from an environment variable."""
89 log_level_str = os.getenv(env_var, default)
91 # Try to convert to integer first (for numeric levels)
92 try:
93 log_level = int(log_level_str)
95 if 10 <= log_level <= 50: 95 ↛ 98line 95 didn't jump to line 98 because the condition on line 95 was always true
96 return log_level
97 else:
98 logging.warning(
99 f"Log level {log_level} outside standard range (10-50). Using {logging.getLevelName(default)}."
100 )
101 return default
103 except ValueError:
104 log_level_str = log_level_str.upper()
105 try:
106 return getattr(logging, log_level_str)
107 except AttributeError:
108 logging.error(f"Invalid LOG_LEVEL '{log_level_str}'. Using {logging.getLevelName(default)}.")
109 return default
112egse_logger = logging.getLogger("egse")
113egse_logger.level = get_log_level_from_env() # We might want to choose another env e.g. CGSE_LOG_LEVEL
115root_logger = logging.getLogger()
116root_logger.level = get_log_level_from_env()
118egse_handler = logging.StreamHandler()
119if os.getenv("LOG_FORMAT", "").lower() == "full": 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true
120 egse_formatter = logging.Formatter(fmt=LOG_FORMAT_FULL, datefmt=LOG_DATE_FORMAT_FULL, style=LOG_FORMAT_STYLE)
121else:
122 egse_formatter = logging.Formatter(fmt=LOG_FORMAT_CLEAN, datefmt=LOG_DATE_FORMAT_CLEAN, style=LOG_FORMAT_STYLE)
124egse_handler.setFormatter(egse_formatter)
125egse_handler.addFilter(EGSEFilter())
126egse_handler.addFilter(PackageFilter())
128root_logger.addHandler(egse_handler)
130for handler in root_logger.handlers:
131 if handler != egse_handler: # Don't filter our new handler
132 handler.addFilter(NonEGSEFilter())
133 handler.addFilter(PackageFilter())
135logger = egse_logger
137if __name__ == "__main__": 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true
138 root_logger = logging.getLogger()
140 rich.print(
141 textwrap.dedent(
142 """
143 Example logging statements
144 - logging level set to INFO
145 - fields are separated by a colon ':'
146 - fields: date & time: process name : level : logger name : lineno : filename : message
147 """
148 )
149 )
151 if os.getenv("LOG_FORMAT_FULL") == "true":
152 rich.print(
153 f"[b]{'Date & Time':^23s} : {'Process Name':20s} : {'Level':8s} : {'Logger Name':^25s} : {' Line '} : "
154 f"{'Filename':20s} : {'Message'}[/]"
155 )
156 else:
157 rich.print(f"[b]{'Date & Time':^19s} [ Level ] Message (filename:lineno)[/]")
159 rich.print("-" * 150)
160 for name, level in logging.getLevelNamesMapping().items():
161 logger.log(level, f"{name} logging message")
163 root_logger.info("This should come out of the root logger, not the egse logger.")