Task Execution Logs
Available since: v3.0
EverTask provides a built-in log capture system that records logs written during task execution. The logger acts as a proxy that ALWAYS forwards logs to the standard ILogger infrastructure (console, file, Serilog, Application Insights, etc.) and optionally persists them to the database for audit trails.
Why Use Log Capture?
- Debugging: Review exactly what happened during task execution, including retry attempts
- Audit Trails: Keep a permanent record of task execution logs in the database
- Compliance: Meet regulatory requirements for task execution logging
- Root Cause Analysis: Investigate failures with full execution context
Basic Usage
Access the logger via the Logger property in your task handler:
public class ProcessOrderHandler : EverTaskHandler<ProcessOrderTask>
{
public override async Task Handle(ProcessOrderTask task, CancellationToken ct)
{
Logger.LogInformation("Processing order {OrderId}", task.OrderId);
// Your business logic here
await ProcessOrder(task.OrderId);
Logger.LogInformation("Order {OrderId} processed successfully", task.OrderId);
}
}
Key Point: Logs are ALWAYS written to ILogger (console, file, etc.) regardless of persistence settings. This ensures you never lose visibility into task execution.
Structured Logging Support
The Logger property fully supports structured logging with message templates and parameters, just like standard ILogger:
public class DataProcessingHandler : EverTaskHandler<DataProcessingTask>
{
public override async Task Handle(DataProcessingTask task, CancellationToken ct)
{
// Structured logging with parameters
Logger.LogTrace("Processing step {Step}/{Total}", 1, task.TotalSteps);
Logger.LogDebug("User {UserId} initiated processing at {Timestamp}", task.UserId, DateTimeOffset.UtcNow);
Logger.LogInformation("Processing {Count} items from source {Source}", task.ItemCount, task.Source);
try
{
await ProcessData(task);
}
catch (Exception ex)
{
// Exception overload - exception as first parameter
Logger.LogError(ex, "Failed to process task {TaskId} at step {Step}", task.Id, currentStep);
throw;
}
}
}
Supported Overloads:
- Simple messages:
Logger.LogInformation("message") - Structured parameters:
Logger.LogInformation("User {UserId} logged in", userId) - With exception:
Logger.LogError(exception, "Failed processing {TaskId}", taskId) - All log levels:
LogTrace,LogDebug,LogInformation,LogWarning,LogError,LogCritical
Important: When persisted to the database, structured parameters are formatted into the final message (e.g., "User john.doe logged in"), while the original structured template and parameters are preserved in the ILogger infrastructure for Serilog, Application Insights, etc.
Configuration
Enable Database Persistence (Optional)
services.AddEverTask(opt => opt
.RegisterTasksFromAssembly(typeof(Program).Assembly)
.WithPersistentLogger(log => log // Auto-enables persistent logging
.SetMinimumLevel(LogLevel.Information) // Only persist Information+
.SetMaxLogsPerTask(1000))) // Limit logs per task
.AddSqlServerStorage(connectionString);
Configuration Options
| Option | Default | Description |
|---|---|---|
WithPersistentLogger | Disabled | Auto-enables persistent logging. Logs always go to ILogger regardless! |
Disable() | - | Disable database persistence (logs still go to ILogger) |
SetMinimumLevel() | Information | Minimum log level to persist. Only affects database, not ILogger. |
SetMaxLogsPerTask() | 1000 | Maximum logs to persist per task execution. null = unlimited. |
How It Works
The log capture system uses a proxy pattern:
Handler.Logger.LogInformation("msg")
↓
TaskLogCapture (proxy)
↙ ↘
ILogger Database
(always) (optional)
- Always Log to ILogger: Every log call forwards to
ILogger<THandler>for standard logging infrastructure - Conditional Persistence: If persistent logging is enabled via
.WithPersistentLogger(log => log.Enable()), logs are also stored in database - Filtered Persistence:
SetMinimumLevel()filters only database persistence, not ILogger
Retrieving Persisted Logs
// Get all logs for a task
var logs = await storage.GetExecutionLogsAsync(taskId);
foreach (var log in logs)
{
Console.WriteLine($"[{log.Level}] {log.TimestampUtc}: {log.Message}");
if (log.ExceptionDetails != null)
Console.WriteLine($"Exception: {log.ExceptionDetails}");
}
// Get paginated logs
var page = await storage.GetExecutionLogsAsync(taskId, skip: 0, take: 50);
Retry Attempt Tracking
Logs accumulate across ALL retry attempts:
public class RetryTaskHandler : EverTaskHandler<RetryTask>
{
public override async Task Handle(RetryTask task, CancellationToken ct)
{
Logger.LogInformation("Attempt started");
// If this fails and retries, each attempt logs "Attempt started"
// Database will contain: ["Attempt started", "Attempt started", "Attempt started", ...]
}
}
This is intentional - it provides complete visibility into all execution attempts.
Performance Considerations
When Disabled
- Zero overhead — JIT optimizations eliminate all log capture code paths
- Single
ifcheck per log call (negligible performance impact)
When Enabled
- Minimal impact — ~5-10ms overhead for typical tasks
- ~100 bytes per log in memory, single bulk INSERT after task completion
- Logs persist even on failure — Captured in the finally block for debugging failed tasks
Always
- ILogger Always Invoked — Standard Microsoft.Extensions.Logging overhead applies regardless of persistence settings
Best Practices
- Use Standard Log Levels:
LogInformationfor normal flow,LogWarningfor issues,LogErrorfor failures - Include Context: Log task parameters and key decision points
- Set Reasonable Limits: Default 1000 logs per task prevents unbounded growth
- Use for Debugging: Don’t rely on persisted logs for real-time monitoring (use ILogger infrastructure)
- Clean Up Old Logs: Implement retention policies to prevent database bloat
Example: Audit Trail
public class PaymentProcessorHandler : EverTaskHandler<ProcessPaymentTask>
{
public override async Task Handle(ProcessPaymentTask task, CancellationToken ct)
{
Logger.LogInformation("Payment processing started for amount {Amount}", task.Amount);
// Audit critical steps
Logger.LogInformation("Validating payment method");
await ValidatePaymentMethod(task.PaymentMethodId);
Logger.LogInformation("Charging payment gateway");
var result = await ChargePaymentGateway(task);
if (result.IsSuccess)
{
Logger.LogInformation("Payment succeeded with transaction ID {TransactionId}", result.TransactionId);
}
else
{
Logger.LogError("Payment failed: {ErrorMessage}", result.ErrorMessage);
throw new PaymentException(result.ErrorMessage);
}
}
}
With persistent logging enabled (.WithPersistentLogger(...)), all these logs are stored in the database and queryable by taskId.
Next Steps
- Monitoring Dashboard - View execution logs in the web UI
- Dashboard UI Guide - Terminal-style log viewer with color-coded severity levels
- Custom Event Monitoring - Build custom monitoring integrations
- Configuration Reference - All log capture configuration options