Skip to content

Callbacks & Hooks

Logly provides several callback mechanisms for customizing log processing. Use these to handle exceptions, filter records dynamically, transform messages, and control output formatting.

catch(onerror=...) - Exception Callback

The onerror callback is invoked when catch() captures an exception.

Basic Usage

python
from logly import logger


def handle_error(exc: BaseException) -> None:
    print(f"Caught: {type(exc).__name__}: {exc}")


with logger.catch(onerror=handle_error):
    raise ValueError("something went wrong")

With Reraise

python
from logly import logger


def notify_on_error(exc: BaseException) -> None:
    print(f"Notifying admin about: {exc}")


try:
    with logger.catch(onerror=notify_on_error, reraise=True):
        raise RuntimeError("critical failure")
except RuntimeError:
    print("Handled upstream")

As Decorator

python
from logly import logger


def log_failure(exc: BaseException) -> None:
    logger.error("Function failed: {}", exc)


@logger.catch(onerror=log_failure)
def risky_operation():
    raise ConnectionError("timeout")

add(filter=...) - Callable Filter

The filter callable receives the record dict and returns True to allow or False to reject.

Signature

python
def my_filter(record: dict[str, object]) -> bool:
    # Access record fields
    level = record.get("level", "")
    message = record.get("message", "")
    extra = record.get("extra", {})

    # Return True to allow, False to reject
    return True

Filter by Level

python
from logly import logger


def only_errors(record: dict[str, object]) -> bool:
    return record.get("level") in {"ERROR", "CRITICAL", "FAIL"}


logger.add("errors.log", filter=only_errors)

Filter by Message Content

python
from logly import logger


def important_messages(record: dict[str, object]) -> bool:
    msg = str(record.get("message", "")).lower()
    return "critical" in msg or "urgent" in msg


logger.add("important.log", filter=important_messages)

Filter by Extra Fields

python
from logly import logger


def production_only(record: dict[str, object]) -> bool:
    extra = record.get("extra", {})
    return extra.get("env") == "production"


logger.add("prod.log", filter=production_only)

Block All Records

python
from logly import logger


def block_all(record: dict[str, object]) -> bool:
    return False


logger.add("disabled.log", filter=block_all)

add(patch=...) - Patcher Callback

The patcher callable receives the record dict and mutates it in-place before dispatch.

Signature

python
def my_patcher(record: dict[str, object]) -> None:
    # Modify record in-place
    record["extra"]["custom_field"] = "value"

Add Extra Fields

python
from logly import logger


def add_service_name(record: dict[str, object]) -> None:
    record.setdefault("extra", {})["service"] = "my-api"


patched = logger.patch(add_service_name)
patched.info("Request processed")  # extra includes service=my-api

Modify Messages

python
from logly import logger


def uppercase_message(record: dict[str, object]) -> None:
    record["message"] = str(record.get("message", "")).upper()


patched = logger.patch(uppercase_message)
patched.info("hello")  # Output: HELLO

Multiple Patchers

python
from logly import logger


def add_env(record: dict[str, object]) -> None:
    record.setdefault("extra", {})["env"] = "prod"


def add_region(record: dict[str, object]) -> None:
    record.setdefault("extra", {})["region"] = "us-east-1"


patched = logger.patch(add_env).patch(add_region)
patched.info("Deployed")  # extra includes env=prod, region=us-east-1

Per-Sink Patcher

python
from logly import logger


def json_enricher(record: dict[str, object]) -> None:
    record.setdefault("extra", {})["source"] = "logly"


sink_id = logger.add(
    "structured.json",
    format="{message}",
    serialize=True,
    patch=json_enricher,
)

add(format=...) - Callable Formatter

The format callable receives the record dict and returns the formatted string.

Signature

python
def my_format(record: dict[str, object]) -> str:
    return f"{record['level']}: {record['message']}"

Simple Custom Format

python
from logly import logger


def minimal_format(record: dict[str, object]) -> str:
    return f"[{record['level']}] {record['message']}"


logger.add("minimal.log", format=minimal_format)
logger.info("Hello")  # Output: [INFO] Hello

Include Extra Fields

python
from logly import logger


def json_format(record: dict[str, object]) -> str:
    import json

    extra = record.get("extra", {})
    return json.dumps(
        {
            "level": record.get("level"),
            "message": record.get("message"),
            "extra": extra,
        }
    )


logger.add("json.log", format=json_format)

Conditional Formatting

python
from logly import logger


def smart_format(record: dict[str, object]) -> str:
    level = record.get("level", "INFO")
    msg = record.get("message", "")
    if level in {"ERROR", "CRITICAL"}:
        return f"!!! {level}: {msg} !!!"
    return f"{level}: {msg}"


logger.add("smart.log", format=smart_format)

opt(exception=...) - Exception Formatting

Attach an exception to a log record for formatted traceback output.

Capture Current Exception

python
from logly import logger

try:
    1 / 0
except ZeroDivisionError:
    logger.opt(exception=True).error("Division failed")
    # Includes full traceback in output

Attach Specific Exception

python
from logly import logger

exc = ValueError("invalid input")
logger.opt(exception=exc).error("Processing failed")

With Backtrace Control

python
from logly import logger

try:
    risky_operation()
except Exception as e:
    # Full backtrace (default)
    logger.opt(exception=e, backtrace=True).error("Failed")

    # Short traceback
    logger.opt(exception=e, backtrace=False).error("Failed")

With Diagnostics

python
from logly import logger

try:
    complex_operation()
except Exception as e:
    # Include diagnostic information (variable values, etc.)
    logger.opt(exception=e, diagnose=True).error("Diagnostic failure")

opt(lazy=...) - Lazy Evaluation

Defer string formatting and callable evaluation until the message is actually emitted.

Skip Evaluation When Filtered

python
from logly import logger

call_count = 0


def expensive_computation() -> str:
    global call_count
    call_count += 1
    return "result: " + str(sum(range(1_000_000)))


messages = []


def capture(msg: str) -> None:
    messages.append(msg)


sink_id = logger.add(capture, level="WARNING")

# This will NOT call expensive_computation because level is filtered
logger.opt(lazy=True).info(expensive_computation)

print(call_count)  # 0 - function was never called
logger.remove(sink_id)

Evaluate When Emitted

python
from logly import logger

messages = []


def capture(msg: str) -> None:
    messages.append(msg)


def lazy_value() -> str:
    return "computed value"


sink_id = logger.add(capture)
logger.opt(lazy=True).info("Value: {}", lazy_value)
logger.remove(sink_id)

print(messages[0])  # "Value: computed value"

Lazy with Callables in Arguments

python
from logly import logger


def heavy_db_query() -> str:
    # Simulate expensive query
    return "query_result"


logger.opt(lazy=True).debug("Query result: {}", heavy_db_query)
# heavy_db_query() only evaluated if DEBUG level is active

Combining Callbacks

python
from logly import logger


def enrich_record(record: dict[str, object]) -> None:
    record.setdefault("extra", {})["source"] = "api"


def only_api_errors(record: dict[str, object]) -> bool:
    level = record.get("level", "")
    extra = record.get("extra", {})
    return level in {"ERROR", "CRITICAL"} and extra.get("source") == "api"


def api_format(record: dict[str, object]) -> str:
    return f"[API] {record.get('level')}: {record.get('message')}"


sink_id = logger.add(
    "api-errors.log",
    filter=only_api_errors,
    patch=enrich_record,
    format=api_format,
)

patched = logger.patch(enrich_record)
patched.error("Request failed")  # Goes to api-errors.log with custom format

Complete Example

python
from logly import logger


# Custom patcher to add context
def add_request_context(record: dict[str, object]) -> None:
    extra = record.setdefault("extra", {})
    extra.setdefault("service", "web-api")
    extra.setdefault("env", "production")


# Custom filter for production errors
def production_errors(record: dict[str, object]) -> bool:
    level = record.get("level", "")
    extra = record.get("extra", {})
    return level in {"ERROR", "CRITICAL"} and extra.get("env") == "production"


# Custom formatter for structured output
def structured_format(record: dict[str, object]) -> str:
    import json

    return json.dumps(
        {
            "ts": str(record.get("time", "")),
            "level": record.get("level"),
            "msg": record.get("message"),
            "extra": record.get("extra", {}),
        }
    )


# Configure sinks with callbacks
logger.add(
    "app.log",
    level="INFO",
    rotation="daily",
    retention="30 days",
    patch=add_request_context,
)

logger.add(
    "production-errors.json",
    level="DEBUG",
    filter=production_errors,
    format=structured_format,
    serialize=False,
    patch=add_request_context,
)


# Exception handling with onerror
def alert_on_critical(exc: BaseException) -> None:
    logger.critical("Alert: {}", exc)


with logger.catch(onerror=alert_on_critical):
    critical_operation()


# Lazy evaluation for expensive computations
def compute_metrics() -> str:
    return "cpu=45% mem=62%"


logger.opt(lazy=True).debug("Metrics: {}", compute_metrics)

logger.complete()

Released under the MIT License.