Skip to main content

hotlog

Generalized logging utility for Python projects

Overview

hotlog provides a simple, flexible, and structured logging interface for Python applications and libraries. It wraps structlog and rich to offer:

  • Three-level verbosity system for controlling output detail
  • Rich colored output with YAML-formatted context
  • Live logging support for dynamic status updates (at default level)
  • Context-aware logging with prefix-based filtering (_verbose_, _debug_)
  • Unified API: from hotlog import get_logger, configure_logging
  • Works seamlessly in CLI tools, scripts, and libraries

Features

Three Verbosity Levels

  • Level 0 (default): Essential info only, supports live updates that disappear
  • Level 1 (-v): More context visible, includes _verbose_ prefixed keys
  • Level 2 (-vv): All debug info, includes _debug_ prefixed keys, no live updates

Why only three levels? Hotlog caps verbosity at level 2 because these three levels represent distinct user experience patterns that cover the vast majority of logging needs. Level 0 provides clean, interactive output with live updates. Level 1 adds useful context without overwhelming the user. Level 2 shows everything for debugging. Beyond level 2, you're typically showing internal implementation details that are better handled by traditional debug logging rather than a verbosity scale.

Prefix Filtering

Control which context appears at different verbosity levels by using key prefixes:

  • Regular keys (no prefix): Always shown at all levels
  • _verbose_key: Only shown at level 1 (-v) or 2 (-vv)
  • _debug_key: Only shown at level 2 (-vv)

The prefixes are automatically removed when displayed, so _verbose_source becomes source.

logger.info(
    "Processing dataset",
    records=1000,                    # Always shown
    _verbose_source="data.csv",      # Only at -v or -vv
    _debug_file_size="2.5MB"         # Only at -vv
)

No special configuration needed - just prefix your keys and hotlog handles the rest!

Visibility Gating with _display_level

Sometimes you want to hide entire events unless the user explicitly opts into higher verbosity. Add _display_level to the event context to specify the minimum verbosity (0, 1, or 2) required to display it:

logger.info("download_entry_start", entry=item.name, _display_level=1)
  • Messages log as normal when the active verbosity (set via configure_logging or environment detection) is at least _display_level.
  • When the required level is higher than the active verbosity, the event is skipped entirely. The key is removed before rendering, so it never appears in the formatted output.
  • _display_level complements _verbose_*/_debug_* prefixes—use it to gate whole events, while prefixes still gate individual context values.

Custom Log Matchers

Configure custom formatting rules for specific log patterns using matchers. Matchers check if a log entry matches certain conditions and apply custom formatting.

Built-in Matchers

ToolMatch: Format tool execution logs (toolbelt style)

from hotlog import configure_logging, get_logger, ToolMatch

# Configure with ToolMatch
configure_logging(
    verbosity=0,
    matchers=[
        ToolMatch(event="executing", prefix="tb")
    ]
)
logger = get_logger(__name__)

# Automatically formats as: tb[ruff-format] => ruff format .
logger.info(
    "executing",
    command="ruff format .",
    tool="ruff-format",
    _verbose_files_changed=14
)

The ToolMatch matcher checks for:

  • Event name: "executing" (configurable)
  • Log level: "INFO" (configurable)
  • Required key: command
  • Optional key: tool (shows in brackets)

Customize the matcher:

ToolMatch(
    event="executing",      # Event name to match
    prefix="pkg",          # Prefix shown before tool name
    level="INFO",          # Log level to match
    command_key="command", # Key containing command
    tool_key="tool"        # Key containing tool name
)

Creating Custom Matchers

Extend LogMatcher to create your own formatting rules:

from hotlog import LogMatcher, configure_logging, get_logger
from structlog.typing import EventDict
from typing import Optional

class InstallMatch(LogMatcher):
    """Custom matcher for package installation logs."""
    
    def matches(self, level: str, event: str, event_dict: EventDict) -> bool:
        """Check if this log entry matches our pattern."""
        return (
            level == "INFO" and
            event == "installed" and
            "package" in event_dict
        )
    
    def format(self, level: str, event: str, event_dict: EventDict) -> Optional[str]:
        """Format matching log entries.
        
        Note: Extract and remove keys you use from event_dict.
        Return None to fall back to default formatting.
        """
        package = event_dict.pop("package")
        version = event_dict.pop("version", "unknown")
        
        return f'[green]✓[/green] Installed [bold]{package}[/bold] [dim](version {version})[/dim]'

# Use your custom matcher
configure_logging(
    verbosity=0,
    matchers=[
        InstallMatch(),
        ToolMatch(event="executing", prefix="pkg"),
    ]
)
logger = get_logger(__name__)

# This will use InstallMatch formatting
logger.info("installed", package="requests", version="2.31.0")

# This will use ToolMatch formatting
logger.info("executing", command="uv pip install requests", tool="uv")

# This will use default formatting
logger.info("Build completed")

Key points about custom matchers:

  • Matchers are checked in order - first match wins
  • Use event_dict.pop() to remove keys you've formatted
  • Return None to fall back to default formatting
  • Remaining context keys are shown as YAML below the message
  • Rich markup is supported in formatted strings

Rich Output

  • Colored log levels (INFO=blue, WARNING=yellow, ERROR=red, DEBUG=magenta)
  • YAML-formatted context for easy reading
  • Syntax highlighting for structured data
  • Clean output without log level prefixes (like uv) - set show_levels=True for traditional [INFO] style

Highlighting Important Information

Emphasize specific values in your messages using Rich markup or the highlight() helper:

from hotlog import get_logger, highlight

logger = get_logger(__name__)

# Option 1: Direct Rich markup
logger.info("Installed [bold]5 packages[/bold] in [bold]3ms[/bold]")

# Option 2: Using highlight() helper
logger.info(highlight("Downloaded {} in {}", "14 files", "2.5s"))
# Renders as: "Downloaded [bold]14 files[/bold] in [bold]2.5s[/bold]"

This is perfect for level 0 summaries where you want to emphasize key metrics while keeping the output clean.

Live Logging

Use the live_logging context manager for operations with progress updates (level 0 only):

with live_logging("Downloading..."):
    # Your operation here
    logger.info("Connected", host="example.com")

Conditional Live Logging Helper

When you only want live updates at verbosity 0, use maybe_live_logging(message):

from hotlog import maybe_live_logging

with maybe_live_logging("Downloading repos...") as live:
        if live:
                live.info("Downloading", name=repo)
  • At verbosity 0, it delegates to live_logging() and yields a LiveLogger.
  • At verbosity 1 or 2, it yields None without printing anything, so you can skip live output and avoid extra branching in your code.

Usage

Basic Example

from hotlog import configure_logging, get_logger

# Configure logging (verbosity: 0, 1, or 2)
configure_logging(verbosity=0)
logger = get_logger(__name__)

# Log messages
logger.info("Starting process", task_id=123)
logger.info("Processing data", records=100, _verbose_source="db.sqlite")
logger.warning("Rate limit approaching", current=95, limit=100)

With Different Verbosity Levels

# Level 0: Only essential info
configure_logging(verbosity=0)
logger.info("Processing", items=10, _verbose_detail="xyz", _debug_internals="abc")
# Output: items=10 only

# Level 1: Include verbose context
configure_logging(verbosity=1)
logger.info("Processing", items=10, _verbose_detail="xyz", _debug_internals="abc")
# Output: items=10, detail="xyz"

# Level 2: Include all debug context
configure_logging(verbosity=2)
logger.info("Processing", items=10, _verbose_detail="xyz", _debug_internals="abc")
# Output: items=10, detail="xyz", internals="abc"

Live Logging Example

from hotlog import configure_logging, get_logger, live_logging
import time

configure_logging(verbosity=0)
logger = get_logger(__name__)

with live_logging("Downloading repository..."):
    time.sleep(1)
    logger.info("Connected to server")
    time.sleep(1)
    logger.info("Fetching metadata")
    
logger.info("Download completed")

CLI Integration

import argparse
from hotlog import configure_logging, get_logger

parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', action='count', default=0)
args = parser.parse_args()

# Map -v/-vv to verbosity levels
configure_logging(verbosity=min(args.verbose, 2))
logger = get_logger(__name__)

Typer Integration

Hotlog works seamlessly with Typer for modern CLI apps. Note: Typer is an optional dependency - install with pip install hotlog[typer].

import typer
from hotlog import verbosity_option, configure_logging, get_logger, resolve_verbosity

app = typer.Typer()

@app.command()
def my_command(verbose: int = verbosity_option):
    # Resolve verbosity from CLI args and environment (CI detection)
    verbosity = resolve_verbosity(verbose=verbose)
    configure_logging(verbosity=verbosity)
    logger = get_logger(__name__)
    
    logger.info("Starting process", task_id=123)
    logger.info("Processing data", records=100, _verbose_source="db.sqlite")

Key functions:

  • verbosity_option: Pre-configured Typer option for -v/--verbose flags
  • resolve_verbosity(verbose=count): Resolves final verbosity level from CLI and environment
  • All hotlog features work: live logging, prefixes, matchers, highlighting

Example with Typer:

# Default verbosity (level 0)
python my_app.py my-command

# Verbose (level 1) 
python my_app.py my-command -v

# Debug (level 2)
python my_app.py my-command -vv

See examples_typer/ directory for complete Typer examples that mirror the argparse versions.

Environment Variables

HOTLOG_FORCE_TERMINAL

Controls whether Rich's terminal formatting is forced, even in non-interactive environments.

Values:

  • 1, true, yes, on, y, t (case-insensitive): Force terminal mode
  • 0, false, no (or any other value): Disable forced terminal mode
  • Not set: Auto-detect (enabled in CI/normal use, disabled during tests)

When to use:

  • CI/CD pipelines: Some CI systems don't auto-detect as terminals. Set HOTLOG_FORCE_TERMINAL=1 to ensure colored output in CI logs.
  • Disable colors: Set HOTLOG_FORCE_TERMINAL=0 when redirecting to files or when colors interfere with log processing tools.
  • Debugging: Temporarily disable to see raw output without ANSI codes.
  • Library integration: Downstream users can control formatting without code changes.

Example:

# Force colors in CI
HOTLOG_FORCE_TERMINAL=1 python my_script.py

# Disable colors for file output
HOTLOG_FORCE_TERMINAL=0 python my_script.py > output.log

Note: During test execution (pytest), force_terminal is automatically disabled to ensure live logging works correctly with captured output.

Installation

# Core dependencies (required)
pip install hotlog

# With Typer support (optional)
pip install hotlog[typer]

Note: Typer is optional and only needed if you want to use the verbosity_option for Typer-based CLI apps. The core hotlog functionality works without Typer.

Why hotlog?

Hotlog standardizes logging across multiple projects and CLIs, making it easy to:

  • Provide consistent verbosity control
  • Display beautiful, readable logs
  • Handle live updates for better UX
  • Filter context based on user's needs
  • Customize log formatting with matchers

No more reinventing logging configuration for every project!

Examples

See the example_*.py files for more usage patterns:

  • example_quickstart.py - Comprehensive quick start guide
  • example_cli.py - Full CLI simulation
  • example_toolbelt.py - Tool execution logging with ToolMatch
  • example_custom_matcher.py - Creating custom matchers
  • example_highlight.py - Using highlight() and Rich markup
  • example_prefixes.py - Prefix filtering demonstration

Typer examples (equivalent functionality with Typer):

  • examples_typer/example_cli_typer.py - Full CLI simulation with Typer
  • examples_typer/example_prefixes_typer.py - Prefix filtering with Typer

Run examples with different verbosity levels:

python example_toolbelt.py      # Level 0 (default)
python example_toolbelt.py -v   # Level 1 (verbose)
python example_toolbelt.py -vv  # Level 2 (debug)

Metadata

Release files for hotlog 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hotlog 0.2.0
File Size Uploaded
hotlog-0.2.0.tar.gz 15.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hotlog 0.2.0
File Interpreter ABI Platform
hotlog-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 36.2 kB

Release files / hotlog-0.2.0.tar.gz

Download URL hotlog-0.2.0.tar.gz
Size 15.7 kB
Tags Source
SHA-256 checksum
How to use checksums
231c66e58277cfae0fc70a4d06042cbf4e56abb59c468403144f26685a302a67
BLAKE2b-256 checksum
How to use checksums
e1e47993b2ee1833c89b37b4b20ff8284183e0210ed797cc6eb43fc73999e452
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","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}

Release files / hotlog-0.2.0-py3-none-any.whl

Download URL hotlog-0.2.0-py3-none-any.whl
Size 20.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ed11b2d25ca51430eaea3447ee389ed8ebe448b2007f587749e3b3521a161929
BLAKE2b-256 checksum
How to use checksums
e38aa02dc3d7f5c0fd988db3b3806f19fb3d4f5db1cdda22e507343fbdf08f96
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","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}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page