Skip to main content

Spring-inspired config-driven logger for Python

Project description

logger

Language Python License: MIT

Config-driven, category-based structured logging for Python. Log levels are set in config under logging.level using a dot-hierarchy that mirrors Python's logging namespace. The library wraps Python's stdlib logging as the emit backend, so all existing stdlib handlers (file, rotating, socket, syslog) work without any extra configuration.

Port of @alt-javascript/logger to Python.

Install

uv add alt-python-logger        # or: pip install alt-python-logger

Requires Python 3.12+ and the config package (workspace dependency).

Quick Start

from logger import logger_factory

log = logger_factory.get_logger("com.example.MyService")

log.fatal("Service crashed")
log.error("Request failed", {"status": 500, "path": "/api/users"})
log.warn("Retry attempt 3")
log.info("Application started")
log.verbose("Processing record 42 of 1000")
log.debug("SQL: SELECT * FROM users WHERE id = ?")

Zero setup — logger_factory reads from the default config singleton, which discovers application.yaml (or .json, .properties, .env) from the current working directory.

Log Levels

From least to most verbose, with the internal severity integer used for is_*_enabled() comparisons:

Level Severity Python stdlib int Method
fatal 0 (most severe) 50 (CRITICAL) log.fatal(msg, meta=None)
error 1 40 (ERROR) log.error(msg, meta=None)
warn 2 30 (WARNING) log.warn(msg, meta=None)
info 3 20 (INFO) log.info(msg, meta=None)
verbose 4 15 (custom) log.verbose(msg, meta=None)
debug 5 (least severe) 10 (DEBUG) log.debug(msg, meta=None)

A logger set to level info enables fatal, error, warn, and info. It suppresses verbose and debug. See ADR-007 for why the ordering is preserved from the JS source.

Config-Driven Levels

Set levels in config under logging.level. The hierarchy uses nested dicts — each node corresponds to a dot segment of the category name.

# application.yaml
logging:
  level:
    /:           warn       # root level — applies to all loggers
    com:
      example:   debug      # com.example.* → debug
      noisy:
        handler: warn       # com.example.noisy.handler → warn
  format: text              # text or json (default: json)

The lookup walks the category's dot segments from left to right, applying the most-specific match found:

logger_factory.get_logger("com.example.MyService")   # → debug (from com.example)
logger_factory.get_logger("com.example.noisy.handler") # → warn (from com.example.noisy.handler)
logger_factory.get_logger("other.pkg.Handler")        # → warn (from root /)

Config key format: Level keys must be nested dicts — flat dotted keys like "com.example": debug are not recognised by the segment walker. See ADR-008.

Level Guards

Use guards before constructing expensive log arguments:

if log.is_debug_enabled():
    log.debug(f"Query plan: {explain_query(sql)}")
Method Returns True when...
is_fatal_enabled() level is fatal
is_error_enabled() level is fatal or error
is_warn_enabled() level is fatal, error, or warn
is_info_enabled() level is fatal through info
is_verbose_enabled() level is fatal through verbose
is_debug_enabled() any level (level is debug)

Log Formats

JSON (default)

{"level": "info", "message": "Started", "timestamp": "2026-01-15T12:00:00+00:00", "category": "com.example.MyService"}

Pass a plain dict as meta to merge fields into the JSON object:

log.info("Request complete", {"status": 200, "duration_ms": 42})
# => {"level":"info","message":"Request complete","timestamp":"...","category":"...","status":200,"duration_ms":42}

Pass any other value as meta to include it under a "meta" key:

log.error("Unexpected value", "some_string")
# => {"level":"error","message":"Unexpected value","timestamp":"...","category":"...","meta":"some_string"}

Plain text

2026-01-15T12:00:00+00:00:com.example.MyService:info:Application started

Set logging.format: text in config to enable plain text output.

API Reference

LoggerFactory

Main factory class. Creates ConfigurableLogger instances wired to a config source.

Constructor

LoggerFactory(config=None, cache=None, config_path=None)
Parameter Type Description
config config-like Config source. Default: module-level config singleton.
cache LoggerCategoryCache | None Level cache. Default: fresh per-instance cache (see ADR-009).
config_path str Root path for level lookup. Default: "logging.level".

factory.get_logger(category)

Returns a ConfigurableLogger for the given category.

category may be:

  • A string: "com.example.MyService"
  • A class instance with a qualifier attribute
  • A class instance (uses type(instance).__name__)
  • None (uses "ROOT")
log = factory.get_logger("com.example.MyService")
log = factory.get_logger(my_service_instance)

LoggerFactory.get_logger_static(category, config, config_path, cache)

Static convenience method equivalent to constructing a factory and calling get_logger().


ConfigurableLogger

A DelegatingLogger whose level is set from config at construction time.

ConfigurableLogger.get_logger_level(category, config_path, config, cache)

Static method. Walks the category's dot segments and returns the most-specific level found in config. Falls back to "info" if nothing is found.

The root level is read from {config_path}./ (e.g. logging.level./). Category segment levels are read from {config_path}.{segment} (e.g. logging.level.com, logging.level.com.example).


Logger

Base class. Stores the severity level and provides is_*_enabled() guards.

Logger(category=None, level=None)

Parameter Default Description
category "ROOT" Logger category name
level "info" Initial log level

logger.set_level(level)

Change the level at runtime. Accepts any key from LoggerLevel.ENUMS.


ConsoleLogger

Logger that emits via a stdlib logging.Logger. Extends Logger.

ConsoleLogger(category=None, level=None, formatter=None, stdlib_logger=None)
Parameter Description
category Logger category name
level Initial level
formatter JSONFormatter (default) or PlainTextFormatter
stdlib_logger stdlib logging.Logger instance, or CachingConsole for tests

DelegatingLogger

Wraps a provider logger and forwards all calls to it.

DelegatingLogger(provider)

Raises ValueError if provider is None.


MultiLogger

Fans out log calls to multiple child loggers.

MultiLogger(loggers=None, category=None, level=None)

set_level() propagates to all child loggers.

from logger import MultiLogger, ConsoleLogger

ml = MultiLogger([console_logger, file_logger], level="info")
ml.info("Written to both")

JSONFormatter

formatter.format(timestamp, category, level, message, meta=None)

Returns a JSON string. Dict meta is merged into the top-level object; other types are stored under "meta".


PlainTextFormatter

formatter.format(timestamp, category, level, message, meta=None)

Returns "{timestamp}:{category}:{level}:{message}{meta}".


LoggerCategoryCache

Simple dict cache for resolved level strings.

Method Description
get(key) Returns the cached level string or None.
put(key, level) Stores a level string.

LoggerLevel

Level constants and mappings.

from logger import LoggerLevel

LoggerLevel.FATAL    # "fatal"
LoggerLevel.ERROR    # "error"
LoggerLevel.WARN     # "warn"
LoggerLevel.INFO     # "info"
LoggerLevel.VERBOSE  # "verbose"
LoggerLevel.DEBUG    # "debug"

LoggerLevel.ENUMS    # {"fatal": 0, ..., "debug": 5}
LoggerLevel.STDLIB   # {"fatal": 50, ..., "debug": 10}

CachingConsole

In-memory log sink for test fixtures. Pass as stdlib_logger to ConsoleLogger.

from logger import ConsoleLogger, CachingConsole, PlainTextFormatter

sink = CachingConsole()
log  = ConsoleLogger(
    category="test",
    level="debug",
    formatter=PlainTextFormatter(),
    stdlib_logger=sink,
)

log.info("captured")
assert "captured" in sink.messages[0][1]

sink.clear()

sink.messages is a list of (level_int, formatted_string) tuples.


All Exports

from logger import (
    LoggerLevel,
    Logger,
    ConsoleLogger,
    DelegatingLogger,
    ConfigurableLogger,
    LoggerCategoryCache,
    LoggerFactory,
    JSONFormatter,
    PlainTextFormatter,
    CachingConsole,
    MultiLogger,
    logger_factory,   # module-level singleton
)

Testing

Use CachingConsole to capture log output in tests without writing to stdout:

from config import EphemeralConfig
from logger import (
    LoggerFactory, ConsoleLogger, CachingConsole, PlainTextFormatter
)

def test_logs_at_correct_level():
    cfg  = EphemeralConfig({"logging": {"level": {"/": "debug"}}})
    sink = CachingConsole()
    provider = ConsoleLogger(
        category="test",
        formatter=PlainTextFormatter(),
        stdlib_logger=sink,
    )
    from logger import ConfigurableLogger, LoggerCategoryCache
    log = ConfigurableLogger(
        config=cfg,
        provider=provider,
        category="test",
        cache=LoggerCategoryCache(),
    )

    log.info("hello")
    assert any("hello" in msg for _, msg in sink.messages)

Alternatively, create a LoggerFactory with an EphemeralConfig and call get_logger() — the factory wires CachingConsole is not needed for level assertions:

def test_level_from_config():
    cfg     = EphemeralConfig({"logging": {"level": {"/": "warn"}}})
    factory = LoggerFactory(config=cfg)
    log     = factory.get_logger("my.service")

    assert log.is_warn_enabled() is True
    assert log.is_info_enabled() is False

Troubleshooting

All log messages appear regardless of configured level Check that logging.level in your config file uses nested dicts, not flat dotted keys. {"com.example": "debug"} is not recognised — use {"com": {"example": "debug"}}. See ADR-008.

is_debug_enabled() returns True at info level This should not happen with the current implementation. If you observe this, check whether a stale LoggerCategoryCache from a previous factory is being passed explicitly. The default per-instance cache is always fresh.

Logger emits to the wrong stdlib handler ConsoleLogger creates a stdlib logger named after the category (logging.getLogger(category)). If your application calls logging.basicConfig() or attaches handlers to the root logger, those handlers will also receive these messages via propagation. Use logging.getLogger("com.example").propagate = False to suppress if needed.

logger_factory.get_logger() always returns info level The module-level logger_factory uses the module-level config singleton, which reads from the current working directory. If no application.yaml is present and PY_ACTIVE_PROFILES is not set, the level defaults to info (the fallback when no logging.level./ key is found). Create a config file or pass an explicit config to LoggerFactory.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

alt_python_logger-1.1.1.tar.gz (13.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

alt_python_logger-1.1.1-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

Details for the file alt_python_logger-1.1.1.tar.gz.

File metadata

  • Download URL: alt_python_logger-1.1.1.tar.gz
  • Upload date:
  • Size: 13.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for alt_python_logger-1.1.1.tar.gz
Algorithm Hash digest
SHA256 341dcb71a17e6b32059b54adfb285a95e0f08cf8555e453c4ec89c282862815e
MD5 326ea7e620116952985c8b90ddcc7673
BLAKE2b-256 5764bfbfda768683a467146b4cd56a9cb78cecd74c983905583f2ce931f7e81b

See more details on using hashes here.

File details

Details for the file alt_python_logger-1.1.1-py3-none-any.whl.

File metadata

  • Download URL: alt_python_logger-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 16.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for alt_python_logger-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a9d628ab22149b504cf5b995220c81adec776f8a3ca68a088d678d2f96cae3a2
MD5 834560ebadad50d4d3ef88780b38c7ae
BLAKE2b-256 d9e31de254367afa532269e59c38d0003dad844031bdad95c83c997e679ebe7c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page