ControlUp Disk Monitor Log Analyzer 24 Hours

This script will analyze the log file of today created by ControlUp Disk Monitor and created a CSV output
Version 4.3.26
Created on 2025-03-30
Modified on 2025-11-28
Created by Chris Twiest
Downloads: 472

The Script Copy Script Copied to clipboard
#Requires -Version 5.1

<#
.SYNOPSIS
    Extracts and processes Disk I/O activity from ControlUp Disk Monitor log files and exports the top entries to text or csv
.DESCRIPTION
    https://support.controlup.com/v1/docs/disk-monitor-overview
.PARAMETER dummy1
    (Ignored) Placeholder parameter, passed by ControlUp for compatibility.

.PARAMETER top
    Specifies the maximum number of results to display or export. Default is 20.

.PARAMETER last
    Specifies the time window to analyze (e.g., '24h' for last 24 hours, '10m' for last 10 minutes). Default is '24h'.

.PARAMETER includeUNC
    Determines whether to include UNC (network) paths in the results. Accepts 'Yes', 'No', 'True', or 'False'. Default is 'No'.

.PARAMETER includeFSlogix
    Determines whether to include FSLogix VHD file activity in the results. Accepts 'Yes', 'No', 'True', or 'False'. Default is 'Yes'.

.PARAMETER csvOutputFile
    Specifies the path to a CSV file where results will be exported. If not specified, results are displayed in the console.
    Folder willbe created if it does not exist.
    If the file already exists, it will be overwritten.

.PARAMETER sortBy
    Specifies the property to sort results by. Default is 'TransferSizeMB'.

.PARAMETER ascending
    If specified, sorts results in ascending order. By default, results are sorted in descending order.

.PARAMETER delimiter
    Specifies the delimiter to use when exporting to CSV. Default is ','.

.PARAMETER LogFilePattern
    Specifies the file name pattern to match log files. Default is 'ActivitiesSummary*'.

.PARAMETER LogDirectory
    Specifies the directory containing log files. If not specified, the script attempts to auto-detect the location.

.PARAMETER deleteOlderThanDays
    Deletes log files older than the specified number of days. Default is 0 (no deletion).

.PARAMETER cu4d
    Switch for manual debugging; forces ControlUp for Desktops (CU4D) mode.

.PARAMETER raw
    If specified, outputs raw objects instead of formatted output or CSV/JSON.

.NOTES
    2025/01/01  Chris Twiest  Original version
    2025/05/20  Guy Leech     Added support for FSlogix (where filename is VHD*Filename)
    2025/06/05  Guy Leech     Fixed issue when there are multiple log files to process
    2025/06/16  Guy Leech     Added parameters for filtering UNC and/or FSlogix.
    2025/06/17  Guy Leech     Added code to deal with non-default log file locations
    2025/06/19  Guy Leech     Fixed bug in log file delete logic
    2025/06/24  Guy Leech     Added system drive free disk space to output. Removed property filtering on csv export
#>

[CmdletBinding()]

Param
(
    [string]$dummy1 , ## passed by CU4D, ignored
    [int]$top = 20 ,
    [string]$last = '24h' ,
    [ValidateSet('Yes','No','True','False')]
    [string]$includeUNC = 'No' ,
    [ValidateSet('Yes','No','True','False')]
    [string]$includeFSlogix = 'Yes' ,
    [string]$csvOutputFile ,
    [string]$sortBy = 'TransferSizeMB' ,
    [switch]$ascending ,
    [string]$delimiter = ',' ,
    [string]$LogFilePattern = "ActivitiesSummary*" ,
    [string]$LogDirectory ,
    [decimal]$deleteOlderThanDays = 7 , # Delete  logs older then x days if found
    [switch]$cu4d , ## for manual debugging only so can force CU4D mode
    [switch]$raw
)

######## add regular expressions here which when matched to the full path name of the file will exclude them from the results #################
[string[]]$excludedPaths = @(
    "^$env:SystemDrive\\pagefile\.sys$"
    "^$env:SystemDrive\\\`$" ## NTFS metadata files
    "^$env:SystemDrive\\Windows\\System32\\config\\" ## registry hives (can't use SystemRoot as \ would need escaping)
)

## figure out what ControlUp product is calling us so we can behave accordingly
[int]$parentProcessPid = (Get-CimInstance -ClassName win32_process -Filter "ProcessId = '$pid'" -Verbose:$false).ParentProcessId
[string]$thisProcessName = (Get-Process -Id $pid).Name
[string]$parentProcessName = $null
[bool]$isControlUpForDesktops = $false
$parentProcess = $null

if( $parentProcessPid -ge 4 )
{
    $parentProcess = Get-Process -Id $parentProcessPid -ErrorAction SilentlyContinue
    $parentProcessName = $parentProcess | Select-Object -ExpandProperty Name -ErrorAction SilentlyContinue
}

$isControlUpForDesktops = ( $parentProcessName -ieq 'SIPAgent' ) -or $cu4d

Write-Verbose "Pid $pid, parent $parentProcessName, CU4D $isControlUpForDesktops"

if( $isControlUpForDesktops )
{
    try
    {
        [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # This line is added to help with Unicode characters
    }
    catch
    {
        ## probably because running in ISE
    }
}
else
{
    $VerbosePreference = $(if( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'SilentlyContinue' })
    $DebugPreference = $(if( $PSBoundParameters[ 'debug' ] ) { $DebugPreference } else { 'SilentlyContinue' })
    $ErrorActionPreference = $(if( $PSBoundParameters[ 'erroraction' ] ) { $ErrorActionPreference } else { 'Stop' })
    $ProgressPreference = 'SilentlyContinue'

    try
    {
        [int]$outputWidth = 400
        if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ( $WideDimensions = $PSWindow.BufferSize ) )
        {
            $WideDimensions.Width = $outputWidth
            $PSWindow.BufferSize = $WideDimensions
        }
        else {
            Write-Warning "PROBLEM GETTING WIDTH"
        }
    }
    catch
    {
        ## nothing much we can do but not fatal, just output possibly wrapping prematurely
        Write-Warning "PROBLEM SETTING WIDTH : $_"
    }
}

[datetime]$startDate = [datetime]::Today
if( $last.Trim('" ') -match '^([\d]*\.?[\d]*)\s*([smhdwy])?' ) { ## 24h 10m
    [int]$multiplier = 3600 ## default is hours
    if( -Not [string]::IsNullOrEmpty( $matches[2] ) ) {
        switch( $matches[2] )
        {
            "s" { $multiplier = 1 }
            "m" { $multiplier = 60 }
            "h" { $multiplier = 3600 }
            "d" { $multiplier = 86400 }
            "w" { $multiplier = 86400 * 7 }
            "y" { $multiplier = 86400 * 365 }
            default { Throw "Unknown multiplier `"$($matches[2])`"" }
        }
    }
    [decimal]$secondsBack = ($matches[1] -as [decimal]) * $multiplier
    if( $secondsBack -le 0 ) {
        Throw "$last resulted in invalid offset $secondsBack"
    }
    $startDate = [datetime]::Now.AddSeconds( -$secondsBack )
    Write-Verbose "Start date is $($startDate.ToString('G'))"
}
else {
    Throw "Cannot interpret -last $last"
}

[string[]]$displayProperties = @( 'Timestamp', 'ProcessName', 'UserName', 'TransferSizeMB', 'FileName' )
[bool]$updatedProperties = $false
if( [string]::IsNullOrEmpty( $LogDirectory )) {
    $service = Get-CimInstance -ClassName win32_service -filter "name = 'ControlUp Disk Monitor'" -Verbose:$false -ErrorAction SilentlyContinue
    if( $null -eq $service ) {
        Write-Warning -Message "Failed to find disk monitor service to get log file folder, is this the correct machine & if so you may need to use -LogDirectory"
    }
    else {
        if( $service.PathName -match  '^"([^"]+)"' -or $service.PathName -match '^([^\s]+)' ) {
            $serviceFolder = Split-Path -Path $matches[1] -Parent
            if( Test-Path -Path $serviceFolder -PathType Container ) {
                $log4netFile = Join-Path -Path $serviceFolder -ChildPath 'log4net.config'
                if( -Not ( Test-Path -Path $log4netFile -PathType Leaf ) ) {
                    Write-Warning "Log2net config `"$log4netFile`" not found"
                }
                else {
                    [xml]$logConfig = Get-Content -Path $log4netFile -ErrorAction Continue
                    if( $null -ne $logConfig ) {
                        [string]$logFileBase = ($logConfig.log4net.appender|Where-Object name -ieq 'SummaryRollingFileAppender').file.value
                        if( [string]::IsNullOrEmpty( $logFileBase ) ) {
                            Write-Warning "Failed to find SummaryRollingFileAppender section log4net config file $log4netFile"
                        }
                        else {
                            $LogDirectory = Split-Path -Path $logFileBase -Parent
                            if( -Not ( Test-Path -Path $LogDirectory -PathType Container ) ) {
                                Write-Warning "Log directory `"$LogDirectory`" from log4net config file does not exist"
                                $LogDirectory = $null
                            }
                            else {
                                if( -Not $PSBoundParameters[ 'LogFilePattern' ] ) {
                                    $LogFilePattern = (Split-Path -Path $logFileBase -Leaf) + '*'
                                }
                                Write-Verbose "Using log directory $LogDirectory and pattern $LogFilePattern from $log4netFile"
                            }
                        }
                    }
                }
            }
            else {
                Write-Warning "Folder `"serviceFolder`" for disk monitor service deso not exist. Fetched from $($service.Pathname)"
            }
        }
        else {
            Write-Warning -Message "Failed to determine folder from service path $($service.Pathname)"
        }
    }
    if( [string]::IsNullOrEmpty( $LogDirectory )) {
        $LogDirectory = Join-Path -Path ([Environment]::GetFolderPath( [Environment+SpecialFolder]::CommonApplicationData )) -ChildPath 'ControlUp\DiskMonitor'
    }
}

# Get all matching log files
$AllLogFiles = @( Get-ChildItem -Path $LogDirectory -Filter $LogFilePattern -File  )

# Filter files last written to within the required time period
$RecentLogFiles = @( $AllLogFiles | Where-Object LastWriteTime -ge $startDate | Sort-Object LastWriteTime -Descending )

# Filter and delete files older than x days
if( $deleteOlderThanDays -gt 0 ) {
    [datetime]$CutoffDate = (Get-Date).AddDays( -$deleteOlderThanDays )
    $OldLogFiles = $AllLogFiles | Where-Object CreationTime -lt $CutoffDate
    if ($OldLogFiles) {
        Write-Output "INFO: Deleting $(($OldLogFiles).Count) log file(s) older than 7 days..."
        $OldLogFiles | ForEach-Object {
            Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
            if( -Not $raw ) {
                Write-Output "Deleted: $($_.FullName)"
            }
        }
    }
}

# Check if there are logs created in the time window
if ( $null -eq $RecentLogFiles -or $RecentLogFiles.Count -eq 0 ) {
    Throw "ERROR: No log files created since $($startDate.ToString('G')) in $LogDirectory"
}

[int]$diskFreeSpaceGB = [math]::Round( (Get-PSDrive -Name $env:SystemDrive[0]).Free / 1GB , 2 )

try {
    if( -Not $raw ) {
        Write-Output "Reading log file(s): $($RecentLogFiles.Name -join ' , ') for activity since $($startDate.ToString('G')), disk free space $diskFreeSpaceGB GB"
    }
    # Initialize a list to store results efficiently
    $Results = @( ForEach( $file in $RecentLogFiles.FullName ) {
        ##                                                                    1               2                 3                  4               5              6                            7
        [IO.File]::ReadLines( $file ) | Where-Object { $_ -match 'Timestamp:(.*?), Interval:(\d+), Process Id:(\d+), ProcessName:(.*?), UserName:(.*?), FileName:(.*?), TransferSize \(MB\):([\d,\.]+)' } | ForEach-Object {
            [string]$fileName = $matches[6].Trim()
            [bool]$excluded = $false
            ForEach( $pattern in $excludedPaths ) {
                if( $excluded = ( $fileName -match $pattern )) {
                    break 
                }
            }
            if( -Not $excluded ) {
                $timestamp = $( if( $timestamp = $matches[1] -as [datetime] ) { $timestamp } else { [datetime]$matches[1] })
                $excluded = $timestamp -lt $startDate
            }
            if( -Not $excluded ) {
                $object = [PSCustomObject]@{
                    Timestamp      = $timestamp
                    Interval       = [int]$matches[2]
                    ProcessId      = [int]$matches[3]
                    ProcessName    = $matches[4].Trim()
                    UserName       = $matches[5].Trim()
                    FileName       = $fileName
                    TransferSizeMB = [math]::Round(($matches[7] -replace ",", ".") -as [double], 2)
                    FreeDiskSpace  = $diskFreeSpaceGB
                }
                ## file name is probably FSlogix if VHD*FileName
                if( $object.FileName -match '^(.+)\*(.+)$' ) {
                    if(  $includeFSlogix -eq 'Yes' -or $includeFSlogix -ieq 'True' )  {
                        Add-Member -InputObject $object -Force -NotePropertyMembers @{
                            VHD = $matches[1]
                            FileName = $matches[2]
                        }
                        if( -Not $updatedProperties ) {
                            $displayProperties += 'VHD'
                            $updatedProperties = $true
                        }
                    }
                    else {
                        $object = $null
                    }
                }
                elseif( ( $includeUNC -eq 'No' -or $includeUNC -ieq 'False' ) -and $object.FileName -match '^\\\\[^\\]+\\' )
                {
                    $object = $null
                }

                if( $null -ne $object ) {
                    $object ## output to pipeline
                }
            }
        }
    })

    if( $Results.Count -eq 0 ) {
        Throw "No unfiltered results read from $LogFilePath"
    }

    # Sort and select top results
    $TopResults = @( $Results | Sort-Object -Property $sortBy -Descending:(-Not $ascending) | Select-Object -First $Top )

    if( $isControlUpForDesktops ) {
        Write-Output("### SIP DATA BEGINS ###") 
        ## deal with dates that export as /Date(1750065158000)/ which CU4D does not like
        $TopResults | Select-Object -ExcludeProperty Timestamp -Property @{ name = 'TimeStamp' ; expression = { $_.timestamp.ToString('o') } },* | ConvertTo-Json -Depth 2 -Compress
        Write-Output("### SIP DATA ENDS ###") 

        Write-Output("### SIP EVENT BEGINS ###") 
        Write-Output "Top $Top results exported to JSON"
        Write-Output("### SIP EVENT ENDS ###") 
    }
    elseif( $raw ){
        $TopResults
    }
    elseif( -Not [string]::IsNullOrEmpty( $csvOutputFile )) {
        $folder = Split-Path -Path $csvOutputFile -Parent
        if( -Not ( Test-Path -Path $folder -PathType Container ) ) {
            $null = New-Item -Path $folder -Force -ItemType Directory
        }
        $TopResults | Export-Csv -Path $csvOutputFile -NoTypeInformation -Delimiter $delimiter
        if( $? ) {
            Write-Output "Results written to $csvOutputFile"
        }
    }
    else {
        $TopResults | Select-Object -Property $displayProperties | Format-Table -AutoSize | Out-String -Width $outputWidth
    }

} catch {
    Write-Output "ERROR: An error occurred while processing the log file: $_"
    exit 1
}