Skip to content

Async Sinks

Logly supports async callable sinks, including regular async def functions, class instances with async __call__ methods, and functools.partial wrapping async callables.

Basic Usage

python
import asyncio
from logly import logger


async def async_sink(message: str) -> None:
    await send_to_cloud(message)


logger.add(async_sink, level="INFO")
logger.info("Hello from async sink")

Class-Based Async Sinks

Logly correctly detects class instances with async __call__ methods as async sinks:

python
import asyncio
from logly import logger


class AsyncCloudSink:
    def __init__(self, api_key: str):
        self.api_key = api_key

    async def __call__(self, message: str) -> None:
        await send_to_cloud(message, self.api_key)


sink = AsyncCloudSink(api_key="your-key")
logger.add(sink, level="INFO")
logger.info("Sent via async class sink")
logger.complete()  # Flush pending async tasks

This also works with stateful sinks that maintain context between log calls:

python
from logly import logger


class BufferedAsyncSink:
    def __init__(self):
        self.buffer = []

    async def __call__(self, message: str) -> None:
        self.buffer.append(message)
        if len(self.buffer) >= 10:
            await self.flush()

    async def flush(self):
        await send_batch(self.buffer)
        self.buffer.clear()


logger.add(BufferedAsyncSink(), level="INFO")

functools.partial Wrapping

python
import asyncio
import functools
from logly import logger


async def cloud_sink(prefix: str, message: str) -> None:
    await send_to_cloud(f"[{prefix}] {message}")


sink = functools.partial(cloud_sink, "APP")
logger.add(sink, level="INFO")
logger.info("Wrapped async sink")

Event Loop Detection

Logly automatically detects the event loop context when you add an async sink:

If an event loop is running (e.g., inside async def main()):

python
import asyncio
from logly import logger


async def cloud_sink(message: str) -> None:
    await http_client.post("https://logs.example.com", content=message)


async def main():
    logger.add(cloud_sink, level="INFO")
    logger.info("Running inside event loop")
    logger.complete()  # Waits for async tasks


asyncio.run(main())

If no event loop is running (e.g., in a sync script):

python
from logly import logger


async def cloud_sink(message: str) -> None:
    await send_to_cloud(message)


logger.add(cloud_sink, level="INFO")
logger.info("Running outside event loop")
logger.complete()  # Logly creates a background loop and flushes

When no loop is running, Logly creates a background thread with its own event loop to run the async sink.

Explicit Loop Parameter

You can pass a specific event loop via the loop parameter:

python
import asyncio
from logly import logger

loop = asyncio.new_event_loop()


async def async_sink(message: str) -> None:
    await process_log(message)


logger.add(async_sink, level="INFO", loop=loop)
logger.info("Sent to async sink on custom loop")
logger.complete()

Flushing Async Sinks

Always call logger.complete() before your process exits. This:

  1. Drains all enqueued synchronous messages
  2. Awaits all pending async sink tasks
python
from logly import logger


async def metrics_sink(message: str) -> None:
    await send_to_metrics(message)


async def main():
    logger.add(metrics_sink, level="INFO")
    logger.info("Processing...")
    logger.complete()  # Ensures all async messages are flushed


asyncio.run(main())

Multiple Async Sinks

python
import asyncio
from logly import logger


async def cloud_sink(message: str) -> None:
    await send_to_cloud(message)


async def db_sink(message: str) -> None:
    await insert_into_database(message)


async def main():
    logger.add(cloud_sink, level="INFO")
    logger.add(db_sink, level="ERROR")
    logger.info("This goes to cloud only")
    logger.error("This goes to both cloud and database")
    logger.complete()


asyncio.run(main())

Mixing Sync and Async Sinks

python
import asyncio
from logly import logger


def sync_sink(message: str) -> None:
    print(f"SYNC: {message}", end="")


async def async_sink(message: str) -> None:
    await send_to_cloud(message)


async def main():
    logger.add(sync_sink, level="INFO")
    logger.add(async_sink, level="INFO")
    logger.info("Goes to both sync and async sinks")
    logger.complete()


asyncio.run(main())

Error Handling

If an async sink raises an exception, the error is logged to stderr but does not crash the application. Use catch=True (default) to prevent sink errors from propagating:

python
from logly import logger


async def risky_sink(message: str) -> None:
    # This might fail
    await unreliable_api_call(message)


logger.add(risky_sink, level="ERROR", catch=True)

Common Patterns

Cloud Logging

python
import asyncio
from logly import logger


async def cloud_logger(message: str) -> None:
    async with aiohttp.ClientSession() as session:
        await session.post(
            "https://your-cloud-logging-endpoint.com/ingest",
            json={"log": message},
        )


async def main():
    logger.add(cloud_logger, level="WARNING")
    logger.complete()


asyncio.run(main())

Database Logging

python
import asyncio
from logly import logger


async def db_logger(message: str) -> None:
    async with aiosqlite.connect("logs.db") as db:
        await db.execute(
            "INSERT INTO logs (message, created_at) VALUES (?, datetime('now'))",
            (message,),
        )
        await db.commit()


async def main():
    logger.add(db_logger, level="INFO")
    logger.complete()


asyncio.run(main())

Message Queue Logging

python
import asyncio
from logly import logger


async def kafka_sink(message: str) -> None:
    producer = AIOKafkaProducer()
    await producer.start()
    await producer.send("log-topic", message.encode())
    await producer.stop()


async def main():
    logger.add(kafka_sink, level="INFO")
    logger.complete()


asyncio.run(main())

API Reference

python
logger.add(
    async_sink,  # async def callable, class with async __call__, or functools.partial
    level="INFO",  # Minimum log level
    format=None,  # Format string or callable
    loop=None,  # Explicit event loop (optional)
    catch=True,  # Catch sink errors
    enqueue=False,  # Use background worker
    **kwargs,  # Other add() parameters
)
ParameterTypeDescription
loopasyncio.AbstractEventLoop | NoneEvent loop for the async sink. If None, auto-detected.
catchboolCatch sink errors (default True).
enqueueboolDispatch through background worker (default False).

Supported Async Callable Types

TypeDetected as AsyncExample
async def functionYesasync def sink(msg): ...
Class with async __call__Yesclass Sink: async def __call__(self, msg): ...
functools.partial wrapping asyncYesfunctools.partial(async_func, arg)
Regular functionNo (sync)def sink(msg): ...
LambdaNo (sync)lambda msg: ...

Batch HTTP Sink

For high-performance cloud logging, use BatchHttpJsonSink to batch multiple log records before sending:

python
from logly import BatchHttpJsonSink, logger

# Create a batch HTTP sink
sink = BatchHttpJsonSink(
    url="https://logs.example.com/ingest",
    batch_size=100,  # Send after 100 records
    flush_interval=5.0,  # Or flush every 5 seconds
    headers={"Authorization": "Bearer token"},
)

logger.add(sink, level="INFO")
logger.info("This will be batched")

Batch Sink Features

  • Automatic batching: Groups log records into batches
  • Time-based flushing: Flushes after a configurable interval
  • Size-based flushing: Flushes when batch reaches max size
  • Thread-safe: Safe to use from multiple threads
  • Rust-native: Implemented in Rust for maximum performance

Batch Sink API

python
from logly import BatchHttpJsonSink

sink = BatchHttpJsonSink(
    url="https://logs.example.com/ingest",
    batch_size=100,  # Max records per batch
    flush_interval=5.0,  # Seconds between flushes
    timeout=10.0,  # HTTP timeout in seconds
    headers={},  # Custom HTTP headers
)

# Manual control
sink.write(record)  # Add record to buffer
sink.flush()  # Force flush all buffered records
sink.buffer_len()  # Get current buffer size

Released under the MIT License.