Async Logging ​
Logly provides comprehensive asynchronous logging capabilities to ensure that logging operations do not block your application's main execution flow. This is particularly important for high-performance applications.
Overview ​
Logly offers multiple async logging options:
- Simple Async Sinks: Basic buffered file writing
- AsyncLogger: Full-featured async logger with ring buffers
- AsyncFileWriter: Optimized async file writing
Quick Start ​
Simple Async Sinks ​
The simplest way to use async logging:
_ = try logger.add(.{ // Short alias for addSink()
.path = "logs/app.log",
.async_write = true, // Enable async (default)
.buffer_size = 8192, // Buffer size in bytes (default 8KB)
});Full AsyncLogger ​
For more control, use the AsyncLogger directly:
const logly = @import("logly");
var async_logger = try logly.AsyncLogger.init(allocator, .{
.buffer_size = 8192,
.flush_interval_ms = 100,
.batch_size = 64,
});
defer async_logger.deinit();
try async_logger.start();
defer async_logger.stop();Parallel Logging (Thread Pool) ​
For high-throughput scenarios requiring heavy processing (e.g., complex formatting, compression, or multiple slow sinks), Logly supports parallel logging using a work-stealing thread pool.
Enabling Parallel Logging ​
var config = logly.Config.default();
config.thread_pool = .{
.enabled = true,
.thread_count = 0, // 0 = auto-detect based on CPU cores
.queue_size = 10000,
.work_stealing = true,
};
const logger = try logly.Logger.initWithConfig(allocator, config);When enabled, the Logger dispatches log records to the thread pool. Each record is deep-copied to ensure thread safety. The thread pool distributes tasks among worker threads, which then write to the configured sinks.
Benefits ​
- Non-blocking: The main application thread submits the task and returns immediately (unless the queue is full).
- Scalability: Utilizes multiple CPU cores for formatting and I/O.
- Resilience: Isolates slow sinks from the main application flow.
How it Works ​
When async logging is enabled, log messages follow this flow:
- Queuing: Messages are added to a lock-free ring buffer
- Background Processing: A worker thread processes queued messages
- Batch Writing: Messages are written in batches for efficiency
- Flushing: Buffers are flushed based on time or size
Configuration ​
Logger Configuration ​
Enable async logging through the Config struct:
const logly = @import("logly");
var config = logly.Config.default();
config.async_config = .{
.enabled = true, // Enable async logging
.buffer_size = 8192, // Ring buffer size
.batch_size = 100, // Messages per batch
.flush_interval_ms = 100, // Auto-flush interval
.overflow_policy = .drop_oldest, // On buffer overflow
.background_worker = true, // Auto-start worker thread
};
// Or use helper method
var config2 = logly.Config.default().withAsync(.{
.buffer_size = 16384,
});Basic Configuration ​
const config = logly.AsyncLogger.AsyncConfig{
.buffer_size = 8192, // Ring buffer size
.flush_interval_ms = 100, // Auto-flush interval
.batch_size = 64, // Messages per batch
.overflow_policy = .drop_oldest,
.background_worker = true,
.enable_metrics = true,
};Configuration Options ​
| Option | Default | Description |
|---|---|---|
buffer_size | 8192 | Ring buffer capacity |
flush_interval_ms | 100 | Auto-flush interval in ms |
min_flush_interval_ms | 0 | Minimum time between flushes |
max_latency_ms | 5000 | Maximum latency before forcing flush |
batch_size | 64 | Messages written per batch |
overflow_policy | .drop_oldest | Behavior when buffer full |
background_worker | true | Enable background thread |
Overflow Policies ​
Control what happens when the buffer is full:
pub const OverflowPolicy = enum {
drop_oldest, // Remove oldest to make room (default)
drop_newest, // Drop new messages
block, // Block until space available
};Choosing a Policy ​
drop_oldest: Best for most applications, ensures recent logsdrop_newest: Use when historical logs are more importantblock: When you can't afford to lose any logs
Callbacks ​
AsyncLogger supports various callbacks for monitoring and customization:
// Define callbacks
fn onOverflow(dropped: u64) void {
std.debug.print("Dropped {d} records\n", .{dropped});
}
fn onFlush(count: u64, bytes: u64, elapsed_ms: u64) void {
std.debug.print("Flushed {d} records ({d} bytes) in {d}ms\n", .{count, bytes, elapsed_ms});
}
// Register callbacks
async_logger.setOverflowCallback(onOverflow);
async_logger.setFlushCallback(onFlush);
async_logger.setFullCallback(onFull);
async_logger.setEmptyCallback(onEmpty);
async_logger.setWorkerStartCallback(onStart);
async_logger.setWorkerStopCallback(onStop);
async_logger.setErrorCallback(onError);Available callbacks:
on_overflow: Buffer overflow occurredon_full: Buffer became fullon_empty: Buffer became emptyon_flush: Flush operation completedon_worker_start: Worker thread startedon_worker_stop: Worker thread stopped (provides uptime)on_batch_processed: Batch processed (provides processing time)on_latency_threshold_exceeded: Latency exceeded thresholdon_error: Error occurred during write
Presets ​
Use built-in presets for common scenarios:
// Maximum throughput
const high_throughput = logly.AsyncPresets.highThroughput();
// Minimum latency
const low_latency = logly.AsyncPresets.lowLatency();
// Balanced (default)
const balanced = logly.AsyncPresets.balanced();
// Never drop messages
const no_drop = logly.AsyncPresets.noDrop();Blocking vs Non-Blocking ​
- Console Sink: Typically blocking (direct write to stdout/stderr)
- File Sink: Non-blocking (buffered) by default
- AsyncLogger: Fully non-blocking with background worker
Flushing ​
Manual Flush ​
// Flush all pending logs
async_logger.flush();
// Or for simple sinks
try logger.flush();Auto-Flush ​
Auto-flush triggers based on:
- Time: After
flush_interval_msmilliseconds - Size: When batch reaches
batch_size - Shutdown: Automatically on
stop()ordeinit()
Statistics ​
Monitor async performance:
const stats = async_logger.getStats();
std.debug.print("Queued: {d}\n", .{stats.records_queued.load(.monotonic)});
std.debug.print("Written: {d}\n", .{stats.records_written.load(.monotonic)});
std.debug.print("Dropped: {d}\n", .{stats.records_dropped.load(.monotonic)});
std.debug.print("Drop rate: {d:.2}%\n", .{stats.dropRate() * 100});
std.debug.print("Avg latency: {d}ns\n", .{stats.averageLatencyNs()});AsyncFileWriter ​
For optimized file writing:
var writer = try logly.AsyncFileWriter.init(allocator, .{
.file_path = "logs/app.log",
.buffer_size = 64 * 1024, // 64KB
.flush_interval_ms = 1000,
.sync_on_flush = false,
});
defer writer.deinit();
try writer.write("Log message\n");
try writer.flush();FileWriter Options ​
| Option | Default | Description |
|---|---|---|
file_path | required | Log file path |
buffer_size | 64KB | Write buffer size |
flush_interval_ms | 1000 | Auto-flush interval |
sync_on_flush | false | fsync on flush |
direct_io | false | Bypass OS cache |
append | true | Append to existing file |
Best Practices ​
1. Choose Appropriate Buffer Size ​
// High volume: larger buffers
.buffer_size = 65536
// Low latency: smaller buffers
.buffer_size = 10242. Monitor Drop Rate ​
if (stats.dropRate() > 0.01) { // > 1% drops
// Consider larger buffer or faster flush
}3. Graceful Shutdown ​
// Always stop properly to flush pending logs
defer async_logger.stop();4. Handle Backpressure ​
// For critical logs, use blocking policy
const critical_config = logly.AsyncLogger.AsyncConfig{
.overflow_policy = .block,
};Performance Tips ​
- Use batch writing: Larger batches = fewer I/O operations
- Tune flush interval: Balance latency vs throughput
- Pre-allocate buffers: Set
preallocate_buffers = true - Use direct I/O: For very high throughput (with caution)
Example: High-Throughput Setup ​
var async_logger = try logly.AsyncLogger.init(allocator, .{
.buffer_size = 65536,
.flush_interval_ms = 500,
.batch_size = 256,
.overflow_policy = .drop_oldest,
.preallocate_buffers = true,
});Example: Low-Latency Setup ​
var async_logger = try logly.AsyncLogger.init(allocator, .{
.buffer_size = 1024,
.flush_interval_ms = 10,
.batch_size = 16,
.overflow_policy = .block,
});See Also ​
New Methods (v0.0.9) ​
var async_logger = try logly.AsyncLogger.init(allocator, config);
defer async_logger.deinit();
// State methods
const running = async_logger.isRunning();
const capacity = async_logger.bufferCapacity();
const full = async_logger.isFull();
const depth = async_logger.queueDepth();
const empty = async_logger.isQueueEmpty();
// Reset statistics
async_logger.resetStats();Aliases ​
| Alias | Method |
|---|---|
enqueue | log |
push | log |
logMsg | log |
statistics | getStats |
depth | queueDepth |
pending | queueDepth |
begin | startWorker |
halt | stop |
end | stop |
