Logging Patterns

Common patterns for structured logging with stogger. For log level details, decorator API, and output key reference, see Reference.

User Output with _replace_msg

Use log.info() instead of print() or typer.echo() for line-oriented user output. The event name provides structure; _replace_msg provides the human-readable sentence:

log.info("package-installed",
         _replace_msg="Successfully installed {package_name} v{version} ({size_mb:.1f} MB)",
         package_name="hello",
         version="2.10.0",
         size_mb=15.7)

Single source of truth for user communication and diagnostics — structured data for analysis, formatted text for humans, audit trail of user interactions.

Exception Handling

Use log.exception() in except blocks. It is equivalent to log.error() with exc_info=True and automatically includes the full traceback. Do not pass error=str(e) — the exception context is already captured.

try:
    process_package(package_data)
except ValidationError as e:
    log.exception("package-validation-failed",
                  package_name=package_data.get("name"))
    raise

Use log.error with exc_info=True only when you have a specific reason not to use log.exception(). Never use exc_info on info/debug levels.

Correlation IDs

Track requests across services with correlation IDs:

import uuid

correlation_id = str(uuid.uuid4())

log.info("request-started",
         correlation_id=correlation_id,
         endpoint="/api/orders",
         method="POST")

# ... processing ...

log.info("request-completed",
         correlation_id=correlation_id,
         status_code=201,
         processing_time_ms=150)

Common Patterns

Function Tracing

def process_package(package_name: str):
    log.info("package-processing-started", package_name=package_name)

    try:
        result = perform_processing(package_name)
        log.info("package-processing-completed",
                package_name=package_name,
                result_size=len(result))
        return result
    except Exception as e:
        log.exception("package-processing-failed", package_name=package_name)
        raise

Or use the @log_call decorator instead.

Timing Operations

import time

def timed_operation():
    start = time.time()
    log.info("operation-started", operation="data_sync")

    result = perform_operation()

    duration = time.time() - start
    log.info("operation-completed",
            operation="data_sync",
            duration_ms=round(duration * 1000))

    return result

Or use the @log_result decorator instead.

CLI Command Logging

def deploy_command(package_name: str, environment: str):
    log.info("deployment-started",
             _replace_msg="Deploying {package} to {env}...",
             package=package_name,
             env=environment)

    try:
        result = deploy_package(package_name, environment)
        log.info("deployment-completed",
                 _replace_msg="Successfully deployed {package} to {env}",
                 package=package_name,
                 env=environment,
                 deployment_id=result.id)
    except DeploymentError as e:
        log.exception("deployment-failed",
                      _replace_msg="Failed to deploy {package}: {error}",
                      package=package_name,
                      error=str(e))
        raise

Progress Reporting

def process_files(file_list: list[str]):
    total = len(file_list)
    log.info("batch-processing-started",
             _replace_msg="Processing {total} files...",
             total=total)

    for i, file_path in enumerate(file_list, 1):
        log.debug("file-processing-started", file_path=file_path, index=i)
        process_file(file_path)

        if i % 10 == 0 or i == total:
            log.info("batch-progress-update",
                     _replace_msg="Processed {current}/{total} files",
                     current=i,
                     total=total,
                     progress_percent=round(i/total*100))

    log.info("batch-processing-completed",
             _replace_msg="All {total} files processed successfully",
             total=total)

Error Context

def validate_config(config_path: Path):
    log.debug("config-validation-started", config_path=str(config_path))

    if not config_path.exists():
        log.error("config-file-missing",
                  _replace_msg="Configuration file not found: {path}",
                  config_path=str(config_path))
        raise FileNotFoundError(f"Config file missing: {config_path}")

    try:
        with open(config_path) as f:
            config = yaml.safe_load(f)
    except yaml.YAMLError as e:
        log.exception("config-parse-failed",
                      _replace_msg="Invalid YAML in config file: {error}",
                      config_path=str(config_path),
                      error=str(e))
        raise

    log.info("config-validation-completed",
             _replace_msg="Configuration validated successfully",
             config_path=str(config_path),
             config_keys=list(config.keys()))

    return config

Security

Never log passwords, tokens, or raw PII. Structure log calls to exclude sensitive fields from the outset:

log.info("user-authenticated", user_id=123, email_domain="example.com")

Performance

Log batch start/end, not every item:

log.info("batch-processing-started", item_count=len(large_list))
# ... process items ...
log.info("batch-processing-completed", processed_count=processed)

For high-volume events, sample instead of logging every occurrence:

import random

if random.random() < 0.1:  # 10% sampling
    log.debug("high-frequency-event", event_data=data)

Avoid chatty info logs in tight loops. Use debug, sample, or aggregate instead.

Monitoring-Friendly Logging

Design logs for easy parsing by monitoring systems:

log.info("api-response",
         endpoint="/api/users",
         method="GET",
         status_code=200,
         response_time_ms=45,
         cache_hit=True)