Skip to content

Logging

Logging configuration management for consistent log formatting across services.

Overview

The logging module provides utilities to configure Python's logging system using the bundled log-config.toml file. All Imbi services use the same logging configuration for consistency.

Basic Usage

from imbi_common import logging

# Configure logging with bundled config
logging.configure_logging()

# Enable DEBUG level for development
logging.configure_logging(dev=True)

# Use custom config
custom_config = {
    "version": 1,
    "formatters": {
        "simple": {
            "format": "%(levelname)s: %(message)s"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "simple"
        }
    },
    "root": {
        "level": "INFO",
        "handlers": ["console"]
    }
}
logging.configure_logging(log_config=custom_config)

Log Configuration

The bundled log-config.toml provides: - Structured log formatting with timestamps - Separate handlers for console and file output - Appropriate log levels for different components - Rotating file handlers with size limits

Development Mode

When dev=True is passed to configure_logging(), all loggers under the imbi namespace are automatically set to DEBUG level. This is useful for local development.

Access Log Middleware

imbi_common.access_log.AccessLogMiddleware is an ASGI middleware that replaces uvicorn's built-in access log. It emits one record per HTTP request on the imbi_common.access logger, in NCSA-style format with the authenticated principal in the authuser slot.

from fastapi import FastAPI
from imbi_common.access_log import AccessLogMiddleware

app = FastAPI()
app.add_middleware(
    AccessLogMiddleware,
    quiet_paths={'/status', '/health'},
)

Attaching per-request context

Handlers and downstream middleware can attach extra context to the access-log line by writing a mapping to request.state.imbi_common_access_log. Each entry is rendered as key:value in a space-separated list wrapped in parentheses after the status code:

from fastapi import Request

@app.post('/events')
async def record_event(request: Request, event: Event) -> None:
    request.state.imbi_common_access_log = {
        'event_type': event.kind,
        'selected': event.selected,
    }
    ...

Produces a log line like:

... "POST /events HTTP/1.1" 200 (event_type:whatever selected:False)

The suffix is omitted entirely when the mapping is missing or empty, so existing log lines are unchanged for requests that don't opt in. The imbi_common_access_log name is namespaced to avoid collisions with other middleware that shares request.state.

API Reference

get_log_config

get_log_config() -> _LoggingConfig

Load logging configuration from bundled log-config.toml.

Returns:

Name Type Description
dict _LoggingConfig

Logging configuration dictionary suitable for logging.dictConfig()

Source code in src/imbi_common/logging.py
def get_log_config() -> _LoggingConfig:
    """Load logging configuration from bundled log-config.toml.

    Returns:
        dict: Logging configuration dictionary suitable for
            logging.dictConfig()

    """
    log_config_file = resources.files('imbi_common') / 'log-config.toml'
    return typing.cast(
        _LoggingConfig,
        typing.cast(object, tomllib.loads(log_config_file.read_text())),
    )

configure_logging

configure_logging(
    log_config: _LoggingConfig | None = None,
    dev: bool = False,
) -> None

Configure logging using dictConfig.

Parameters:

Name Type Description Default
log_config _LoggingConfig | None

Optional logging config dict. If None, loads from log-config.toml

None
dev bool

If True, sets imbi logger to DEBUG level

False
Source code in src/imbi_common/logging.py
def configure_logging(
    log_config: _LoggingConfig | None = None, dev: bool = False
) -> None:
    """Configure logging using dictConfig.

    Args:
        log_config: Optional logging config dict. If None, loads from
            log-config.toml
        dev: If True, sets imbi logger to DEBUG level

    """
    if log_config is None:
        log_config = get_log_config()

    if dev:
        loggers = log_config.setdefault('loggers', {})
        loggers.setdefault('imbi', {})
        loggers['imbi']['level'] = 'DEBUG'

    config.dictConfig(log_config)  # type: ignore[arg-type]

AccessLogMiddleware

AccessLogMiddleware(
    app: ASGIApp,
    *,
    quiet_paths: Collection[str] = (),
    quiet_status_codes: Container[int] = range(200, 300),
    logger: Logger | None = None,
    include_principal: bool = True,
)

ASGI middleware that logs each HTTP request.

Parameters:

Name Type Description Default
app ASGIApp

The wrapped ASGI application.

required
quiet_paths Collection[str]

Request paths whose successful responses should not be logged. Matched exactly against scope['path'].

()
quiet_status_codes Container[int]

Status codes considered "successful" for the purpose of silencing quiet_paths. Defaults to range(200, 300) so 4xx/5xx on a quiet path is still logged (e.g. GET /status returning 404 because the endpoint isn't wired up).

range(200, 300)
logger Logger | None

Logger to emit records on. Defaults to imbi_common.access.

None
include_principal bool

When True (the default), inspect the Authorization header on each request and render the authenticated principal in the log line. Set to False to suppress the lookup for services that don't issue JWTs or API keys.

True
Source code in src/imbi_common/access_log.py
def __init__(
    self,
    app: ASGIApp,
    *,
    quiet_paths: abc.Collection[str] = (),
    quiet_status_codes: abc.Container[int] = range(200, 300),
    logger: logging.Logger | None = None,
    include_principal: bool = True,
) -> None:
    self.app = app
    self.quiet_paths = frozenset(quiet_paths)
    self.quiet_status_codes = quiet_status_codes
    self.logger = logger or LOGGER
    self.include_principal = include_principal