logger_loguru

Loguru configuration helpers for structured logging with indentation

Provides pre-configured logging setup using Loguru with notebook-friendly defaults and hierarchical indentation support for tracking nested operations.

Key features:
config_logger() — One-line setup returning configured logger + tree logger
tree_logger — Hierarchical logging with automatic indentation (llogger.push, llogger.pop)
LogFormatter — Customizable format with indentation support
LogLevelFilter — Dynamic level filtering with temporary override

When to use: Need readable, structured logging in notebooks or scripts with hierarchical context tracking (e.g., nested async operations, recursive algorithms).

Typical usage:

from loguru import logger
from pote.logger_loguru import config_logger

logger, llogger = config_logger(logger)
llogger.info("root")
with llogger.inc_indent():
    llogger.info("nested")

loguru basics

Quick exploration of loguru’s core features and rationale for this module’s configuration choices.

Default handler:
- logs are emitted to sys.stderr by default
- messages can be logged with different severity levels
- messages are formatted using curly braces (it uses str.format() under the hood)

logger.debug("Hello, world! {}", 'aaaa')

In notebooks, traces are not good:

@logger.catch
def f(x): 100 / x  # type: ignore

def g():
    f(10)
    f(0)

g()
  ERROR | An error has been caught in function 'g', process 'MainProcess' (43630), thread 'MainThread' (8320000192):

filter traceback in Jupyter when using @logger.catch

In notebooks contexts, configure your handler with backtrace option disabled:

logger.remove()
logger.add(sys.stderr, backtrace=False)
7
g()
2025-11-13 14:40:05.849 | ERROR    | __main__:g:6 - An error has been caught in function 'g', process 'MainProcess' (43630), thread 'MainThread' (8320000192):

Traceback (most recent call last):



  File "/var/folders/np/k2wj6f4s3rj0m9n0yt8pkk680000gn/T/ipykernel_43630/4019600944.py", line 6, in g

    f(0)

    <function f>



  File "/var/folders/np/k2wj6f4s3rj0m9n0yt8pkk680000gn/T/ipykernel_43630/4019600944.py", line 2, in f

    def f(x): 100 / x  # type: ignore

        │ │         └ 0

        │ └ 0

    <function f>



ZeroDivisionError: division by zero

Add some colors and formatting to the output:

logger.remove()
i = logger.add(sys.stderr, colorize=True, format="[<fg #66a3ff>{time:YYYY-MM-DD HH:mm:ss}</fg #66a3ff>] [<fg #00ff00>{level}</fg #00ff00>] {message}")
logger.info("test {}", i)
logger.debug("Hello, world!")
[2025-11-13 14:40:05] [INFO] test 8

[2025-11-13 14:40:05] [DEBUG] Hello, world!

formatter

Custom formatter that adds indentation tracking for hierarchical log output.


source

LogFormatter


def LogFormatter(
    
)->None:

Formats log records with indentation for tree-structured output

fmt = LogFormatter()
test_eq(fmt.indent, '')
test_eq(LogFormatter._ind_level, 0)

record = {"extra": {}, "name": "test", "function": "fn", "line": 10}
fmt.format(record)
test_eq(record["extra"]["indent"], '')
record
{'extra': {'indent': ''}, 'name': 'test', 'function': 'fn', 'line': 10}
LogFormatter._ind_level = 2
fmt2 = LogFormatter()
test_eq(fmt2.indent, '    ')  # 2 levels * 2 spaces
LogFormatter._ind_level = 0  # reset for other tests

source

LogLevelFilter


def LogLevelFilter(
    level:int | str
):

Filter log records by minimum level with context manager for temporary override

# ⎸
# LEFT VERTICAL BOX LINE
# Unicode: U+23B8, UTF-8: E2 8E B8

# ⏐
# VERTICAL LINE EXTENSION
# Unicode: U+23D0, UTF-8: E2 8F 90

# │
# BOX DRAWINGS LIGHT VERTICAL
# Unicode: U+2502, UTF-8: E2 94 82

loggers

Wrapper that manages indentation levels for hierarchical log output.


source

tree_logger


def tree_logger(
    logger:Any, fmt:LogFormatter
):

Logger wrapper managing hierarchical indentation for tree-structured output

setup

Configuration functions for quick logger setup in library modules


source

setup_logger


def setup_logger(
    logger, name:str='__main__'
)->Logger:

Apply standard config (format, level filter, backtrace) to logger

Note logging is disabled after setup_logger if called from a module distinct to ‘main’.

logger = setup_logger(logger)
logger.info('test')
   INFO | test

source

config_logger


def config_logger(
    logger:Logger, # Logger instance from each module
)->tuple[Logger, tree_logger]:

Configure logger with colors and return (logger, tree_logger) tuple

Module usage pattern:

Each module imports and configures its own logger:

from loguru import logger
from pote.logger_loguru import config_logger

logger, llogger = config_logger(logger)

# Standard logging
logger.info("Processing started")

# Hierarchical logging  
llogger.info("Main operation")
with llogger.inc_indent():
    llogger.info("Sub-operation")

This gives each module independent logger configuration while maintaining consistent formatting.

logger, llogger = config_logger(logger)
logger.info('configured')
   INFO | configured
test_eq(llogger.level, 0)
test_eq(llogger.indent, '')
llogger.info('root level')
   INFO | root level
_ = llogger.push
llogger.info("pushed once")
test_eq(llogger.level, 1)
test_eq(llogger.indent, '│ ')
   INFO | │ pushed once
_ = llogger.push
llogger.info("pushed twice")
test_eq(llogger.level, 2)
test_eq(llogger.indent, '│ │ ')
   INFO | │ │ pushed twice
_ = llogger.pop
test_eq(llogger.level, 1)
_ = llogger.reset()
test_eq(llogger.level, 0)
llogger.info('root')
with llogger.inc_indent():
    test_eq(llogger.level, 1)
    llogger.info('child')
    with llogger.inc_indent():
        test_eq(llogger.level, 2)
        llogger.info('grandchild')
    test_eq(llogger.level, 1)
    llogger.info('child')
test_eq(llogger.level, 0)
llogger.info('root')
   INFO | root
   INFO | │ child
   INFO | │ │ grandchild
   INFO | │ child
   INFO | root
class _Test:
    a = 'a'
    b = 'b'
    
    @llogger.bracket_logging("<n>{}</> <y>{}</>", 'test', b'b')
    async def test_bracket_logging_b(self):
        llogger.info("inside test_bracket_logging_b")
        test_eq(llogger.level, 2)  # nested 2 levels deep
        
    @llogger.bracket_logging("<n>{}</> <y>{}</>", 'test', b'a')
    async def test_bracket_logging_a(self):
        llogger.info("inside test_bracket_logging_a")
        test_eq(llogger.level, 1)  # nested 1 level
        await self.test_bracket_logging_b()  # type: ignore
        test_eq(llogger.level, 1)  # back to 1 after inner returns

_t = _Test()
llogger.reset()
test_eq(llogger.level, 0)
await _t.test_bracket_logging_a()  # type: ignore
test_eq(llogger.level, 0)  # back to root
   INFO | test a: >>>> test_bracket_logging_a...
   INFO | │ inside test_bracket_logging_a
   INFO | │ test b: >>>> test_bracket_logging_b...
   INFO | │ │ inside test_bracket_logging_b

bracket_logging decorator with nested calls