# 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 ==="