An extension of the standard logging package with indentation, severity coloring, default formatting and saving, trace level.
Project description
Indented Logger
An extension of the standard logging package, supplying:
- block indentation,
- severity coloring,
- default formatting and saving,
- dedicated level for control flow tracing.
Contents
1. Installation
1.1 From PyPI index
The most straightforward way:
$ pip install indelog
1.2 From GitHub sources
If you plan to play with the code and make improvements, use editable mode with dev dependencies:
$ git clone https://github.com/petergee302/indelog.git .
$ pip install -e .[dev]
Run static code analysis and automated tests before each push:
$ pyright
$ pytest -vs
2. Usage
2.1 Setup
The recommended way to initialize and configure this logger is to use the with ilog.Setup(...) clause which guarantees the entry/exit points to be executed:
import ilog
...
with ilog.Setup() as logger:
...
There are several options to this object that control e.g. the global level or output file:
global_level(strorintfromilog.LEVELS): Verbosity level to set for the logger. Can be either the name or the numerical value corresponding to the level. The default value oflogging.NOTSETdoes not change current threshold.output_path(strorPath): Where to save the log file: This can be either complete file path, or a directory. In the latter casemodule_fnameis required.module_fname: If given, its base name will be combined with given directory path and extension to build module-related log file. For instancetrain.pywill become<output_path>/train.lognamespace(str): If given, overrides the namespacecolorize(bool): Enable/disable colored console output (notice, if enabled the escape sequences also appear in the file)line_format(str): Message prefix to override the default format.
Example:
Create <module>-<timestamp>.log file besides the executing <module>.py file. All messages within the two trace calls will be indented at least once.
import ilog
...
if __name__ == '__main__':
with ilog.Setup(ilog.TRACE, __file__) as logger:
logger.trace(f'> main')
...
logger.trace(f'< main')
2.2 Indentation
The major shortcoming of the default logging module is the lack of block indentation mechanism. This is particularly painful when trying to analyze the control flow of a long, real-time, multi-turn, possibly multi-threaded or asynchronous code, like user interaction, or REST interface of a service. Debugging is basically excluded in these cases, and simple status printing does very poor job, if any.
Log indentation serves the same purpose as indentation of the code that generated it: To structure the flow into nested blocks that are much easier to understand. The difference is that while the code reflects the 'spatial' structure of an algorithm, the logs are its 'temporal' signature.
In Python one can use decorators to capture the entry and exit points of a function or method, nevertheless it is not recommended to use it as default means of logging due to (i) noticeable overhead, and (ii) lack of control over the selection and format of arguments and returned values.
In this module we use manual marking of the entry and exit points of a block. This has some pros and cons of course, but the need to control what and how to log is definitely more important than a desire to just dump everything automatically on a single press of a button.
There are two indentation markers:
>= begin indented block starting next line<= end of a block and unindent
Example:
def fun() -> int:
logger.debug('> fun()')
result = gun(2)//3
logger.debug('< fun(): %d', result)
return result
def gun(arg :int) -> int:
logger.debug('> gun(arg=%d)', arg)
result = 5*hun(arg + 1)
logger.debug('< gun(): %d', result)
return result
def hun(arg :int) -> int:
result = 13 + arg
logger.debug('hun(arg=%d): %d', arg, result)
return result
The above code would give output like this:
🔎 DEBUG 2025-11-26 14:13:23.731 78F45860B080 > fun()
🔎 DEBUG 2025-11-26 14:13:23.732 78F45860B080 > gun(arg=2)
🔎 DEBUG 2025-11-26 14:13:23.732 78F45860B080 hun(arg=3): 16
🔎 DEBUG 2025-11-26 14:13:23.732 78F45860B080 < gun(): 80
🔎 DEBUG 2025-11-26 14:13:23.732 78F45860B080 < fun(): 26
Notice that
(i) the indentation is used only in nested functions; there's no point in nesting inside hun() because it neither calls any other function that logs, nor performs any time-consuming work.
(ii) we use consistent convention to format the arguments and returned values. more on that in the last section.
2.3 Verbosity levels
Verbosity level is a severity threshold above which messages are passed to the handlers. There is one global level set by either:
ILOG_VERBOSITY_LEVEL = ...environment variable, orSetup(global_level = ...)argument during initialization
And there are local levels which can be set when fetching local loggers:
getLogger(local_level = ...)
The effective level within module is the maximum of the two, for instance:
local global effective
----- ------ ---------
DEBUG INFO INFO
ERROR INFO ERROR
💀 FATAL:
This level indicates a critical error that causes the application to abort or terminate. It is used for unrecoverable errors that require immediate attention, such as a system crash or an application shutdown.
💥 ERROR:
This level signifies a significant problem that prevents a specific operation from being completed. While the application may continue running, this level alerts teams to critical issues that need immediate investigation and resolution.
⚠️ WARNING:
This level is used for unexpected events that do not prevent the application from functioning but may indicate potential problems. It is often used to signal conditions that are close to causing errors, such as approaching resource limits.
💬 INFO:
This level is used for general operational messages that provide information about the normal flow of the application. It is suitable for tracking typical operations, such as successful logins or service startups.
🐾 TRACE:
This level is used for tracing the execution flow of the application, and is not part of the standard Python logger. It is typically employed during development or debugging sessions to monitor the sequence of function calls and execution paths. Trace logs should provide object types and unique identifiers of their instances to allow object-level resolution of information.
🔎 DEBUG:
This level provides the most detailed information useful for diagnosing problems. It is typically used during development or troubleshooting to trace application state, variable values, and business logic decisions. Since this type of logs may output frequently and in large volumes necessary to understand particular problem, it may have significant impact on the performance. Python does not have preprocessor like C/C++ which could automatically eliminate preparation of data for such calls, the only available remedies are to condition the logger.debug() call, or simply not to push such debug logs to the repository.
2.4 Namespace
The IndentedLogger derives from, but does not modify the default root logger. Hence, all its instances must have non-empty namespace i.e. a name prefix distinguishing from loggers created by other packages in use. There are two ways the namespace may be specified by:
ILOG_NAMESPACE = ...environment variable, or bySetup(namespace = ...)argument during initialization
2.5 Message prefix
By default all output messages are prefixed with additional information containing the event severity, its ISO date-time up to millisecond, and the thread ID of origin. Examples are given below:
💥 ERROR 2019-02-27 02:13:24.632 753DCED0A180 TrainingHistory.teardown(): UNEXPECTED_CALL
⚠️ WARNING 2020-12-01 12:58:34.763 7F46F17AA700 Job d50395084c9f4cfdb0c73190b740465d already removed
🐾 TRACE 2023-06-08 21:04:45.158 7036CCD1D080 DensityMatrix[7035BB4622D0](size=2)
🔎 DEBUG 2025-10-21 11:26:05.830 78F45860B080 n_samples=5674
The second part of the prefix can be modified (e.g. the thread ID removed, if not needed) by either:
ILOG_LINE_FORMATenvironment variable, or bySetup(line_format = ...)argument during initialization
2.6 Automated logging
As projects get shorter and shorter time-frames, hardly anyone has enough patience to go through the entire code weaving in such a lines. For those who seek error-resistance and uniformity despite lesser control and bigger overhead, there are set of decorators that make the logging automated. There are two kind of each:
| call | Log arguments and result after exit of a function/method |
| block | Log arguments before the entry of a function/method, and result after its exit |
They operate on function, method, and whole class level (the latter may be particularly useful if you just need to trace your code execution, but have no time for applying the logs):
| function-level |
@fatal_function_call(), @error_function_call(), @warning_function_call(), @info_function_call(), @trace_function_call(), @debug_function_call(), @fatal_function_block(), @error_function_block(), @warning_function_block(), @info_function_block(), @trace_function_block(), @debug_function_block(), |
| method-level |
@fatal_method_call(), @error_method_call(), @warning_method_call(), @info_method_call(), @trace_method_call(), @debug_method_call(), @fatal_method_block(), @error_method_block(), @warning_method_block(), @info_method_block(), @trace_method_block(), @debug_method_block(), |
| class-level |
@fatal_calls(), @error_calls(), @warning_calls(), @info_calls(), @trace_calls(), @debug_calls(), @fatal_blocks(), @error_blocks(), @warning_blocks(), @info_blocks(), @trace_blocks(), @debug_blocks(), |
The drawbacks of using automated logs is that
- there's no control over which arguments and return values are serialized, what sometimes may backfire resulting in a cluttered and less readable console output, instead of a clear trace.
- they do not capture any internal working of a function/method, nor signal any state or stage changes; they are primarily intended for tracing.
Example:
Instead of
def fun() -> int:
logger.debug('> fun()')
result = ...
logger.debug('< fun(): %d', result)
return result
One can equivalently write
@debug_function_block(logger)
def fun() -> int:
result = ...
return result
Instead of painstakingly applying logs to all methods of a class
class A:
def __init(self, ...):
logger.trace(...)
...
you can simply use class-level decorator
@trace_calls(logger)
class A:
def __init(self, ...):
...
2.7 Tracing recommendation
Standard Python logger is not designed with execution tracing in mind, thus there's no TRACE level, nor there's a convention on what and when to log. Herein we outline such proposal based on past experiences from over a couple of decades.
-
First of all, log the creation of objects (in C/C++ we'd also log destruction thereof).
-
Unless the object is a singleton, log its unique ID for the sake of distinguishing them in the trace, for example:
def __init__(self, arg1 :int, arg2 :bool): logger.trace(f'ClassName[{id(self):012X}]({arg1=}, {arg2=})')
-
Unless the class is final replace the explicit
ClassNameabove with{type(self).__name__}in order to make dynamic substitution:def __init__(self, arg1 :int, arg2 :bool): logger.trace(f'{type(self).__name__}[{id(self):012X}]({arg1=}, {arg2=})')
-
If the function or method is time-consuming, may crash, or causes other log messages to appear, use indentation with at least two messages marking both entry and exit points:
def method(self, arg3 :str) -> float: logger.trace(f'> {type(self).__name__}[{id(self):012X}].method({arg3=})') ... logger.trace(f'< {type(self).__name__}[{id(self):012X}].method(): {result=}') return result
Do not use indentation in functions or methods that are quick to complete and do not invoke other logging code.
-
The above logs are pretty standard, but long enough to make room for mistakes. In order to reduce their chances, bring the number of return statements to bare minimum, ideally to just one. That means instead of the usual code like:
if condition-one: logger.trace(f'< some_function(): {x}') return x ... if condition-two: logger.trace(f'< some_function(): {y}') return y ... logger.trace(f'< some_function(): {z}') return z
write
if condition-one: result = x else: ... if condition-two: result = y else: ... result = z logger.trace(f'< some_function(): {result}') return result
or use function/method/class decorators to automate logging.
-
In order to reduce chances for mistakes and inconsistency, prefer automated logging over manual. It is also the easier way to start with.
Use manual logging if the automated one is insufficient, e.g.: it formats and prints large values which are not particularly important, or prints too much or too little, inundates the stream or does not capture some important elements.
Copyright © 2018-2025 Peter Gee
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file indelog-1.0.0.tar.gz.
File metadata
- Download URL: indelog-1.0.0.tar.gz
- Upload date:
- Size: 23.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b59272c3bbdf203e2e7624341c1b728a46f20d41deac39e242cb7f0cdaef5b3
|
|
| MD5 |
cfdd4aa25e7a35ce5d57aeb8125a385a
|
|
| BLAKE2b-256 |
e3fdbaa068f8b428b25ddf54ace3f46950807a9ab2101ebebe758cb34908a6e9
|
File details
Details for the file indelog-1.0.0-py3-none-any.whl.
File metadata
- Download URL: indelog-1.0.0-py3-none-any.whl
- Upload date:
- Size: 17.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1851ee4b1dddd6d54e651e3245ec94aafe5c2d3d52fe47c55e0e8bacb6d59108
|
|
| MD5 |
110a61f897fde31920c4cc50986d4549
|
|
| BLAKE2b-256 |
35695055af27e17ed0de9391c5b47bd21c3552147e40e0c23f1ad1a55c56c510
|