Async Safe Logger
A simple, thread-safe, and asyncio-friendly logging wrapper built on top of Python's standard logging module.
AsyncSafeLogger moves log formatting, file I/O, log rotation, retention cleanup, and console output to a dedicated background thread using QueueHandler and QueueListener.
Your application code continues to use Python's standard logging API:
import logging
logger = logging.getLogger(__name__)
logger.info("Hello, world!")
No await is required for normal logging calls.
Features
- Built on Python's standard
loggingmodule - Asyncio-friendly — logging calls do not perform file or console I/O on the event-loop thread
- Thread-safe — safe to use from multiple threads and coroutines
- Background logging — formatting and output are handled by a dedicated
QueueListenerthread - Automatic log rotation — rotate log files when they reach the configured size
- Log retention cleanup — automatically remove expired log files
- Console and file output — independently configurable
- Multiple independent logger instances — no required global mutable state
- Synchronous and asynchronous lifecycle APIs
- Context manager support
- Zero runtime dependencies — only Python's standard library is required
Requirements
- Python 3.13+
The library currently has no third-party runtime dependencies.
Installation
Using pip
pip install async-safe-logger
Using uv
If you use uv, add the package to your project with:
uv add async-safe-logger
Or install it directly into the current environment:
uv pip install async-safe-logger
Quick Start
The simplest usage is to configure the logger and continue using Python's standard logging API.
import logging
from async_safe_logger import setup_logging, stop_logging
setup_logging()
logger = logging.getLogger(__name__)
logger.info("Hello, world!")
logger.warning("Something may be wrong.")
logger.error("Something went wrong.")
stop_logging()
By default, logs are:
- written to
logs/app.log - printed to stdout
- rotated when the file reaches 2 MB
- retained according to the configured retention policy
Asyncio Usage
AsyncSafeLogger is designed for applications using asyncio.
Normal logging calls remain synchronous from the caller's perspective:
import asyncio
import logging
from async_safe_logger import setup_logging, stop_logging_async
async def main():
setup_logging()
logger = logging.getLogger(__name__)
logger.info("Running inside asyncio")
logger.warning("This logging call does not write directly to disk.")
try:
...
finally:
await stop_logging_async()
asyncio.run(main())
Why stop_logging_async()?
Stopping a QueueListener may wait for its background thread to finish processing queued log records.
The synchronous:
stop_logging()
may therefore block while the listener shuts down.
When running inside an asyncio event loop, prefer:
await stop_logging_async()
This moves the potentially blocking shutdown operation to a worker thread.
How It Works
The library uses Python's standard QueueHandler and QueueListener.
The normal logging path looks approximately like this:
Application
│
│ logger.info(...)
▼
logging.Logger
│
▼
QueueHandler
│
│ put LogRecord into memory queue
▼
queue.Queue
│
▼
QueueListener
│
│ background thread
▼
┌───────────────┬────────────────┐
│ FileHandler │ ConsoleHandler │
└───────────────┴────────────────┘
│ │
▼ ▼
app.log stdout
The important point is that the application thread or asyncio event-loop thread does not directly perform:
- file writes
- console writes
- log rotation
- expired-log cleanup
Those operations are handled by the background listener thread.
This keeps the normal logging path lightweight and avoids performing blocking output operations directly from an asyncio event loop.
Configuration
The default logger can be configured through setup_logging().
For example:
from async_safe_logger import setup_logging
setup_logging(
log_dir="logs",
log_filename="app.log",
level=logging.INFO,
max_bytes=2 * 1024 * 1024,
backup_count=20,
retention_days=5,
file=True,
console=True,
)
Configuration Options
| Parameter | Default | Description |
|---|---|---|
log_dir |
"logs" |
Directory used for log files |
log_filename |
"app.log" |
Log file name |
level |
logging.INFO |
Minimum logging level |
max_bytes |
2 MB |
Maximum size of a log file before rotation |
backup_count |
20 |
Number of rotated log files to keep |
retention_days |
5 |
Delete log files older than this number of days |
file |
True |
Enable file logging |
console |
True |
Enable console logging |
fmt |
See below | Log message format |
datefmt |
"%Y-%m-%d %H:%M:%S" |
Timestamp format |
Default format:
%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s
For example:
2026-09-06 20:30:15 | INFO | myapp.main:main:42 - Server started
File and Console Output
File and console logging can be independently enabled.
File only
AsyncSafeLogger(
file=True,
console=False,
)
Console only
AsyncSafeLogger(
file=False,
console=True,
)
File and console
AsyncSafeLogger(
file=True,
console=True,
)
This is also the default configuration.
For safety, the following configuration is rejected:
AsyncSafeLogger(
file=False,
console=False,
)
It raises ValueError instead of silently discarding all log messages.
Multiple Independent Logger Systems
You can create multiple AsyncSafeLogger instances when different subsystems need different configurations.
For example:
import logging
from async_safe_logger import AsyncSafeLogger
business_logger = AsyncSafeLogger(
log_dir="logs/business",
log_filename="business.log",
level=logging.INFO,
)
debug_logger = AsyncSafeLogger(
log_dir="logs/debug",
log_filename="debug.log",
level=logging.DEBUG,
)
business_logger.setup()
debug_logger.setup()
logging.getLogger("business").info("Business event")
logging.getLogger("debug").debug("Debug information")
# ...
business_logger.stop()
debug_logger.stop()
Each instance owns its own queue, listener, and handlers.
Context Manager
AsyncSafeLogger supports both synchronous and asynchronous context managers.
Synchronous
import logging
from async_safe_logger import AsyncSafeLogger
with AsyncSafeLogger() as logger:
logging.info("Inside the context manager")
The logger is automatically stopped when leaving the with block.
Asynchronous
import logging
from async_safe_logger import AsyncSafeLogger
async with AsyncSafeLogger() as logger:
logging.info("Inside the async context manager")
When leaving the async with block, the logger is shut down asynchronously.
This is the recommended pattern when the surrounding application is already asynchronous.
Standard Logging Compatibility
The main goal of this library is to keep application code compatible with Python's standard logging API.
You can continue to use:
import logging
logger = logging.getLogger(__name__)
logger.debug("Debug message")
logger.info("Information")
logger.warning("Warning")
logger.error("Error")
logger.critical("Critical error")
There is no need to replace your application's logging calls with a custom API such as:
await logger.info(...)
Instead, logging remains familiar:
logger.info(...)
while the output pipeline is handled asynchronously in the background.
Log Rotation and Retention
The file handler combines size-based rotation with automatic retention cleanup.
For example:
AsyncSafeLogger(
log_dir="logs",
log_filename="app.log",
max_bytes=2 * 1024 * 1024,
backup_count=20,
retention_days=5,
)
When app.log reaches the configured size, it is rotated according to RotatingFileHandler.
After rotation, log files older than the configured retention period are removed.
Graceful Shutdown
It is important to stop the logger before the application exits so that queued log records have an opportunity to be processed.
For synchronous applications:
stop_logging()
For asyncio applications:
await stop_logging_async()
A typical asyncio application should use:
async def main():
setup_logging()
try:
...
finally:
await stop_logging_async()
This is especially important for applications that produce logs immediately before shutdown.
Design Goals
The project intentionally focuses on a small set of goals:
1. Keep the standard logging API
No custom logging syntax is required.
logger.info("hello")
remains the primary interface.
2. Keep blocking output away from the event loop
The application produces a LogRecord, which is placed into an in-memory queue.
The listener thread performs the actual output work.
3. Minimize runtime dependencies
The current implementation uses only Python's standard library.
4. Support both synchronous and asynchronous applications
The same logger can be used by:
- normal synchronous applications
- asyncio applications
- applications using both threads and asyncio
5. Keep the implementation simple
The library intentionally builds on well-tested components already provided by Python:
loggingQueueHandlerQueueListenerRotatingFileHandlerqueue.Queuethreading
Rather than implementing a completely new logging framework.
Important Notes
Logging is not completely "non-blocking"
AsyncSafeLogger moves output operations away from the calling thread, but the initial logging operation still performs normal Python logging work and enqueues a LogRecord.
Therefore, it should be understood as:
asyncio-friendly / async-safe logging output
rather than a mathematically guaranteed zero-cost or lock-free logging implementation.
Queue Backpressure
The internal queue is currently unbounded.
This means normal logging calls do not wait for the background listener to consume records.
However, an application that continuously produces logs significantly faster than the output destination can consume them may accumulate an increasing number of LogRecord objects in memory.
For high-volume logging workloads, monitor queue growth and application memory usage.
Shutdown
Always stop the logger during application shutdown:
await stop_logging_async()
for asyncio applications, or:
stop_logging()
for synchronous applications.
Project Status
This project is currently in an early development stage.
API design and implementation details may change before the 1.0.0 release.
The current version is:
0.1.0
License
MIT License.
See LICENSE for details.
Metadata
Release files for async-safe-logger 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| async_safe_logger-0.1.4.tar.gz | 6.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| async_safe_logger-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 13.9 kB
Release files / async_safe_logger-0.1.4.tar.gz
| Download URL | async_safe_logger-0.1.4.tar.gz |
|---|---|
| Size | 6.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c49da0916b9df5dcb6dca3dbbf93da6add7aea126457f0c09bf2ae9cc9e0e2fd
|
|
BLAKE2b-256 checksum How to use checksums |
f4363c3949e88f402b3bc066f61f04be70df5eea82039118dbc0ff64be4f7282
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.
Transparency logRelease files / async_safe_logger-0.1.4-py3-none-any.whl
| Download URL | async_safe_logger-0.1.4-py3-none-any.whl |
|---|---|
| Size | 7.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4f1e99b5f1027ded896b027204a9ee9a09c4a82a1801ffdf958e7893325c7b7f
|
|
BLAKE2b-256 checksum How to use checksums |
a5a205c172a65c2975acb8b2adf491613f6c861b7fc46b7cd228beb4398eb180
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.
Transparency log