Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Monitoring Windows Server Backup Across Domain Controllers: A PowerShell Solution

I manage several domain controllers, and like many administrators, I rely on Windows Server Backup for regular backups. The problem is that checking backup status across multiple servers becomes tedious quickly. Logging into each server, opening Event Viewer, and manually checking for backup events is time-consuming and error-prone. I needed a way to see the backup status of all domain controllers at a glance.

Visuals of Website

This is the resulting website from the live backup data, you can clearly see that one Domain Controller has a backup issue with the error shown below in the this blog post:

Understanding Windows Server Backup Events

Windows Server Backup writes specific events to the Microsoft-Windows-Backup operational log when backups run. There are three event IDs I need to track:

  • Event ID 4: Backup completed successfully
  • Event ID 5: Backup failed with an error
  • Event ID 7: Backup completed but with errors

Event ID 4 is straightforward - it simply states "The backup operation has finished successfully." This is what I want to see.

Event ID 5 indicates a complete failure. The message typically looks like this:

The backup operation that started at '2025-10-07T19:00:06.212053500Z' has failed 
with following error code '0x80780102' (The system writer is not found in the backup.). 
Please review the event details for a solution, and then rerun the backup operation 
once the issue is resolved.

Event ID 7 sits somewhere in between - the backup ran, but something went wrong. The message format is similar to Event ID 5, but the operation technically completed despite the errors.

Querying Domain Controllers

The first script queries all domain controllers in the domain for these three event IDs. I start by retrieving the list of domain controllers:

$DomainControllers = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName

Then I loop through each domain controller and query the backup log:

$Events = Get-WinEvent -ComputerName $DC -FilterHashtable @{
    LogName = 'Microsoft-Windows-Backup'
    ID = 4,5,7
} -ErrorAction Stop

Using -FilterHashtable is more efficient than filtering after retrieval, especially when dealing with large event logs. The Get-WinEvent cmdlet connects to each remote server and pulls only the events I need.

For each event, I determine the status based on the event ID:

switch ($Event.Id) {
    4 { $Status = 'Success' }
    5 { $Status = 'Failed' }
    7 { $Status = 'Warning' }
    default { $Status = 'Unknown' }
}

Event IDs 5 and 7 contain error codes in their messages, which I extract using a regular expression:

if ($Event.Id -eq 5 -or $Event.Id -eq 7) {
    if ($Event.Message -match "'(0x[0-9A-Fa-f]+)'") {
        $ErrorCode = $matches[1]
    }
}

The script collects all this information and exports it to a CSV file with a timestamp:

$Results | Sort-Object TimeCreated -Descending | Export-Csv -Path $OutputFile -NoTypeInformation

I sort by TimeCreated in descending order so the most recent backups appear first in the CSV.

Generating the HTML Dashboard

The second script reads the CSV file and generates an HTML dashboard. I find the latest CSV file automatically:

$LatestCSV = Get-ChildItem -Path "DC_Backup_Events_*.csv" | 
    Sort-Object LastWriteTime -Descending | 
    Select-Object -First 1

Then I import the data and convert it to JSON:

$BackupData = Import-Csv -Path $LatestCSV.FullName
$JsonData = $BackupData | ConvertTo-Json -Compress

The JSON data gets embedded directly into the HTML file. This creates a completely self-contained dashboard - no external dependencies, no web server required. Just open the HTML file in a browser.

The Grid Layout

I originally tried a timeline view with dots and dates, but it was difficult to scan quickly. The grid format works much better. Each domain controller gets its own card, and each backup is represented by a colored square:

.servers-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
}

The auto-fit and minmax combination creates a responsive layout that adjusts based on screen width. On a wide monitor, I can see multiple servers side-by-side. On a laptop, they stack vertically.

Inside each card, the backups are displayed in a 7-column grid:

.backup-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 6px;
}

Seven columns gives a roughly weekly pattern, which makes it easier to spot when backups might have failed on a particular day of the week. Each backup is a square that maintains its aspect ratio:

.backup-dot {
    width: 100%;
    aspect-ratio: 1;
    border-radius: 4px;
}

Color Coding

The color scheme is simple: green for success, red for failure, orange for warnings. I use specific hex values that are easy to distinguish:

.backup-dot.success { background: #27ae60; }
.backup-dot.failure { background: #e74c3c; }
.backup-dot.warning { background: #f39c12; }

Handling the Date Format

PowerShell exports dates in UK format (DD/MM/YYYY), which JavaScript doesn't parse natively. I handle this by splitting the date string and reconstructing it:

const dateParts = item.TimeCreated.split(/[\s\/\:]/);
const parsedDate = new Date(
    parseInt(dateParts[2]), // year
    parseInt(dateParts[1]) - 1, // month (0-indexed)
    parseInt(dateParts[0]), // day
    parseInt(dateParts[3] || 0), // hour
    parseInt(dateParts[4] || 0), // minute
    parseInt(dateParts[5] || 0)  // second
);

The regex /[\s\/\:]/ splits on spaces, slashes, and colons, giving me an array of date components. JavaScript months are zero-indexed (January = 0), so I subtract 1 from the month value.

Tooltips on Hover

Each colored square displays a tooltip when you hover over it. I use CSS ::after pseudo-elements for this:

.backup-dot::after {
    content: attr(data-date);
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%) translateY(-5px);
    background: #2c3e50;
    color: white;
    padding: 6px 10px;
    border-radius: 4px;
    opacity: 0;
}

.backup-dot:hover::after {
    opacity: 1;
}

The tooltip content comes from the data-date attribute on each element. This approach is cleaner than JavaScript-based tooltips and doesn't require any libraries.

Filtering

The dashboard includes three filters: server, status, and a search box. The status filter is straightforward:

if (statusFilterValue === 'success') {
    filteredData = filteredData.filter(item => item.isSuccess);
} else if (statusFilterValue === 'failure') {
    filteredData = filteredData.filter(item => item.isFailure);
} else if (statusFilterValue === 'warning') {
    filteredData = filteredData.filter(item => item.isWarning);
}

The search box searches across server names, status values, and messages:

if (searchFilterValue) {
    filteredData = filteredData.filter(item => 
        item.Message.toLowerCase().includes(searchFilterValue) ||
        item.Status.toLowerCase().includes(searchFilterValue) ||
        item.ServerName.toLowerCase().includes(searchFilterValue)
    );
}

Every time a filter changes, the applyFilters() function recalculates the statistics and re-renders the display. This happens instantly since all the data is already loaded in memory.

The Complete Workflow

Running the monitoring system is a two-step process:

  1. Run the query script to collect backup events from all domain controllers:

    .\get-backup-report.ps1
    
  2. Generate the HTML dashboard from the CSV data:

    .\Generate-Backup-Dashboard.ps1
    

The dashboard file is timestamped, so I can keep historical snapshots if needed. Each HTML file is completely standalone - I can email it, archive it, or open it on any computer with a web browser.

Conclusion

This solution emerged from a simple frustration with checking multiple servers manually. The combination of PowerShell for data collection and a static HTML dashboard for visualization works well because it requires no infrastructure - no databases, no web servers, no external dependencies.

The grid layout was an iterative improvement. The initial timeline view looked clean but proved impractical when scanning dozens of backup events. The grid format, with its consistent spacing and color coding, makes patterns immediately visible. A streak of red squares stands out and a white gap indicates a server that hasn't been backed up recently

Previous Post Next Post

نموذج الاتصال