Spring-inspired config-driven logger for Python
Project description
logger
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
qualifierattribute - 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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
341dcb71a17e6b32059b54adfb285a95e0f08cf8555e453c4ec89c282862815e
|
|
| MD5 |
326ea7e620116952985c8b90ddcc7673
|
|
| BLAKE2b-256 |
5764bfbfda768683a467146b4cd56a9cb78cecd74c983905583f2ce931f7e81b
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9d628ab22149b504cf5b995220c81adec776f8a3ca68a088d678d2f96cae3a2
|
|
| MD5 |
834560ebadad50d4d3ef88780b38c7ae
|
|
| BLAKE2b-256 |
d9e31de254367afa532269e59c38d0003dad844031bdad95c83c997e679ebe7c
|