Repository navigation
reference logger
lclang.logger provides LoggerHandlerConfig, resolve_logger_config,
use_logger_handler, and use_logger. A process owns one asynchronous handler scope. Producer threads
enqueue records; one writer formats and writes console and named file sinks.
Nested/concurrent scopes are rejected. Logger wrappers select the current
process scope, and reject use outside a scope. Initialize after worker creation.
Python objects and mappings share the level, format, console, file,
takeover_loggers, and capture_warnings fields. No configuration files are
read by this package. Applications load .lclcfg files with lclang.config,
then call await resolve_logger_config(frame, verbose=False) to materialize
logger.* from their ordinary Frame. CLI/Workflow use the same resolver after
their -c/-o layering, before entering the handler scope.
file is a mapping of sink names. file.default supplies missing leaf fields
and never creates a sink. Explicit sink fields always take precedence over
the template, including False and empty collections. Thus setting
logger.file.default.enabled: False disables only sinks without an explicit
enabled: True. Override one sink directly to disable that exception:
-o logger.file.audit.enabled "LCL[False]". A disabled sink does no file I/O.
Files have independent enabled, directory, filename, level, encoding,
flush_interval, logger_names, and rotation settings. Rotation supports
none, size, time, and size_or_time; intervals use positive integer
s/m/h/d units. Aligned rotation supports UTC 1h and 1d. Source names
match themselves and dot-separated descendants. Empty source lists match all.
The console defaults to INFO on stderr. Files default to INFO, UTF-8 with
backslashreplace, one-second flushing, and no rotation. An enabled file needs
a directory. Missing names become lclang.<sink>.{pid}.log. Templates support
{pid} and {process}. Global level defaults to NOTSET. Warnings capture is
opt-in. Verbose CLI execution lowers enabled output thresholds to DEBUG,
without enabling disabled sinks or removing source filters.
Disabled outputs retain their configured thresholds. Invalid fields include
their complete logger path; CLI diagnostics also identify the defining source
and position when available, or the owning configuration layer otherwise.
All of the following can be imported directly from lclang.logger:
| Type | Role |
|---|---|
Logger |
Wrapper returned by await use_logger(...); use it to annotate application logging arguments. |
LoggerHandlerConfig |
Immutable declaration accepted by use_logger_handler. |
LoggerRuntime |
Runtime yielded by the handler scope; read metrics for diagnostics. |
ConsoleConfig, FileConfig, RotationConfig
|
Resolved output policies returned by resolved_console() and resolved_files(). |
RuntimeMetrics, SinkMetrics
|
Detached aggregate and per-sink diagnostic snapshots. |
Obtain loggers and runtimes through the scope APIs. Resolved policy types describe
validated results; declare console, file and rotation settings with mappings in
LoggerHandlerConfig.
resolve_logger_config evaluates only the logger subtree and its dependencies,
using the supplied Frame's hierarchy, overrides and cached snapshots. Missing
fields use the ordinary handler defaults; an absent logger namespace returns
LoggerHandlerConfig() settings. If present, logger must be a scoped
FrameProxy, not a scalar or a nested Python mapping. Invalid namespace or
field types raise TypeError; invalid values and failing leaf expressions raise
ValueError. Field validation includes source metadata when available, and
leaf evaluation errors include the logger path and retain their cause.
Resolution does not enter a handler scope, initialize sinks, modify bindings,
or close the Frame. Application-supplied callables evaluated by LCL retain their
normal effects. verbose=True lowers the global and enabled output thresholds
to DEBUG while preserving lower thresholds, disabled outputs and source filters.
No CLI invocation variables are injected; supply application inputs explicitly.
import asyncio
from pathlib import Path
from tempfile import TemporaryDirectory
from lclang.config import load_config
from lclang.logger import Logger, resolve_logger_config, use_logger, use_logger_handler
async def main() -> None:
with TemporaryDirectory() as directory:
path = Path(directory) / "service.lclcfg"
path.write_text('''
__LCL_VERSION__: 1
logger.console.enabled: False
logger.file.default.directory: log_directory
logger.file.service.filename: f"{service}.log"
logger.file.service.level: "INFO"
''', encoding="utf-8")
loaded = await load_config(path)
async with loaded.frame_factory().create(
values={"service": "catalog", "log_directory": directory}
) as frame:
config = await resolve_logger_config(frame)
assert config.resolved_files()["service"].filename == "catalog.log"
async with use_logger_handler(config) as runtime:
logger: Logger = await use_logger("catalog")
logger.info("service ready")
metrics = runtime.metrics
assert metrics.records_written == 1
segment = metrics.sinks["file.service"].path
assert segment is not None
assert "service ready" in segment.read_text(encoding="utf-8")
asyncio.run(main())The application owns service and log_directory; the file derives the sink's
filename and inherits its directory from file.default. Resolving the Frame
produces the same immutable configuration used by CLI. The handler scope drains
the queued record before the assertions read its permanent segment, then the
Frame and temporary directory close.
use_logger(name=None, prefix="", emit_level=0) selects a stdlib logger;
None selects root. Prefixes are formatter-only and appear on the first line.
emit_level skips additional wrapper callers through stdlib stacklevel.
Messages, arguments and exception formatting run in the writer. Queue records
are shallow copies: do not mutate referenced arguments after logging.
The root and explicitly listed takeover loggers are restored on exit. Existing handlers are detached, never closed or reformatted. Default takeover names are uvicorn, uvicorn.error and uvicorn.access. Other logger-specific levels and handlers retain stdlib semantics. Streams are borrowed and never closed.
Each file is exclusively created at its permanent path, with UTC microseconds and a process-wide increasing sequence appended to its base name. The first line identifies that path. Rollover creates the next segment first, then ends the old segment with its successor path and closes it. Closed segments are never changed, renamed, compressed, or deleted. Header bytes count toward size; the successor footer may exceed the limit. Oversized records are never split. Empty segments still rotate on time. Size rotation preserves time deadlines.
Scope entry fails if an enabled output cannot initialize. Normal, exceptional, and cancelled exits stop admission, drain, flush, close, asynchronously join the writer, and restore logging state. There is no drain timeout. Runtime sink failures are counted and reported minimally to sys.stderr; other sinks continue. Failed files recover into new segments without replaying possibly partial writes. Hardware failures may leave gaps. Forced process termination and permanently blocked I/O cannot guarantee delivery.
runtime.metrics supplies immutable aggregate and per-sink snapshots with
records_enqueued, records_written, writer_errors, rollover_count and paths.
Aggregate records_written counts a record once if any sink accepted its write;
metadata is excluded. Successful writes do not imply fsync durability.
Initialize in the actual worker, before Uvicorn emits its startup messages.
Wrapping only FastAPI's lifespan starts too late and closes too early for the
server's own logging. Request tasks and background tasks can call use_logger
directly because admission is process-wide. Do not nest handler scopes in routes
or in a Workflow invoked by the service.
On Windows or Linux, save this as service.py and run python service.py.
FastAPI and Uvicorn are dependencies of this example application, not lclang.
The serve boundary can also surround an existing configured Server instance.
import asyncio
import uvicorn
from fastapi import FastAPI
from lclang.logger import LoggerHandlerConfig, use_logger, use_logger_handler
app = FastAPI()
@app.get("/")
async def index():
logger = await use_logger(name=__name__, prefix="[HTTP]")
logger.info("request received")
return {"ok": True}
async def serve(server, config, *, sockets=None):
async with use_logger_handler(config):
await server.serve(sockets=sockets)
if __name__ == "__main__":
server = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=8000, log_config=None))
config = LoggerHandlerConfig(file={"service": {"directory": "./logs"}})
asyncio.run(serve(server, config))log_config=None lets the handler scope own logging installation. Scope exit
waits for queued server and application records, including the final server
shutdown message. Configure process spawning outside this entry point when
multiple independent Windows workers are required; each child calls serve.
For Linux Gunicorn, save the following as logging_worker.py alongside
service.py, install the application's gunicorn and uvicorn-worker
dependencies, and run:
gunicorn service:app --workers 4 --worker-class logging_worker.LoggingWorker --access-logfile -import os
import signal
from uvicorn_worker import UvicornWorker
from lclang.logger import LoggerHandlerConfig, use_logger_handler
class LoggingWorker(UvicornWorker):
def init_signals(self):
super().init_signals()
signal.signal(signal.SIGTERM, self.handle_exit)
async def _serve(self):
config = LoggerHandlerConfig(
file={"service": {"directory": os.environ.get("LOGGER_DIRECTORY", "./logs")}}
)
async with use_logger_handler(config):
await super()._serve()The subclass enters the scope inside the worker's asynchronous service method,
after fork and worker initialization. The master never starts a logger writer;
Gunicorn's own handlers remain its responsibility. LOGGER_DIRECTORY optionally
selects the file directory. Every worker gets independent permanent paths.
Keep Gunicorn's graceful SIGTERM handler installed before entering Uvicorn.
Uvicorn restores the previous handler and replays SIGTERM after server shutdown;
the worker handler marks it no longer alive and returns so the outer logging
scope can drain and close. Leaving SIGTERM at its default action terminates the
process before that cleanup and can leave buffered log files with only a header.
The adapter depends on uvicorn-worker's _serve hook; integration tests target
uvicorn-worker==0.4.0 and uvicorn==0.52.4. Recheck this hook when upgrading
the server adapter (worker source,
Uvicorn server).
The quality suite runs actual loopback HTTP, lifespan, request, background-task, and shutdown checks with FastAPI/Uvicorn. Its Linux job additionally starts and terminates Gunicorn using the exact worker example above. Server packages appear only in the development integration dependency group.
Run python -m scripts.logger_benchmark --records 1000 --threads 1 10 100 in a
development checkout. It reports call p50/p95/p99 and producer records/second
for console, file and combined output. Console targets the null device to avoid
terminal-rendering variability. Startup, thread creation, and queue drain are
outside the producer measurement. This measures admission capacity, not sustained
disk throughput; an unbounded queue can grow when producers outpace the writer.
The checked-in measurement sample records
the host and Python version and is not a portable performance guarantee.
Published from ac045a6487d6. Documentation is maintained and tested in the source repository.