Custom Event Monitoring
Looking for the complete monitoring solution? Check out the Monitoring Dashboard for a ready-to-use web dashboard with REST API, embedded React UI, and real-time monitoring. This page covers custom event-based monitoring and DIY integrations.
EverTask gives you visibility into your background tasks through an event system that tracks execution, failures, and performance. This page covers the event-based monitoring system and custom integrations. For a complete monitoring solution with web dashboard, see the Monitoring Dashboard guide.
Table of Contents
- Task Events
- Durable Occurrence and Schedule Events
- Event Data Structure
- Basic Event Monitoring
- SignalR Real-Time Monitoring
- Custom Monitoring Integrations
- Logging Integration
- Best Practices
Task Events
EverTask publishes events for the task lifecycle through the TaskEventOccurredAsync event on IEverTaskWorkerExecutor. Subscribe to these events to build monitoring dashboards, send alerts, or integrate with external systems.
Event Types
- Task Started: When a task begins execution
- Task Completed: When a task finishes successfully
- Task Failed: When a task fails after all retry attempts
- Task Cancelled: When a task is cancelled
- Task Timeout: When a task exceeds its timeout
- Recurring Task Scheduled: When a recurring task is scheduled for next execution
-
Rate Limit Deferred (v3.7+): When the rate-limit gate parks a task waiting for its key’s budget. Severity
Information, machine-parseable message:Rate limit deferred task {id}: key={key} slotUtc={slot:O} policy={taskType} deferredCount={n}Deferral events are aggregated at the source (the first deferral of a (task type, key) window emits immediately, further deferrals surface as one summary per window with
deferredCount), so sustained throttling never floods subscribers. Per-deferral details are logged atDebug. Disable viaSetRateLimiterOptions(o => o.EmitDeferralEvents = false). - Rate Limit Fail-Open (v3.7+): Severity
Warning, published when the limiter exceedsMaxTrackedKeysand starts executing tasks for new keys unthrottled (message containsfail OPENand the runningtotalFailOpenCount). Mandatory signal: a silent fail-open under key-cardinality pressure would be invisible. - Rate Limit Rejected (v3.7+): Severity
Error(one-shot tasks markedFailedwith a typedRateLimitRejectedException) orWarning(recurring occurrence skipped, series alive). See terminal outcomes.
Severity Levels
public enum SeverityLevel
{
Information, // Normal operation (started, completed, scheduled)
Warning, // Non-critical issues (cancelled, timeout)
Error // Failures and exceptions
}
Durable Occurrence and Schedule Events
Schedules that materialize their slots as rows (durable occurrences) and schedules changed at runtime through ITaskScheduleManager publish through the same TaskEventOccurredAsync channel and carry the schedule context described above.
Grouping them by schedule takes both halves of that context, because the events are about two different rows. An event about an occurrence carries ParentTaskId, the schedule it belongs to. An event about the schedule itself (a halt, a reschedule, a provider that could not answer) is published on the schedule row, so its own TaskId is the schedule id and ParentTaskId is null, as it is for every row that is nobody’s occurrence. ScheduleVersion is set on both.
Messages are stable text, matched by consumers; the table quotes the shape, with {} for the values.
| Event | Severity | Message |
|---|---|---|
| Occurrence materialized | Information | Materialized occurrence {id} of schedule {scheduleId} for slot {slot} (run {n}) |
| Occurrence skipped | Warning | Schedule {scheduleId} skipped {n} due slot(s) from {from}: {cause} |
| Slot already served | Warning | Slot {slot} of schedule {scheduleId} already has an occurrence: the cursor was carried past it |
| Stale occurrence reconciled | Warning | Stale occurrence {id} of schedule {scheduleId} was stranded in {status} and has been requeued |
| Occurrence requeued | Information | Occurrence {id} of schedule {scheduleId} was requeued from {status} |
| Catch-up started | Information | Catch-up of schedule {scheduleId} started from slot {from}: {n} slot(s) are due |
| Catch-up completed | Information | Catch-up of schedule {scheduleId} completed: {n} occurrence(s) materialized since {start} |
| Catch-up halted | Error | Catch-up of schedule {scheduleId} halted at cursor {cursor}: {n} slots are due, more than the configured cap |
| Schedule unusable | Error | Schedule {scheduleId} cannot be rebuilt from its row and materializes nothing: {reason} |
| Occurrence unusable | Error | Occurrence {id} of schedule {scheduleId} cannot be rebuilt from its row and was marked Failed: {reason} |
| Occurrence unusable, still not ended | Error | Occurrence {id} of schedule {scheduleId} cannot be rebuilt from its row and could not be marked Failed (it is still {status}): {reason} |
| Occurrence rebuild exhausted | Error | Occurrence {id} of schedule {scheduleId} could not be rebuilt in {n} consecutive process start(s) and was marked Failed: {reason} |
| Occurrence rebuild exhausted, still not ended | Error | Occurrence {id} of schedule {scheduleId} could not be rebuilt in {n} consecutive process start(s) and could not be marked Failed (it is still {status}): {reason} |
| Provider re-park failed | Error | Schedule {scheduleId} could not be parked to ask the occurrence provider '{key}' again: nothing was written and the series stays where it is until the next startup recovery |
| Schedule revival failed | Error | Schedule {scheduleId} was dispatched again under the task key '{taskKey}' but could not be taken out of Cancelled: the series is not restarted, and no recovery brings back a cancelled row |
| Schedule rescheduled | Information / Warning | Schedule {id} rescheduled from version {a} to version {b} ({mode}): cursor {from} -> {to} |
| Re-park failed (reschedule) | Error | Schedule {id} is at version {v} but could not be handed back to the scheduler: {reason} |
| Re-park failed (materialization) | Error | Schedule {id} could not be re-parked after a failed materialization: it is parked nowhere and only the next startup recovery brings it back |
| Provider evaluation failed | Warning | Occurrence provider '{key}' could not answer for schedule {id} ({n} consecutive failure(s)): …parked to ask again at {retryAt} |
Four of them repay a closer look.
Catch-up started and catch-up completed are the two ends of one replay. Everything between them is reported per occurrence, and per occurrence there is no way to tell where a backlog began, how big it was, or that it has drained: a serial catch-up materializes its slots one delivery at a time, over as many materializer runs as it takes. The pair is per process: a host that restarts in the middle of a replay opens a new episode, which is what the schedule itself does with the backlog that is left. A replay that HALTS gets no completion event (the halt below is the event for that), and neither does one whose series is cancelled under it.
Occurrence skipped is the only ordinary way a durable schedule loses a slot, and it always names the rule that dropped it, because the three have different fixes:
outside the misfire window: older thannow - MaxAge. Widen the window, or accept the loss.older than the most recent slots the catch-up cap keeps: theSkipOldestoverflow policy kept the newestMaxOccurrencesand dropped the rest. Raise the cap, or accept it.the skip policy does not replay a slot that is no longer current: theSkippolicy, working as configured.
The count reads at least {n} instead of a bare number when the grid was counted under a cap: a lower bound is said to be one, never dressed up as a total.
Schedule rescheduled is Warning instead of Information when the new definition discarded a backlog, and the message then ends with ; {n} due slot(s) were discarded, plus ; the catch-up halt was released when the call cleared a halt. The change is committed before the series is handed back to the scheduler, so this event is published even when the re-park then fails and the Error above follows it.
Schedule unusable, occurrence unusable and provider re-park failed are three events an alert rule tends to miss, and they are worth waking someone for. The first two mean this build cannot rebuild a row out of what is persisted (a payload that stopped deserializing, a zone id that has left the tz database, a handler nobody registers any more), so the schedule materializes nothing at all, or the occurrence is ended as Failed instead of holding a slot of the concurrency budget for ever. An occurrence whose registered handler repeatedly fails to build uses could not be rebuilt in {n} consecutive process start(s) after its recovery allowance is exhausted; other unusable rows use cannot be rebuilt from its row. Each cause also has a second wording for the case where ending the row did not take. SetStatus is best-effort on every relational provider, so the row is read back and the event says which outcome really happened. could not be marked Failed means the occurrence is still alive in the status the message names, still holding its slot of the budget, and the schedule materializes nothing more until a later run ends it for real. Same alert, one more thing wrong. The third means an occurrence provider could not answer AND the series could not be parked to ask it again. Nothing polls behind that one: the schedule stays where it is until the next startup recovery.
Catch-up halted is rate-limited to one event per schedule every five minutes: the halt is a standing condition, not a stream of incidents. It is re-emitted when the marker is found again, on every kick and once per restart, until an operator resumes or reschedules the series.
Event Data Structure
Each event includes everything you need to track what happened:
public record EverTaskEventData(
Guid TaskId,
DateTimeOffset EventDateUtc,
string Severity,
string TaskType,
string TaskHandlerType,
string TaskParameters,
string Message,
string? Exception = null,
IReadOnlyList<TaskExecutionLog>? ExecutionLogs = null)
{
public Guid? ParentTaskId { get; init; }
public DateTimeOffset? ScheduledAtUtc { get; init; }
public int? ScheduleVersion { get; init; }
}
ExecutionLogs carries the captured handler logs of the delivery, when log capture is on and the event is one that reports a finished execution.
The three schedule properties are init members in the record body, never appended constructor parameters: the primary constructor and Deconstruct signatures are what a subscriber compiled against an earlier version calls, so they stay exactly what they were.
| Property | Set on |
|---|---|
ParentTaskId | An occurrence of a durable schedule: the schedule row it belongs to |
ScheduledAtUtc | Any scheduled delivery: the nominal slot it stands for, not the moment it fired |
ScheduleVersion | A recurring schedule row and every occurrence of one; null for a task that belongs to no schedule |
Example Event Data
{
"TaskId": "dc49351d-476d-49f0-a1e8-3e2a39182d22",
"EventDateUtc": "2024-10-19T16:10:20+00:00",
"Severity": "Information",
"TaskType": "MyApp.Tasks.SendEmailTask",
"TaskHandlerType": "MyApp.Handlers.SendEmailHandler",
"TaskParameters": "{\"Email\":\"user@example.com\",\"Subject\":\"Welcome\"}",
"Message": "Task with id dc49351d-476d-49f0-a1e8-3e2a39182d22 was completed in 12.5 ms",
"Exception": null,
"ExecutionLogs": null,
"ParentTaskId": null,
"ScheduledAtUtc": null,
"ScheduleVersion": null
}
Basic Event Monitoring
The simplest way to monitor tasks is to subscribe to events in your services:
Simple Event Subscription
public class TaskMonitoringService
{
private readonly ILogger<TaskMonitoringService> _logger;
public TaskMonitoringService(
IEverTaskWorkerExecutor executor,
ILogger<TaskMonitoringService> logger)
{
_logger = logger;
executor.TaskEventOccurredAsync += OnTaskEventAsync;
}
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
_logger.LogInformation(
"Event from EverTask: [{Severity}] {Message}",
eventData.Severity,
eventData.Message);
return Task.CompletedTask;
}
}
// Register in Program.cs
builder.Services.AddSingleton<TaskMonitoringService>();
Filtering by Severity
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
switch (eventData.Severity)
{
case nameof(SeverityLevel.Error):
_logger.LogError(
eventData.Exception,
"Task {TaskId} of type {TaskType} failed: {Message}",
eventData.TaskId,
eventData.TaskType,
eventData.Message);
break;
case nameof(SeverityLevel.Warning):
_logger.LogWarning(
"Task {TaskId} warning: {Message}",
eventData.TaskId,
eventData.Message);
break;
case nameof(SeverityLevel.Information):
_logger.LogInformation(
"Task {TaskId}: {Message}",
eventData.TaskId,
eventData.Message);
break;
}
return Task.CompletedTask;
}
Tracking Specific Tasks
Sometimes you only care about certain types of tasks. Here’s how to filter for specific task types:
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
// Only monitor critical tasks
if (eventData.TaskType.Contains("Payment") ||
eventData.TaskType.Contains("Order"))
{
_logger.LogInformation(
"Critical task event: {@EventData}",
eventData);
// Send to external monitoring
_telemetry.TrackEvent("CriticalTaskEvent", new Dictionary<string, string>
{
["TaskId"] = eventData.TaskId.ToString(),
["TaskType"] = eventData.TaskType,
["Severity"] = eventData.Severity,
["Message"] = eventData.Message
});
}
return Task.CompletedTask;
}
SignalR Real-Time Monitoring
If you’re building an ASP.NET Core application, you can watch tasks execute in real-time using the SignalR integration. This is especially useful for admin dashboards or debugging during development.
Note: The Monitoring Dashboard automatically includes SignalR integration with a React UI. Use the approach below only if you’re building a custom monitoring interface.
Installation
dotnet add package EverTask.Monitor.AspnetCore.SignalR
Configuration
using EverTask.Monitor.AspnetCore.SignalR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEverTask(opt =>
{
opt.RegisterTasksFromAssembly(typeof(Program).Assembly);
})
.AddSqlServerStorage(connectionString)
.AddSignalRMonitoring(); // Add SignalR monitoring
var app = builder.Build();
// Map the hub so the monitor subscribes to task events.
// Without this call the SignalR monitor is registered but never receives events.
// Default hub path: /evertask-monitoring/hub
app.MapEverTaskMonitorHub();
app.Run();
JavaScript Client
<!DOCTYPE html>
<html>
<head>
<title>EverTask Monitor</title>
<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@latest/dist/browser/signalr.min.js"></script>
</head>
<body>
<h1>EverTask Real-Time Monitor</h1>
<div id="events"></div>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/evertask-monitoring/hub")
.withAutomaticReconnect()
.build();
connection.on("TaskEvent", (eventData) => {
console.log("Task event received:", eventData);
const eventDiv = document.createElement("div");
eventDiv.className = `event event-${eventData.severity.toLowerCase()}`;
eventDiv.innerHTML = `
<strong>${eventData.severity}</strong>:
${eventData.taskType} -
${eventData.message}
<small>(${new Date(eventData.eventDateUtc).toLocaleString()})</small>
`;
document.getElementById("events").prepend(eventDiv);
});
connection.start()
.then(() => console.log("Connected to EverTask monitoring"))
.catch(err => console.error("Connection error:", err));
</script>
<style>
.event { padding: 10px; margin: 5px 0; border-left: 4px solid #ccc; }
.event-information { border-left-color: #007bff; }
.event-warning { border-left-color: #ffc107; }
.event-error { border-left-color: #dc3545; }
</style>
</body>
</html>
.NET Client
using Microsoft.AspNetCore.SignalR.Client;
public class EverTaskMonitorClient
{
private readonly HubConnection _connection;
private readonly ILogger<EverTaskMonitorClient> _logger;
public EverTaskMonitorClient(string hubUrl, ILogger<EverTaskMonitorClient> logger)
{
_logger = logger;
_connection = new HubConnectionBuilder()
.WithUrl(hubUrl)
.WithAutomaticReconnect()
.Build();
_connection.On<EverTaskEventData>("TaskEvent", OnTaskEvent);
}
public async Task StartAsync()
{
await _connection.StartAsync();
_logger.LogInformation("Connected to EverTask monitoring hub");
}
private void OnTaskEvent(EverTaskEventData eventData)
{
_logger.LogInformation(
"Received task event: [{Severity}] {TaskType} - {Message}",
eventData.Severity,
eventData.TaskType,
eventData.Message);
}
public async Task StopAsync()
{
await _connection.StopAsync();
}
}
// Usage
var client = new EverTaskMonitorClient("https://localhost:5001/evertask-monitoring/hub", logger);
await client.StartAsync();
Filtering Events on Client
You can filter events client-side to avoid cluttering your UI with informational messages:
connection.on("TaskEvent", (eventData) => {
// Only show errors and warnings
if (eventData.severity === "Error" || eventData.severity === "Warning") {
displayEvent(eventData);
// Play alert sound for errors
if (eventData.severity === "Error") {
playAlertSound();
}
}
// Track metrics
updateMetrics(eventData);
});
function updateMetrics(eventData) {
// Update dashboard metrics
if (eventData.severity === "Error") {
incrementErrorCount();
}
if (eventData.message.includes("completed")) {
incrementCompletedCount();
}
}
Custom Monitoring Integrations
Want to integrate with your existing monitoring tools? Here are examples for popular platforms.
Application Insights
public class ApplicationInsightsMonitor
{
private readonly TelemetryClient _telemetry;
public ApplicationInsightsMonitor(
IEverTaskWorkerExecutor executor,
TelemetryClient telemetry)
{
_telemetry = telemetry;
executor.TaskEventOccurredAsync += OnTaskEventAsync;
}
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
var properties = new Dictionary<string, string>
{
["TaskId"] = eventData.TaskId.ToString(),
["TaskType"] = eventData.TaskType,
["TaskHandlerType"] = eventData.TaskHandlerType,
["Severity"] = eventData.Severity
};
switch (eventData.Severity)
{
case nameof(SeverityLevel.Error):
_telemetry.TrackException(
new Exception(eventData.Exception ?? eventData.Message),
properties);
break;
case nameof(SeverityLevel.Warning):
case nameof(SeverityLevel.Information):
_telemetry.TrackEvent(
$"EverTask.{eventData.Severity}",
properties);
break;
}
return Task.CompletedTask;
}
}
Prometheus Metrics
using Prometheus;
public class PrometheusMonitor
{
private static readonly Counter TasksStarted = Metrics
.CreateCounter("evertask_tasks_started_total", "Total tasks started");
private static readonly Counter TasksCompleted = Metrics
.CreateCounter("evertask_tasks_completed_total", "Total tasks completed");
private static readonly Counter TasksFailed = Metrics
.CreateCounter("evertask_tasks_failed_total", "Total tasks failed",
new CounterConfiguration { LabelNames = new[] { "task_type" } });
private static readonly Histogram TaskDuration = Metrics
.CreateHistogram("evertask_task_duration_seconds", "Task execution duration");
public PrometheusMonitor(IEverTaskWorkerExecutor executor)
{
executor.TaskEventOccurredAsync += OnTaskEventAsync;
}
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
if (eventData.Message.Contains("started"))
{
TasksStarted.Inc();
}
else if (eventData.Message.Contains("completed"))
{
TasksCompleted.Inc();
}
else if (eventData.Severity == nameof(SeverityLevel.Error))
{
TasksFailed.WithLabels(eventData.TaskType).Inc();
}
return Task.CompletedTask;
}
}
Email Alerts
public class EmailAlertMonitor
{
private readonly IEmailService _emailService;
private readonly ILogger<EmailAlertMonitor> _logger;
public EmailAlertMonitor(
IEverTaskWorkerExecutor executor,
IEmailService emailService,
ILogger<EmailAlertMonitor> logger)
{
_emailService = emailService;
_logger = logger;
executor.TaskEventOccurredAsync += OnTaskEventAsync;
}
private async Task OnTaskEventAsync(EverTaskEventData eventData)
{
// Only alert on critical task failures
if (eventData.Severity == nameof(SeverityLevel.Error) &&
IsCriticalTask(eventData.TaskType))
{
try
{
await _emailService.SendAlertAsync(
to: "ops@example.com",
subject: $"Critical Task Failure: {eventData.TaskType}",
body: $@"
Task ID: {eventData.TaskId}
Task Type: {eventData.TaskType}
Time: {eventData.EventDateUtc}
Message: {eventData.Message}
Exception: {eventData.Exception}
");
_logger.LogInformation(
"Alert sent for failed task {TaskId}",
eventData.TaskId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send alert email");
}
}
}
private bool IsCriticalTask(string taskType)
{
return taskType.Contains("Payment") ||
taskType.Contains("Order") ||
taskType.Contains("Billing");
}
}
Slack Notifications
public class SlackMonitor
{
private readonly HttpClient _httpClient;
private readonly string _webhookUrl;
public SlackMonitor(
IEverTaskWorkerExecutor executor,
IConfiguration configuration)
{
_httpClient = new HttpClient();
_webhookUrl = configuration["Slack:WebhookUrl"];
executor.TaskEventOccurredAsync += OnTaskEventAsync;
}
private async Task OnTaskEventAsync(EverTaskEventData eventData)
{
if (eventData.Severity == nameof(SeverityLevel.Error))
{
var payload = new
{
text = $"❌ Task Failed: {eventData.TaskType}",
attachments = new[]
{
new
{
color = "danger",
fields = new[]
{
new { title = "Task ID", value = eventData.TaskId.ToString(), @short = true },
new { title = "Time", value = eventData.EventDateUtc.ToString(), @short = true },
new { title = "Message", value = eventData.Message },
new { title = "Exception", value = eventData.Exception ?? "N/A" }
}
}
}
};
await _httpClient.PostAsJsonAsync(_webhookUrl, payload);
}
}
}
Logging Integration
EverTask uses standard .NET logging, so it works with whatever logging setup you already have:
Basic Logging
// EverTask automatically logs to ILogger
builder.Services.AddEverTask(opt =>
{
opt.RegisterTasksFromAssembly(typeof(Program).Assembly);
});
// Logs appear in your configured sinks (Console, File, etc.)
Serilog Integration
dotnet add package EverTask.Logging.Serilog
builder.Services.AddEverTask(opt =>
{
opt.RegisterTasksFromAssembly(typeof(Program).Assembly);
})
.AddSqlServerStorage(connectionString)
.AddSerilog(opt =>
opt.ReadFrom.Configuration(
builder.Configuration,
new ConfigurationReaderOptions { SectionName = "EverTaskSerilog" }));
appsettings.json:
{
"EverTaskSerilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning"
}
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "Logs/evertask-.txt",
"rollingInterval": "Day"
}
},
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341"
}
}
]
}
}
Best Practices
1. Monitor Critical Tasks
You don’t need to alert on everything. Focus on tasks that matter most to your business:
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
if (IsCriticalTask(eventData.TaskType) &&
eventData.Severity == nameof(SeverityLevel.Error))
{
// Immediate alerting for critical failures
SendImmediateAlert(eventData);
}
return Task.CompletedTask;
}
2. Aggregate Metrics
Instead of just logging individual events, track aggregate metrics to spot trends and performance issues:
public class MetricsAggregator
{
private int _completedCount;
private int _failedCount;
private readonly ConcurrentDictionary<string, int> _failuresByType = new();
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
if (eventData.Message.Contains("completed"))
{
Interlocked.Increment(ref _completedCount);
}
else if (eventData.Severity == nameof(SeverityLevel.Error))
{
Interlocked.Increment(ref _failedCount);
_failuresByType.AddOrUpdate(eventData.TaskType, 1, (_, count) => count + 1);
}
return Task.CompletedTask;
}
public (int Completed, int Failed, Dictionary<string, int> FailuresByType) GetMetrics()
{
return (_completedCount, _failedCount, new Dictionary<string, int>(_failuresByType));
}
}
3. Avoid Blocking Operations
Keep event handlers fast. If you need to do something slow (like sending an alert), fire and forget it:
// Good: fire-and-forget alerting
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
if (eventData.Severity == nameof(SeverityLevel.Error))
{
_ = Task.Run(() => SendAlertAsync(eventData)); // Fire-and-forget
}
return Task.CompletedTask;
}
// Bad: blocking the event pipeline
private async Task OnTaskEventAsync(EverTaskEventData eventData)
{
if (eventData.Severity == nameof(SeverityLevel.Error))
{
await SendAlertAsync(eventData); // This blocks other events from processing
}
}
4. Handle Event Handler Failures
Wrap your event handlers in try-catch blocks so a failing monitor doesn’t bring down your entire application:
private async Task OnTaskEventAsync(EverTaskEventData eventData)
{
try
{
await ProcessEventAsync(eventData);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in event handler");
// Don't let event handler failures crash the application
}
}
5. Use Structured Logging
Always use structured logging with named parameters rather than string interpolation. This makes your logs queryable and easier to analyze:
private Task OnTaskEventAsync(EverTaskEventData eventData)
{
_logger.LogInformation(
"Task event: {TaskId} {TaskType} {Severity} {Message}",
eventData.TaskId,
eventData.TaskType,
eventData.Severity,
eventData.Message);
return Task.CompletedTask;
}
Web Dashboard
For a complete monitoring solution with web UI and REST API, see the Monitoring Dashboard guide.
The monitoring dashboard provides:
- REST API for task querying and analytics
- Embedded React dashboard
- Real-time updates via SignalR
- Task filtering, sorting, and detailed views
- Statistics and performance analytics
- Queue metrics and health monitoring
Quick setup:
builder.Services.AddEverTask(opt =>
{
opt.RegisterTasksFromAssembly(typeof(Program).Assembly);
})
.AddSqlServerStorage(connectionString)
.AddMonitoringApi(options =>
{
options.EnableUI = true;
options.Username = "admin";
options.Password = "admin";
});
var app = builder.Build();
app.MapEverTaskApi();
// Dashboard: http://localhost:5000/evertask-monitoring
Future Monitoring Options
We’re planning to add:
- Sentry Crons - Automatic cron monitoring for recurring tasks
- OpenTelemetry - Distributed tracing and metrics
- Health Checks - Built-in health check endpoints
Next Steps
- Monitoring Dashboard - Web dashboard, REST API, and real-time monitoring
- Resilience - Configure retry policies and error handling
- Storage - Query task status and history from storage
- Configuration Reference - All configuration options
- Architecture - How monitoring works internally