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 logger        # or: pip install 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.0.1.tar.gz (13.6 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.0.1-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: alt_python_logger-1.0.1.tar.gz
  • Upload date:
  • Size: 13.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","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.0.1.tar.gz
Algorithm Hash digest
SHA256 db2949e10a2654845533266dac8885b5633738f8e0ee4f563c28b070768d5937
MD5 50c3633e7da6b4ca60844713a25bba8a
BLAKE2b-256 514e2d07eb88b220223949c5c0bbc8c747f571de15764abc5d64d12ae82b7029

See more details on using hashes here.

File details

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

File metadata

  • Download URL: alt_python_logger-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 16.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","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.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ac3256ffe1acde21078ec7d6bb9475eba35c5644c04310c2b39281fa39ae1ebe
MD5 4580e9c9b3ab79f93e62f02a425f446b
BLAKE2b-256 f023cc96798a8a811eea28a429d181d1675b43d91d1e1ed7dbdaa34218f593ce

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