Skip to content

Scheduler Guide ​

This guide covers automatic log maintenance in Logly using the scheduler, including cleanup, compression, rotation tasks, and custom scheduled operations.

Overview ​

The scheduler module provides automatic log maintenance by running tasks on configurable schedules. This includes cleaning up old logs, compressing files, rotating logs, and running custom maintenance tasks.

Use Scheduler.validateDependencies() after wiring tasks together to catch missing dependency names and dependency cycles early.

Logger Configuration ​

Enable scheduler through the Config struct:

zig
const logly = @import("logly");

var config = logly.Config.default();
config.scheduler = .{
    .enabled = true,              // Enable scheduler
    .cleanupMaxAgeDays = 7,    // Delete logs older than 7 days
    .maxFiles = 10,              // Keep max 10 rotated files
    .compressBeforeCleanup = true, // Compress before deleting
    .filePattern = "*.log",      // Pattern for log files
};

// Or use helper method
var config2 = logly.Config.default().withScheduler(.{ .cleanupMaxAgeDays = 7 });

Advanced Configuration ​

zig
var config = logly.Config.default();
config.scheduler = .{
    .enabled = true,
    .cleanupMaxAgeDays = 7,
    .compressBeforeCleanup = true,
    // Archive root directory for all compressed files
    .archiveRootDir = "logs/archive",
    .createDateSubdirs = true,        // Organize by YYYY/MM/DD
    // Compression settings
    .compressionAlgorithm = .gzip,
    .compressionLevel = .best,
    .keepOriginals = false,
    // File naming customization
    .archiveFilePrefix = "archived_",
    .archiveFileSuffix = "_v1",
    .preserveDirStructure = true,
    // Cleanup settings
    .cleanEmptyDirs = true,
    .minAgeDaysForCompression = 1,
    .maxConcurrentCompressions = 4,
};

Scheduler Configuration Options ​

OptionTypeDefaultDescription
enabledboolfalseEnable the scheduler
cleanupMaxAgeDaysu647Max age for cleanup tasks
maxFiles?usizenullMax files to retain
compressBeforeCleanupboolfalseCompress before deleting
filePattern[]const u8"*.log"File pattern for tasks
archiveRootDir?[]const u8nullRoot directory for archives
createDateSubdirsboolfalseCreate YYYY/MM/DD structure
compressionAlgorithmCompressionAlgorithm.gzipCompression algorithm (gzip, zlib, deflate, zstd, lzma, lzma2, xz, zip, tar.gz, lz4)
compressionLevelCompressionLevel.defaultCompression level
keepOriginalsboolfalseKeep original files
archiveFilePrefix?[]const u8nullPrefix for file names
archiveFileSuffix?[]const u8nullSuffix for file names
preserveDirStructurebooltruePreserve directory structure
cleanEmptyDirsboolfalseRemove empty directories
minAgeDaysForCompressionu641Min age before compression
maxConcurrentCompressionsusize2Max parallel compressions

Quick Start ​

zig
const std = @import("std");
const logly = @import("logly");

pub fn main() !void {
    var gpa = std.heap.DebugAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Create scheduler
    var scheduler = try logly.Scheduler.init(allocator);
    defer scheduler.deinit();

    // Add a cleanup task
     _ = try scheduler.addTask(.{
        .name = "daily_cleanup",
        .taskType = .cleanup,
        .schedule = .{ .daily = .{ .hour = 2, .minute = 0 } },
        .config = .{
            .path = "logs",
            .maxAgeSeconds = 30 * 24 * 60 * 60, // 30 days
        },
    });

    // Start scheduler
    try scheduler.start();
    defer scheduler.stop();
}

Schedule Types ​

Interval ​

Run at fixed intervals:

zig
// Every 60 seconds
.schedule = .{ .interval = 60 * 1000 },

// Every 5 minutes  
.schedule = .{ .interval = 5 * 60 * 1000 },

// Every 2 hours
.schedule = .{ .interval = 2 * 60 * 60 * 1000 },

Daily ​

Run once per day at a specific time:

zig
// At 2:30 AM
.schedule = logly.Schedule.daily(2, 30),

// At midnight
.schedule = logly.Schedule.daily(0, 0),

// At 6 PM
.schedule = logly.Schedule.daily(18, 0),

Weekly ​

Run once per week:

zig
// Sunday at 3 AM
.schedule = logly.Schedule.weekly(0, 3, 0),

// Monday at 9 AM
.schedule = logly.Schedule.weekly(1, 9, 0),

// Friday at 5 PM
.schedule = logly.Schedule.weekly(5, 17, 0),

One-Time ​

Run once at a specific timestamp:

zig
.schedule = .{
    .type = .once,
    .timestamp = future_timestamp,
},

Jitter ​

Add optional random delay (jitter) to prevent thundering herd when multiple tasks are scheduled at the same time:

zig
.schedule = .{
    .interval = 60 * 1000,
    .jitter_ms = 5000, // ±5 seconds random delay
},

Task Types ​

Cleanup Tasks ​

Remove old log files:

zig
 _ = try scheduler.addTask(.{
    .name = "log_cleanup",
    .taskType = .cleanup,
    .schedule = logly.Schedule.daily(3, 0),
    .config = .{ .cleanup = .{
        .path = "logs",
        .maxAgeDays = 30,
        .pattern = "*.log",
        .include_compressed = true,
        .min_files_to_keep = 5,
        .dry_run = false,
    }},
});

Cleanup Configuration ​

OptionDefaultDescription
pathrequiredDirectory to clean
maxAgeDays30Maximum file age
pattern"*.log"Glob pattern
include_compressedtrueInclude .gz files
min_files_to_keep5Keep at least N files
dry_runfalseDon't actually delete

Compression Tasks ​

Compress old log files:

zig
 _ = try scheduler.addTask(.{
    .name = "log_compression",
    .taskType = .compression,
    .schedule = logly.Schedule.everyHours(1),
    .config = .{ .compression = .{
        .path = "logs",
        .pattern = "*.log",
        .minAgeDays = 1,
        .delete_originals = true,
        .skip_compressed = true,
    }},
});

Compression Configuration ​

OptionDefaultDescription
pathrequiredDirectory to compress
pattern"*.log"File pattern
minAgeDays1Minimum age to compress
delete_originalstrueDelete after compression
skip_compressedtrueSkip .gz files

Compression Modes ​

The scheduler supports multiple compression strategies:

ModeFieldBehavior
Compress & DeletecompressBeforeDelete = trueCompress file, then delete original
Compress & KeepcompressAndKeep = trueCompress file, keep both versions
Compress OnlycompressOnly = trueCompress file, never delete anything

Example: Compress and Keep Both

zig
 _ = try scheduler.addTask(.{
    .name = "archive_logs",
    .taskType = .cleanup,
    .schedule = logly.Schedule.daily(2, 0),
    .config = .{
        .path = "logs",
        .filePattern = "*.log",
        .minAgeSeconds = 24 * 60 * 60, // 1 day
        .compressAndKeep = true,       // Keep both original and compressed
        .skipAlreadyCompressed = true,
    },
});

Example: Compress Only (No Deletion)

zig
 _ = try scheduler.addTask(.{
    .name = "pure_archive",
    .taskType = .cleanup,
    .schedule = logly.Schedule.daily(3, 0),
    .config = .{
        .path = "logs",
        .filePattern = "*.log",
        .compressOnly = true,           // Only compress, never delete
        .skipAlreadyCompressed = true,
    },
});

Rotation Tasks ​

Force log rotation:

zig
 _ = try scheduler.addTask(.{
    .name = "forced_rotation",
    .taskType = .rotation,
    .schedule = logly.Schedule.daily(0, 0), // Midnight
    .config = .{ .rotation = .{
        .path = "logs/app.log",
        .force = true,
    }},
});

Custom Tasks ​

Run custom maintenance functions:

zig
 _ = try scheduler.addTask(.{
    .name = "custom_maintenance",
    .taskType = .custom,
    .schedule = logly.Schedule.everyMinutes(30),
    .config = .{ .custom = .{
        .callback = myMaintenanceFunction,
        .context = @ptrCast(&my_data),
    }},
});

fn myMaintenanceFunction(ctx: *anyopaque) void {
    // Custom maintenance logic
    const data: *MyData = @alignCast(@ptrCast(ctx));
    // ...
}

Flush Tasks ​

Flush all log buffers:

zig
 _ = try scheduler.addTask(.{
    .name = "periodic_flush",
    .taskType = .flush,
    .schedule = logly.Schedule.everyMinutes(5),
    .config = .{ .none = {} },
});

Health Check Tasks ​

Run periodic health checks:

zig
 _ = try scheduler.addTask(.{
    .name = "health_check",
    .taskType = .healthCheck,
    .schedule = logly.Schedule.interval(300), // 5 minutes
    .config = .{ .none = {} },
});

Task Management ​

Adding Tasks ​

zig
const taskIndex = try scheduler.addTask(.{
    .name = "my_task",
    // ...
});

Removing Tasks ​

zig
try scheduler.removeTask(taskIndex);

Enabling/Disabling Tasks ​

zig
// Disable temporarily
scheduler.disableTask(taskIndex);

// Re-enable
scheduler.enableTask(taskIndex);

Global Pause and Resume ​

You can temporarily pause all background execution of tasks:

zig
scheduler.pause();
// ... no tasks will execute ...
scheduler.unpause();

Running Tasks Manually ​

zig
// Execute immediately, regardless of schedule
try scheduler.runTaskNow(taskIndex);

Listing Tasks ​

zig
const tasks = scheduler.listTasks();
for (tasks, 0..) |task, i| {
    std.debug.print("Task {d}: {s} ({s})\n", .{
        i,
        task.name,
        @tagName(task.taskType),
    });
}

Presets ​

Use built-in presets for common scenarios:

zig
// Daily cleanup at 2 AM, keep 30 days
 _ = try scheduler.addTask(
    logly.SchedulerPresets.dailyCleanup("logs"),
);

// Hourly compression
 _ = try scheduler.addTask(
    logly.SchedulerPresets.hourlyCompression("logs"),
);

// Weekly deep clean on Sunday 3 AM
 _ = try scheduler.addTask(
    logly.SchedulerPresets.weeklyDeepClean("logs"),
);

Compression Presets ​

zig
const Presets = logly.SchedulerPresets;

// Compress then delete originals (files older than 7 days)
const archive_config = Presets.compressThenDelete("logs", 7);

// Compress and keep both versions (files older than 3 days)
const backup_config = Presets.compressAndKeep("logs", 3);

// Only compress, never delete anything (files older than 1 day)
const pure_archive = Presets.compressOnly("logs", 1);

// Archive old logs: compress after 7 days, delete after 30 days
const tiered_config = Presets.archiveOldLogs("logs", 7, 30);

// Aggressive cleanup: compress & delete, max 100 files
const aggressive_config = Presets.aggressiveCleanup("logs", 30, 100);

Preset Summary ​

PresetCompressDeleteKeep BothUse Case
compressThenDelete✓✓✗Standard archival
compressAndKeep✓✗✓Redundant backup
compressOnly✓✗✗Pure archival
archiveOldLogs✓✓ (aged)✗Tiered retention
aggressiveCleanup✓✓✗Space-constrained

Statistics ​

Monitor scheduler performance with atomic stats and helper methods:

zig
const stats = scheduler.getStats();

// Use helper methods for easy access
std.debug.print("Tasks executed: {d}\n", .{stats.getExecuted()});
std.debug.print("Tasks failed: {d}\n", .{stats.getFailed()});
std.debug.print("Files cleaned: {d}\n", .{stats.getFilesCleaned()});
std.debug.print("Files compressed: {d}\n", .{stats.getFilesCompressed()});
std.debug.print("Bytes freed: {d}\n", .{stats.getBytesFreed()});
std.debug.print("Bytes saved: {d}\n", .{stats.getBytesSaved()});

// Rate calculations
std.debug.print("Success rate: {d:.1}%\n", .{stats.successRate() * 100});
std.debug.print("Failure rate: {d:.1}%\n", .{stats.failureRate() * 100});

// Uptime info
std.debug.print("Uptime: {d} seconds\n", .{stats.uptimeSeconds()});
std.debug.print("Tasks per hour: {d:.2}\n", .{stats.tasksPerHour()});

// Check for failures
if (stats.hasFailures()) {
    std.log.warn("Scheduler has failed tasks!", .{});
}

SchedulerStats Methods ​

MethodReturnsDescription
getExecuted()u64Total successful executions
getFailed()u64Total failed executions
getFilesCleaned()u64Total files cleaned
getFilesCompressed()u64Total files compressed
getBytesFreed()u64Total bytes freed
getBytesSaved()u64Total bytes saved by compression
successRate()f64Success rate (0.0 - 1.0)
failureRate()f64Failure rate (0.0 - 1.0)
hasFailures()boolCheck if any failures occurred
uptimeSeconds()i64Scheduler uptime in seconds
tasksPerHour()f64Average tasks per hour
compressionRatio()f64Compression savings ratio

Configuration ​

Scheduler Configuration ​

zig
// Initialize scheduler with explicit allocator (or pass explicit io handle via initWithIo)
var scheduler = try logly.Scheduler.init(allocator);
defer scheduler.deinit();

// Alternatively, configure via central Config.SchedulerConfig:
var config = logly.Config.default();
config.scheduler = .{
    .enabled = true,
    .cleanupMaxAgeDays = 7,
    .maxFiles = 10,
    .compressBeforeCleanup = true,
    .filePattern = "*.log",
};
var schedulerFromConfig = try logly.Scheduler.initFromConfig(allocator, config.scheduler, "logs");
defer schedulerFromConfig.deinit();

Timezone Handling ​

zig
// UTC
.timezone_offset = 0

// Eastern Time (EST)
.timezone_offset = -5

// Central European Time (CET)
.timezone_offset = 1

// Pacific Time (PST)
.timezone_offset = -8

Error Handling ​

Retry Configuration ​

zig
.retry_failed = true,
.maxRetries = 3,
.retryDelayMs = 5000,

Task Failure Handling & Retries ​

When a task fails (its callback returns an error):

  1. Error is logged (if log_executions is true)
  2. failure_count is incremented
  3. If maxRetries is configured, the task is retried with exponential backoff: delay = retryBackoffMs * (2 ^ attempt)
  4. After maxRetries attempts, the task remains enabled but marked as failed until the next scheduled interval
zig
 _ = try scheduler.addTask(.{
    .name = "my_task",
    .taskType = .custom,
    .schedule = logly.Schedule.daily(2, 0),
    .config = .{
        .maxRetries = 3,         // Retry up to 3 times
        .retryBackoffMs = 1000, // Starts at 1s, then 2s, then 4s
    },
});

Monitoring Failures ​

zig
const tasks = scheduler.listTasks();
for (tasks) |task| {
    if (task.failure_count > 0) {
        std.debug.print("Task '{s}' failed {d} times\n", .{
            task.name,
            task.failure_count,
        });
    }
}

Integration with Other Modules ​

With Compression Module ​

zig
var compression = logly.Compression.init(allocator, .{
    .mode = .scheduled,
});

// Compression triggered by scheduler
 _ = try scheduler.addTask(.{
    .taskType = .compression,
    .config = .{ .compression = .{
        .path = "logs",
    }},
});

With Rotation ​

zig
// Daily rotation at midnight
 _ = try scheduler.addTask(.{
    .name = "daily_rotation",
    .taskType = .rotation,
    .schedule = logly.Schedule.daily(0, 0),
    .config = .{ .rotation = .{
        .path = "logs/app.log",
    }},
});

Best Practices ​

1. Schedule Non-Peak Hours ​

zig
// Run cleanup during low-traffic hours
.schedule = logly.Schedule.daily(3, 0), // 3 AM

2. Stagger Tasks ​

zig
// Don't run all tasks at the same time
 _ = scheduler.addTask(.{ .schedule = logly.Schedule.daily(2, 0) });  // Cleanup
 _ = scheduler.addTask(.{ .schedule = logly.Schedule.daily(3, 0) });  // Compression
 _ = scheduler.addTask(.{ .schedule = logly.Schedule.daily(4, 0) });  // Health check

3. Use Dry Run for Testing ​

zig
.config = .{ .cleanup = .{
    .dry_run = true, // Test without deleting
}},

4. Set Minimum Files ​

zig
// Always keep some logs for debugging
.config = .{ .cleanup = .{
    .min_files_to_keep = 10,
}},

5. Monitor Statistics ​

Regularly check statistics to ensure tasks are running:

zig
const stats = scheduler.getStats();

// Check for failures using helper method
if (stats.hasFailures()) {
    std.log.warn("Scheduler has {d} failed tasks", .{stats.getFailed()});
    // Alert on failures
}

// Monitor efficiency
const rate = stats.successRate();
if (rate < 0.95) {
    std.log.warn("Task success rate below 95%: {d:.1}%", .{rate * 100});
}

// Check uptime and throughput
std.log.info("Running for {d}s, {d:.2} tasks/hour", .{
    stats.uptimeSeconds(),
    stats.tasksPerHour(),
});

Example: Production Setup ​

zig
const std = @import("std");
const logly = @import("logly");

pub fn main() !void {
    var gpa = std.heap.DebugAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Initialize scheduler
    var scheduler = try logly.Scheduler.init(allocator);
    defer scheduler.deinit();

    // Daily cleanup at 2 AM - remove logs older than 30 days
    _ = try scheduler.addTask(
        "daily_cleanup",
        .cleanup,
        logly.SchedulerPresets.dailyAt(2, 0),
        .{
            .path = "logs",
            .maxAgeSeconds = 30 * 24 * 60 * 60,
            .filePattern = "*.log",
        },
    );

    // Hourly compression - compress uncompressed logs
    _ = try scheduler.addTask(
        "hourly_compression",
        .compression,
        .{ .interval = 3600000 },
        .{
            .path = "logs",
            .filePattern = "*.log",
        },
    );

    // Start scheduler
    try scheduler.start();

    // ... application runs ...

    // Graceful shutdown
    scheduler.stop();
}

See Also ​

Telemetry Integration ​

The scheduler supports optional telemetry integration for distributed tracing and observability. When enabled, each task execution creates a span with task-specific attributes.

Basic Setup ​

zig
const logly = @import("logly");

// Create telemetry instance
var telemetry = try logly.Telemetry.init(allocator, .{
    .provider = .file,
    .exporterFilePath = "scheduler_telemetry.jsonl",
});
defer telemetry.deinit();

// Create and configure scheduler
var scheduler = try logly.Scheduler.init(allocator);
defer scheduler.deinit();

// Enable telemetry
scheduler.setTelemetry(&telemetry);

// Add tasks and start
 _ = try scheduler.addTask(.{
    .name = "daily_cleanup",
    .taskType = .cleanup,
    .schedule = logly.Schedule.daily(2, 0),
    .config = .{
        .path = "logs",
        .maxAgeSeconds = 30 * 24 * 60 * 60,
    },
});

try scheduler.start();

Span Attributes ​

Task execution spans include:

AttributeTask TypesDescription
task.typeAllTask type name
task.priorityAllTask priority
task.durationMsAllExecution time
cleanup.filesDeletedcleanupFiles deleted
cleanup.bytesFreedcleanupBytes freed
compression.filescompressionFiles compressed
compression.bytesSavedcompressionSpace saved
health.healthyhealthCheckHealth status

Export to External Systems ​

zig
// Export to Jaeger
var telemetry = try logly.Telemetry.init(allocator, .{
    .provider = .jaeger,
    .endpoint = "http://jaeger:14268/api/traces",
});

// Export to Zipkin
var telemetry = try logly.Telemetry.init(allocator, .{
    .provider = .zipkin,
    .endpoint = "http://zipkin:9411/api/v2/spans",
});

Disabling Telemetry ​

zig
// Disable telemetry when not needed
scheduler.clearTelemetry();

New Presets (v0.0.9) ​

zig
const SchedulerPresets = logly.SchedulerPresets;

// Every N minutes presets
var every_5 = SchedulerPresets.every5Minutes();
var every_15 = SchedulerPresets.every15Minutes();
var every_30 = SchedulerPresets.every30Minutes();

// Hourly presets
var every_hour = SchedulerPresets.everyHour();
var every_6 = SchedulerPresets.every6Hours();
var every_12 = SchedulerPresets.every12Hours();

// Daily presets
var midnight = SchedulerPresets.dailyMidnight();
var maintenance = SchedulerPresets.dailyMaintenance();  // 2 AM
var at_time = SchedulerPresets.dailyAt(9, 30);  // 9:30 AM

// Task configs
var cleanup = SchedulerPresets.dailyCleanup("logs", 30);
var compress = SchedulerPresets.hourlyCompression("logs");
var weekly = SchedulerPresets.weeklyCleanupConfig("logs", 90);

Aliases ​

AliasMethod
beginstart
endstop
haltstop
statisticsgetStats
taskCountcount
isRunningrunning
hasTaskshasScheduledTasks

Released under the MIT License.