Skip to content

Logging

Sayer provides a lightweight but powerful logging system that supports:

  • Lazy and safe logger initialization
  • Pluggable logging backends via LoggerProtocol
  • Configuration using Settings
  • Rich or standard terminal output
  • Thread-safe logger proxy

This document covers how Sayer handles logging internally and how you can customize it.


🧠 Core Concepts

Sayer uses a proxy-based design for logging:

  • You access sayer.logging.logger anywhere in your app
  • Internally, this is a LoggerProxy β€” a thread-safe wrapper
  • At runtime, the proxy is bound to a concrete logger (like StandardLoggingConfig) using bind_logger(...)
  • Logging is controlled through the settings.logging_level

πŸ”§ How to Use It

Basic Usage

from sayer.logging import logger

logger.info("This is an info message")
logger.warning("Watch out!")
logger.debug("Debugging details")

No setup required if your app uses Sayer() or sayer.core.client.run(), since logging is automatically configured.


πŸ›  Logging Setup Flow

Logging is initialized through:

from sayer.logging import setup_logging
setup_logging(settings)

You normally don’t need to call this manually β€” Sayer handles it on startup.


πŸ”§ Configuration via Settings

Logging behavior is configured using the active settings object.

Key fields:

Field Type Description
logging_level str One of: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
is_logging_setup bool Prevents double-initialization
logging_config property Returns a LoggingConfig instance

🎨 Output Styles

Sayer defaults to using Rich for colorful, formatted terminal logs.

Example:

[INFO] Starting app
[ERROR] Something went wrong

You can override the logging backend by setting a custom LoggingConfig:

from sayer.logging import logger
from sayer.core.logging import StandardLoggingConfig

settings.logging_config = StandardLoggingConfig(level="DEBUG")
logger.info("Using custom backend")

πŸ§ͺ Testing Logging

In test mode, you can mock or replace the logger entirely:

from sayer.logging import logger

logger.bind_logger(None)  # Disable

Or inject a test-specific logger that captures logs for assertions.


πŸ“¦ LoggingProtocol

All loggers must implement LoggerProtocol, which includes:

class LoggerProtocol(Protocol):
    def info(self, msg: str): ...
    def debug(self, msg: str): ...
    def warning(self, msg: str): ...
    def error(self, msg: str): ...
    def critical(self, msg: str): ...

You can build and bind your own structured logger like Loguru, structlog, etc.


🧰 Recap

Feature Supported
Proxy logger access βœ…
Rich logging output βœ…
Standard fallback βœ…
Pluggable config βœ…
Settings-integrated βœ…
Thread-safe access βœ…

Logging in Sayer is ready to use, safe by default, and fully extensible.