Citrix Profile Management Log Analyzer

Checks that Citrix Profile Management logging is enabled and reads the log file, filtered to the specific user session for that machine.
Version 1.1.9
Created on 2025-07-28
Modified on 2025-09-05
Created by Trentent Tye
Downloads: 8

The Script Copy Script Copied to clipboard
# Citrix UserProfileManager Logging Configuration Check and Log Parser Script

param(
    [Parameter(Mandatory=$true)]
    [string]$Username,
    
    [Parameter(Mandatory=$true)]
    [int]$SessionId,
    
    [Parameter(Mandatory=$false)]
    [datetime]$LogonTime
)
## TTYE - 2025-08-17 - First release version

# Set console window width to 1080 columns
try {
    # Altering the size of the PS Buffer
    $PSWindow = (Get-Host).UI.RawUI
    $WideDimensions = $PSWindow.BufferSize
    $WideDimensions.Width = 400
    $PSWindow.BufferSize = $WideDimensions
} catch {
    Write-Warning "Could not set console width to 400 columns: $($_.Exception.Message)"
}

# Validate and parse username format
if ($Username -notmatch '^(.+)\\(.+)$') {
    Write-Error "Username must be in format 'DOMAIN\samaccountname'. Example: 'JUPITERLAB\amttye'"
    exit 1
}

$Domain = $Matches[1]
$SamAccountName = $Matches[2]

Write-Verbose "=== Parameters ==="
Write-Verbose "Domain: $Domain"
Write-Verbose "SamAccountName: $SamAccountName"
Write-Verbose "SessionId: $SessionId"
if ($LogonTime) {
    Write-Verbose "LogonTime: $LogonTime"
} else {
    Write-Verbose "LogonTime: Not specified (showing all entries)"
}
Write-Verbose ""

# Define registry path
$RegistryPath = "HKLM:\Software\Policies\Citrix\UserProfileManager"

# Function to check registry value
function Test-RegistryValue {
    param(
        [string]$Path,
        [string]$ValueName,
        [string]$ExpectedData
    )
    
    try {
        if (Test-Path $Path) {
            $actualValue = Get-ItemProperty -Path $Path -Name $ValueName -ErrorAction SilentlyContinue
            if ($actualValue) {
                $actualData = $actualValue.$ValueName
                if ($actualData -eq [int]$ExpectedData) {
                    Write-Verbose "GOOD - $ValueName is correctly configured (Value: $actualData)"
                    return $true
                } else {
                    Write-Verbose "MISSED - $ValueName has incorrect value. Expected: $ExpectedData, Actual: $actualData"
                    return $false
                }
            } else {
                Write-Verbose "MISSED - $ValueName not found in registry"
                return $false
            }
        } else {
            Write-Verbose "MISSED - Registry path $Path does not exist"
            return $false
        }
    } catch {
        Write-Verbose "MISSED - Error checking $ValueName`: $($_.Exception.Message)"
        return $false
    }
}

Write-Verbose "=== Citrix UserProfileManager Logging Configuration Check ==="
Write-Verbose ""

# Step 1: Check logging configuration registry values
Write-Verbose "1. Checking logging configuration registry values..."

$LoggingValues = @(
    @{Name="LoggingEnabled"; Expected="1"},
    @{Name="LogLevelUserName"; Expected="1"},
    @{Name="LogLevelWarnings"; Expected="1"},
    @{Name="LogLevelInformation"; Expected="1"},
    @{Name="LogLevelFileSystemActions"; Expected="1"}
)

$AllConfigsCorrect = $true
foreach ($value in $LoggingValues) {
    $result = Test-RegistryValue -Path $RegistryPath -ValueName $value.Name -ExpectedData $value.Expected
    if (-not $result) {
        $AllConfigsCorrect = $false
    }
}

Write-Verbose ""
if ($AllConfigsCorrect) {
    Write-Verbose "All logging configurations are correct!"
} else {
    Write-Host "Some logging configurations are incorrect or missing!" -ForegroundColor Red
}

Write-Verbose ""

# Step 2: Check for PathToLogFile registry value
Write-Verbose "2. Checking PathToLogFile registry value..."

try {
    $PathToLogFileValue = Get-ItemProperty -Path $RegistryPath -Name "PathToLogFile" -ErrorAction SilentlyContinue
    if ($PathToLogFileValue) {
        $PathToLogFile = $PathToLogFileValue.PathToLogFile
        Write-Verbose "GOOD - PathToLogFile found in registry: $PathToLogFile"
    } else {
        $PathToLogFile = "$env:windir\System32\LogFiles\UserProfileManager"
        Write-Verbose "GOOD - PathToLogFile not found in registry, using default: $PathToLogFile"
    }
} catch {
    $PathToLogFile = "$env:windir\System32\LogFiles\UserProfileManager"
    Write-Verbose "GOOD - Error reading PathToLogFile, using default: $PathToLogFile"
}

Write-Verbose ""

# Step 3: Check for log file existence
Write-Verbose "3. Checking for Citrix log files..."

$LogFilePattern = "*$($env:computername)_pm.log"
Write-Verbose "Looking for files matching pattern: $LogFilePattern in $PathToLogFile"

if (-not (Test-Path $PathToLogFile)) {
    Write-Error "Log directory does not exist: $PathToLogFile"
    exit 1
}

try {
    $LogFiles = Get-ChildItem -Path $PathToLogFile -Filter $LogFilePattern -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending
    
    if ($LogFiles.Count -eq 0) {
        Write-Error "Citrix Log File not found at `"$PathToLogFile`""
        exit 1
    } else {
        $MostRecentLogFile = $LogFiles[0]
        Write-Verbose "GOOD - Found $($LogFiles.Count) log file(s). Most recent: $($MostRecentLogFile.Name)"
        Write-Verbose "  Last modified: $($MostRecentLogFile.LastWriteTime)"
    }
} catch {
    Write-Error "Error searching for log files: $($_.Exception.Message)"
    exit 1
}

Write-Verbose ""

# Step 4: Read and parse the most recent log file
Write-Verbose "4. Reading and parsing the most recent log file..."

try {
    $LogContent = Get-Content -Path $MostRecentLogFile.FullName -ErrorAction Stop
    Write-Verbose "GOOD - Successfully read log file with $($LogContent.Count) lines"
    
    # Parse log entries into objects
    $LogEntries = @()
    
    foreach ($line in $LogContent) {
        if ([string]::IsNullOrWhiteSpace($line)) {
            continue
        }
        
        # Split by semicolon delimiter
        $fields = $line -split ';'
        
        # Ensure we have enough fields (at least 8 for the expected format)
        if ($fields.Count -ge 8) {
            $logEntry = [PSCustomObject]@{
                Date = $fields[0]
                Time = $fields[1]
                LoggingLevel = $fields[2]
                Domain = $fields[3]
                Username = $fields[4]
                UserSession = $fields[5]
                PID = $fields[6]
                Message = ($fields[7..($fields.Count-1)] -join ';')  # Join remaining fields as message might contain semicolons
            }
            $LogEntries += $logEntry
        } else {
            Write-Warning "Skipping malformed line: $line"
        }
    }
    
    Write-Verbose "GOOD - Successfully parsed $($LogEntries.Count) log entries"
    
    # Apply filtering based on parameters
    Write-Verbose ""
    Write-Verbose "5. Filtering log entries based on provided parameters..."
    
    # Filter by Domain, Username, and SessionId
    $FilteredEntries = $LogEntries | Where-Object {
        $_.Domain -eq $Domain -and 
        $_.Username -eq $SamAccountName -and 
        $_.UserSession -eq $SessionId.ToString()
    }
    
    Write-Verbose "GOOD - Found $($FilteredEntries.Count) entries matching Domain: $Domain, Username: $SamAccountName, SessionId: $SessionId"
    
    # Apply LogonTime filter if provided
    if ($LogonTime) {
        $FilteredEntriesWithTime = @()
        
        foreach ($entry in $FilteredEntries) {
            try {
                # Combine date and time to create datetime object
                $entryDateTime = [datetime]::ParseExact("$($entry.Date) $($entry.Time)", "yyyy-MM-dd HH:mm:ss.fff", $null)
                
                if ($entryDateTime -gt $LogonTime) {
                    $FilteredEntriesWithTime += $entry
                }
            } catch {
                Write-Warning "Could not parse datetime for entry: $($entry.Date) $($entry.Time)"
            }
        }
        
        $FilteredEntries = $FilteredEntriesWithTime
        Write-Verbose "GOOD - After applying LogonTime filter (after $LogonTime): $($FilteredEntries.Count) entries"
    }
    
    # Display filtered results
    Write-Verbose ""
    if ($FilteredEntries.Count -gt 0) {
        Write-Verbose "=== Filtered Log Entries ==="
        $FilteredEntries | Format-Table -AutoSize -Wrap
        
        # Display summary of filtered results
        Write-Verbose ""
        Write-Verbose "Filtered Results Summary:"
        Write-Verbose "Total matching entries: $($FilteredEntries.Count)"
        if ($FilteredEntries.Count -gt 0) {
            Write-Verbose "Date range: $($FilteredEntries[0].Date) to $(($FilteredEntries | Sort-Object Date)[-1].Date)"
            Write-Verbose "Logging levels: $(($FilteredEntries | Group-Object LoggingLevel | ForEach-Object { "$($_.Name) ($($_.Count))" }) -join ', ')"
        }
    } else {
        Write-Verbose "No log entries found matching the specified criteria:"
        Write-Verbose "  Domain: $Domain"
        Write-Verbose "  Username: $SamAccountName"
        Write-Verbose "  SessionId: $SessionId"
        if ($LogonTime) {
            Write-Verbose "  After LogonTime: $LogonTime"
        }
    }
    
    # Export filtered entries to global scope
    $Global:FilteredLogEntries = $FilteredEntries
    
    Write-Verbose ""
    Write-Verbose "Filtered log entries are available in the `$FilteredLogEntries variable."
    
} catch {
    Write-Error "Error reading or parsing log file: $($_.Exception.Message)"
    exit 1
}

# Export LogEntries and FilteredLogEntries to global scope
$Global:LogEntries = $LogEntries
$Global:FilteredLogEntries = $FilteredEntries

Write-Verbose ""
Write-Verbose "=== Script completed successfully ==="