Skip to content

Commit f30dd92

Browse files
joamagclaude
andauthored
feat: add TRACE log level and patch_logging() (#20)
* feat: add TRACE log level and patch_logging() Port TRACE log level support from appier (hivesolutions/appier#84) to colony. Adds TRACE constant (logging.DEBUG - 5 = 5) and SILENT constant (logging.CRITICAL + 1 = 51) for fine-grained protocol-level debugging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add trace() method and *args/**kwargs to PluginManager log methods Adds trace() convenience method to PluginManager and makes all log methods (debug, info, warning, error, critical) accept *args and **kwargs for lazy evaluation support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add TRACE-aware logging format with pathname and lineno When the log level is set to TRACE, automatically switches to a more detailed format that includes file path and line number for fine-grained debugging of low-level operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: new module starting * fix: handle GLOBAL_CONFIG override for TRACE logging format The GLOBAL_CONFIG always has a hardcoded logging_format value, so the .get() default never triggers. Added explicit check to switch to the trace format when the config value matches the default and the level is TRACE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: new logging format for trace * chore: new black format * chore: new logging format for trace Passes stacklevel=2 in all PluginManager log methods (trace, debug, info, warning, error, critical) so that pathname and lineno in the format string reflect the actual caller, not the internal log method. Guards stacklevel for Python < 3.8 where it is not supported, and makes DEFAULT_LOGGING_FORMAT_TRACE conditional on Python 3.8+. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: trace reference * fix: small spelling error in logging_util.py docstring --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e1b34be commit f30dd92

11 files changed

Lines changed: 345 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
*
12+
* TRACE log level and `patch_logging()` for fine-grained protocol-level debugging
1313

1414
### Changed
1515

src/colony/__main__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Hive Colony Framework
5+
# Copyright (c) 2008-2024 Hive Solutions Lda.
6+
#
7+
# This file is part of Hive Colony Framework
8+
#
9+
# Hive Colony Framework is free software: you can redistribute it and/or modify
10+
# it under the terms of the Apache License as published by the Apache
11+
# Foundation, either version 2.0 of the License, or (at your option) any
12+
# later version.
13+
#
14+
# Hive Colony Framework is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# Apache License for more details.
18+
#
19+
# You should have received a copy of the Apache License along with
20+
# Hive Colony Framework If not, see <http://www.apache.org/licenses/>.
21+
22+
__copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
23+
""" The copyright for the module """
24+
25+
__license__ = "Apache License, Version 2.0"
26+
""" The license for the module """
27+
28+
from colony_start import main
29+
30+
main()

src/colony/base/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,14 @@
8989
DATE_TIME_FORMAT,
9090
INFORMATION_PATH,
9191
)
92-
from .loggers import BroadcastHandler, MemoryHandler, LogstashHandler
92+
from .loggers import (
93+
SILENT,
94+
TRACE,
95+
BroadcastHandler,
96+
MemoryHandler,
97+
LogstashHandler,
98+
patch_logging,
99+
)
93100
from .system import (
94101
System,
95102
Plugin,

src/colony/base/loggers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@
6464
""" The maximum amount of time in between flush
6565
operations in the logstash handler """
6666

67+
SILENT = logging.CRITICAL + 1
68+
""" The silent level used to silent all the logging
69+
or an handler, this is used as an utility for debugging
70+
purposes more that a real feature for production systems """
71+
72+
TRACE = logging.DEBUG - 5
73+
""" The trace level used for extremely detailed and verbose
74+
logging of protocol-level operations, this is meant to be
75+
used for fine-grained debugging of low-level operations
76+
like raw byte transfers and frame parsing """
77+
6778
LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
6879
""" The sequence of levels from the least sever to the
6980
most sever this sequence may be used to find all the
@@ -386,3 +397,20 @@ def _build_api(self):
386397
return None
387398

388399
return logstash.API()
400+
401+
402+
def patch_logging():
403+
if hasattr(logging, "_colony_patched"):
404+
return
405+
406+
# patches the logging infra-structure adding the trace level
407+
# support and the corresponding trace method to the logger
408+
logging.addLevelName(TRACE, "TRACE")
409+
logging.Logger.trace = _trace
410+
411+
logging._colony_patched = True
412+
413+
414+
def _trace(self, message, *args, **kwargs):
415+
if self.isEnabledFor(TRACE):
416+
self._log(TRACE, message, args, **kwargs)

src/colony/base/system.py

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@
8080
DEFAULT_LOGGING_FORMAT = "%(asctime)s [%(levelname)s] %(message)s"
8181
""" The default logging format """
8282

83+
DEFAULT_LOGGING_FORMAT_TRACE = (
84+
"%(asctime)s [%(levelname)s] %(pathname)s:%(lineno)d | %(message)s"
85+
if sys.version_info >= (3, 8)
86+
else "%(asctime)s [%(levelname)s] %(message)s"
87+
)
88+
""" The format to be used when the logging level is set to TRACE,
89+
includes file path and line number on Python 3.8+ where stacklevel
90+
is supported for accurate caller information """
91+
8392
DEFAULT_LOGGING_FILE_NAME_PREFIX = "colony"
8493
""" The default logging file name prefix """
8594

@@ -236,6 +245,11 @@ def get_manager(self):
236245
raise exceptions.PluginSystemException("no plugin available")
237246
return self.plugin.manager
238247

248+
def trace(self, *args, **kwargs):
249+
if self.plugin == None:
250+
raise exceptions.PluginSystemException("no plugin available")
251+
return self.plugin.trace(*args, **kwargs)
252+
239253
def debug(self, *args, **kwargs):
240254
if self.plugin == None:
241255
raise exceptions.PluginSystemException("no plugin available")
@@ -1380,6 +1394,19 @@ def log_stack_trace(self, level=logging.DEBUG):
13801394
formatted_traceback_line_stripped = formatted_traceback_line.rstrip()
13811395
self.logger.log(level, formatted_traceback_line_stripped)
13821396

1397+
def trace(self, message, *args, **kwargs):
1398+
"""
1399+
Adds the given trace message to the logger.
1400+
1401+
:type message: String
1402+
:param message: The trace message to be added to the logger.
1403+
"""
1404+
1405+
# formats the logger message then prints the
1406+
# trace message to the current stream
1407+
logger_message = self.format_logger_message(message)
1408+
self.logger.trace(logger_message, *args, **kwargs)
1409+
13831410
def debug(self, message, *args, **kwargs):
13841411
"""
13851412
Adds the given debug message to the logger.
@@ -2223,6 +2250,11 @@ def start_logger(self, log_level=DEFAULT_LOGGING_LEVEL):
22232250
:param log_level: The log level of the logger.
22242251
"""
22252252

2253+
# patches the logging infra-structure so that the TRACE level
2254+
# is properly registered and available for usage, this call
2255+
# is idempotent and safe to be called multiple times
2256+
loggers.patch_logging()
2257+
22262258
# retrieves the minimal log level between the current
22272259
# log level and the default one (as specified)
22282260
minimal_log_level = (
@@ -2312,8 +2344,16 @@ def start_logger(self, log_level=DEFAULT_LOGGING_LEVEL):
23122344
logstash_handler.setLevel(minimal_log_level)
23132345

23142346
# retrieves the logging format and uses it
2315-
# to create the proper logging formatter
2316-
logging_format = GLOBAL_CONFIG.get("logging_format", DEFAULT_LOGGING_FORMAT)
2347+
# to create the proper logging formatter, in case the
2348+
# log level is set to trace uses the trace format that
2349+
# includes the file path and line number for debugging
2350+
is_trace = log_level <= loggers.TRACE
2351+
default_format = (
2352+
DEFAULT_LOGGING_FORMAT_TRACE if is_trace else DEFAULT_LOGGING_FORMAT
2353+
)
2354+
logging_format = GLOBAL_CONFIG.get("logging_format", default_format)
2355+
if logging_format == DEFAULT_LOGGING_FORMAT and is_trace:
2356+
logging_format = DEFAULT_LOGGING_FORMAT_TRACE
23172357
formatter = logging.Formatter(logging_format)
23182358

23192359
# sets the formatter in the stream and rotating
@@ -6004,7 +6044,27 @@ def log_stack_trace(self, level=logging.DEBUG):
60046044
# prints a log message with the formatted traceback line
60056045
self.logger.log(level, formatted_traceback_line_stripped)
60066046

6007-
def debug(self, message):
6047+
def trace(self, message, *args, **kwargs):
6048+
"""
6049+
Adds the given trace message to the logger.
6050+
6051+
:type message: String
6052+
:param message: The trace message to be added to the logger.
6053+
"""
6054+
6055+
# in case no logger is defined it's not possible
6056+
# to print the message as a trace
6057+
if not self.logger:
6058+
return
6059+
6060+
# formats the logger message and prints it
6061+
# as a trace message into the logger
6062+
logger_message = self.format_logger_message(message)
6063+
if sys.version_info >= (3, 8):
6064+
kwargs.setdefault("stacklevel", 2)
6065+
self.logger.log(loggers.TRACE, logger_message, *args, **kwargs)
6066+
6067+
def debug(self, message, *args, **kwargs):
60086068
"""
60096069
Adds the given debug message to the logger.
60106070
@@ -6020,9 +6080,11 @@ def debug(self, message):
60206080
# formats the logger message and prints it
60216081
# as a debug message into the logger
60226082
logger_message = self.format_logger_message(message)
6023-
self.logger.debug(logger_message)
6083+
if sys.version_info >= (3, 8):
6084+
kwargs.setdefault("stacklevel", 2)
6085+
self.logger.debug(logger_message, *args, **kwargs)
60246086

6025-
def info(self, message):
6087+
def info(self, message, *args, **kwargs):
60266088
"""
60276089
Adds the given info message to the logger.
60286090
@@ -6038,9 +6100,11 @@ def info(self, message):
60386100
# formats the logger message and prints it
60396101
# as an info message into the logger
60406102
logger_message = self.format_logger_message(message)
6041-
self.logger.info(logger_message)
6103+
if sys.version_info >= (3, 8):
6104+
kwargs.setdefault("stacklevel", 2)
6105+
self.logger.info(logger_message, *args, **kwargs)
60426106

6043-
def warning(self, message):
6107+
def warning(self, message, *args, **kwargs):
60446108
"""
60456109
Adds the given warning message to the logger.
60466110
@@ -6056,12 +6120,14 @@ def warning(self, message):
60566120
# formats the logger message and prints it
60576121
# as a warning message into the logger
60586122
logger_message = self.format_logger_message(message)
6059-
self.logger.warning(logger_message)
6123+
if sys.version_info >= (3, 8):
6124+
kwargs.setdefault("stacklevel", 2)
6125+
self.logger.warning(logger_message, *args, **kwargs)
60606126

60616127
# logs the stack trace
60626128
self.log_stack_trace(level=logging.INFO)
60636129

6064-
def error(self, message):
6130+
def error(self, message, *args, **kwargs):
60656131
"""
60666132
Adds the given error message to the logger.
60676133
@@ -6077,12 +6143,14 @@ def error(self, message):
60776143
# formats the logger message and prints it
60786144
# as an error message into the logger
60796145
logger_message = self.format_logger_message(message)
6080-
self.logger.error(logger_message)
6146+
if sys.version_info >= (3, 8):
6147+
kwargs.setdefault("stacklevel", 2)
6148+
self.logger.error(logger_message, *args, **kwargs)
60816149

60826150
# logs the stack trace
60836151
self.log_stack_trace(level=logging.WARNING)
60846152

6085-
def critical(self, message):
6153+
def critical(self, message, *args, **kwargs):
60866154
"""
60876155
Adds the given critical message to the logger.
60886156
@@ -6094,7 +6162,9 @@ def critical(self, message):
60946162
logger_message = self.format_logger_message(message)
60956163

60966164
# prints the critical message
6097-
self.logger.critical(logger_message)
6165+
if sys.version_info >= (3, 8):
6166+
kwargs.setdefault("stacklevel", 2)
6167+
self.logger.critical(logger_message, *args, **kwargs)
60986168

60996169
# logs the stack trace
61006170
self.log_stack_trace(level=logging.ERROR)
@@ -6570,6 +6640,23 @@ def get_uptime(self):
65706640
# about the uptime for the current plugin system
65716641
return uptime
65726642

6643+
def is_trace(self):
6644+
"""
6645+
Checks if the current logging level is set to trace,
6646+
this check may be used to action conditional code
6647+
execution for fine-grained debugging purposes.
6648+
6649+
:rtype: bool
6650+
:return: Value indicating if the current logging level
6651+
is set to trace (for fine-grained debugging).
6652+
"""
6653+
6654+
if not self.logger:
6655+
return False
6656+
if not self.logger.level:
6657+
return False
6658+
return self.logger.level <= loggers.TRACE
6659+
65736660
def is_development(self):
65746661
"""
65756662
Checks if the current run mode in execution is of type

src/colony/base/system.pyi

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class System:
4646

4747
def __init__(self, plugin) -> None: ...
4848
def get_manager(self): ...
49+
def trace(self, *args, **kwargs): ...
4950
def debug(self, *args, **kwargs): ...
5051
def info(self, *args, **kwargs): ...
5152
def warning(self, *args, **kwargs): ...
@@ -138,11 +139,12 @@ class Plugin:
138139
def get_author_name(self): ...
139140
def get_uptime(self): ...
140141
def log_stack_trace(self, level=...) -> None: ...
141-
def debug(self, message) -> None: ...
142-
def info(self, message) -> None: ...
143-
def warning(self, message) -> None: ...
144-
def error(self, message) -> None: ...
145-
def critical(self, message) -> None: ...
142+
def trace(self, message, *args, **kwargs) -> None: ...
143+
def debug(self, message, *args, **kwargs) -> None: ...
144+
def info(self, message, *args, **kwargs) -> None: ...
145+
def warning(self, message, *args, **kwargs) -> None: ...
146+
def error(self, message, *args, **kwargs) -> None: ...
147+
def critical(self, message, *args, **kwargs) -> None: ...
146148
def format_logger_message(self, message): ...
147149
def _get_capabilities_allowed_names(self): ...
148150

@@ -388,11 +390,12 @@ class PluginManager:
388390
def generate_system_information_map(self) -> None: ...
389391
def get_log_handler(self, name): ...
390392
def log_stack_trace(self, level=...) -> None: ...
391-
def debug(self, message) -> None: ...
392-
def info(self, message) -> None: ...
393-
def warning(self, message) -> None: ...
394-
def error(self, message) -> None: ...
395-
def critical(self, message) -> None: ...
393+
def trace(self, message, *args, **kwargs) -> None: ...
394+
def debug(self, message, *args, **kwargs) -> None: ...
395+
def info(self, message, *args, **kwargs) -> None: ...
396+
def warning(self, message, *args, **kwargs) -> None: ...
397+
def error(self, message, *args, **kwargs) -> None: ...
398+
def critical(self, message, *args, **kwargs) -> None: ...
396399
def format_logger_message(self, message): ...
397400
def print_all_plugins(self) -> None: ...
398401
def get_prefix_paths(self): ...
@@ -428,6 +431,7 @@ class PluginManager:
428431
def get_environment(self): ...
429432
def get_system_information_map(self): ...
430433
def get_uptime(self): ...
434+
def is_trace(self): ...
431435
def is_development(self): ...
432436
def is_production(self): ...
433437
def echo(self, value: str = "echo"): ...

src/colony/libs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@
111111
from .lazy_util import LazyClass, LazyIteratorClass, is_lazy, Lazy, LazyIterator
112112
from .list_util import list_intersect, list_extend, list_no_duplicates
113113
from .logging_util import (
114+
SILENT,
115+
TRACE,
114116
getLogger,
115117
getLevelName,
116118
getLevelInt,

0 commit comments

Comments
 (0)