<# .SYNOPSIS Read-only report of how Veeam jobs and sessions relate, for troubleshooting the collector. .DESCRIPTION The collector matches sessions to jobs by Session.JobId = Job.Id. When a job shows as "Unknown" on the dashboard, no session matched. This lists: 1. every job from Get-VBRJob and how many sessions matched it 2. sessions whose JobId matches no job (the runs that are going missing) 3. for jobs with no matching sessions, their type/link-related properties and methods 4. what other job cmdlets (e.g. Get-VBRBackupCopyJob) return 5. each job's schedule properties, which the collector reads for the next run Nothing is changed in Veeam or the dashboard. Output is also saved to a text file. .EXAMPLE .\Get-VeeamCollectorDiagnostics.ps1 .EXAMPLE .\Get-VeeamCollectorDiagnostics.ps1 -LookbackHours 168 #> [CmdletBinding()] param( [int]$LookbackHours = 48, [string]$OutFile = (Join-Path $env:TEMP 'veeam-collector-diagnostics.txt') ) $ErrorActionPreference = 'Stop' $LinkPattern = 'Parent|Linked|Worker|Child|Policy|Source|Target|Type|Cloud|Mode|Session' if (-not (Get-Command Get-VBRJob -ErrorAction SilentlyContinue)) { if (Get-Module -ListAvailable -Name Veeam.Backup.PowerShell) { Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue } else { Add-PSSnapin VeeamPSSnapin } } function Format-Value($Value) { if ($null -eq $Value) { return '' } $text = ($Value | ForEach-Object { [string]$_ }) -join ', ' if ($text.Length -gt 120) { $text = $text.Substring(0, 117) + '...' } return $text } # Property and method names matching $LinkPattern, with property values. Every read is guarded: # some Veeam properties throw when accessed. function Show-LinkMembers($Object, [string]$Pattern = $LinkPattern, [string]$Indent = ' ') { foreach ($p in $Object.PSObject.Properties | Where-Object { $_.Name -match $Pattern }) { $value = try { Format-Value $p.Value } catch { "" } '{0}{1} = {2}' -f $Indent, $p.Name, $value } $methods = @($Object | Get-Member -MemberType Methods -ErrorAction SilentlyContinue | Where-Object { $_.Name -match $Pattern } | ForEach-Object Name) if ($methods) { $Indent + 'methods: ' + ($methods -join ', ') } } function Get-Prop($Object, [string]$Name) { try { Format-Value $Object.$Name } catch { '' } } $report = & { $vbrVersion = try { (Get-Item (Join-Path ${env:ProgramFiles} 'Veeam\Backup and Replication\Backup\Veeam.Backup.Core.dll')).VersionInfo.ProductVersion } catch { 'unknown' } "Generated $(Get-Date -Format o) on $env:COMPUTERNAME, VBR $vbrVersion, PowerShell $($PSVersionTable.PSVersion)" "Lookback: $LookbackHours hours" '' $since = (Get-Date).AddHours(-$LookbackHours) $jobs = @(Get-VBRJob -WarningAction SilentlyContinue) $sessions = @(Get-VBRBackupSession -WarningAction SilentlyContinue | Where-Object { $_.CreationTime -ge $since }) $jobIds = @{} foreach ($j in $jobs) { $jobIds[$j.Id.ToString()] = $j } '== 1. Jobs from Get-VBRJob' $jobs | ForEach-Object { $id = $_.Id.ToString() [pscustomobject]@{ Name = $_.Name JobType = Get-Prop $_ 'JobType' TypeToString = Get-Prop $_ 'TypeToString' Id = $id MatchingSessions = @($sessions | Where-Object { $_.JobId.ToString() -eq $id }).Count } } | Format-Table -AutoSize | Out-String -Width 300 '== 2. Sessions whose JobId matches no job from Get-VBRJob' $orphans = @($sessions | Where-Object { -not $jobIds.ContainsKey($_.JobId.ToString()) }) if ($orphans.Count -eq 0) { ' (none)' } else { $orphans | Group-Object { $_.JobId.ToString() } | ForEach-Object { $latest = $_.Group | Sort-Object CreationTime | Select-Object -Last 1 [pscustomobject]@{ JobId = $_.Name JobName = Get-Prop $latest 'JobName' JobType = Get-Prop $latest 'JobType' Sessions = $_.Count LatestStart = $latest.CreationTime State = Get-Prop $latest 'State' Result = Get-Prop $latest 'Result' } } | Format-Table -AutoSize | Out-String -Width 300 ' Link-related members of one orphan session:' Show-LinkMembers $orphans[0] '' ' Job object for that session (via GetJob(), if available):' try { $owner = $orphans[0].GetJob() ' Name = {0}, Id = {1}' -f (Get-Prop $owner 'Name'), (Get-Prop $owner 'Id') Show-LinkMembers $owner } catch { " not available: $($_.Exception.Message)" } } '' '== 3. Jobs with no matching sessions in the window' $empty = @($jobs | Where-Object { $id = $_.Id.ToString(); -not ($sessions | Where-Object { $_.JobId.ToString() -eq $id }) }) if ($empty.Count -eq 0) { ' (none)' } foreach ($j in $empty) { " $($j.Name) [$($j.Id)]" Show-LinkMembers $j try { $last = $j.FindLastSession() if ($last) { ' FindLastSession(): JobId = {0}, JobName = {1}, Start = {2}, Result = {3}' -f ` (Get-Prop $last 'JobId'), (Get-Prop $last 'JobName'), $last.CreationTime, (Get-Prop $last 'Result') } else { ' FindLastSession(): ' } } catch { " FindLastSession(): not available: $($_.Exception.Message)" } '' } '== 4. Other job cmdlets' foreach ($cmd in 'Get-VBRBackupCopyJob', 'Get-VBRComputerBackupCopyJob', 'Get-VBRComputerBackupJob') { if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { " ${cmd}: not available"; continue } try { $found = @(& $cmd -WarningAction SilentlyContinue) " ${cmd}: $($found.Count) job(s)" foreach ($f in $found) { ' {0} [{1}] Type = {2}, Mode = {3}' -f (Get-Prop $f 'Name'), (Get-Prop $f 'Id'), (Get-Prop $f 'Type'), (Get-Prop $f 'Mode') } } catch { " ${cmd}: error: $($_.Exception.Message)" } } '' # The collector uses IsScheduleEnabled and ScheduleOptions.NextRun; show what else is there in # case this version keeps the next run somewhere different "== 5. Schedules (culture: $([Globalization.CultureInfo]::CurrentCulture.Name))" $SchedulePattern = 'Schedul|Next|Latest|Run|Enabled|Daily|Periodic|Monthly|Continuous|After|Option' foreach ($j in $jobs) { " $($j.Name) [$(Get-Prop $j 'JobType')]" Show-LinkMembers $j $SchedulePattern ' ' foreach ($source in 'ScheduleOptions', 'GetScheduleOptions()', 'Get-VBRJobScheduleOptions') { try { $opts = switch ($source) { 'ScheduleOptions' { $j.ScheduleOptions } 'GetScheduleOptions()' { $j.GetScheduleOptions() } 'Get-VBRJobScheduleOptions' { Get-VBRJobScheduleOptions -Job $j -WarningAction SilentlyContinue } } if ($null -eq $opts) { " ${source}: "; continue } " ${source} ($($opts.GetType().FullName)):" foreach ($p in $opts.PSObject.Properties) { $value = try { $p.Value } catch { "" } $type = if ($null -eq $value) { 'null' } else { $value.GetType().Name } if ($value -is [string] -or $value -is [ValueType] -or $null -eq $value) { ' {0} ({1}) = {2}' -f $p.Name, $type, (Format-Value $value) } else { ' {0} ({1})' -f $p.Name, $type } } } catch { " ${source}: not available: $($_.Exception.Message)" } } '' } } | Out-String -Width 300 $report $report | Set-Content -Path $OutFile -Encoding UTF8 Write-Host "Saved to $OutFile"