Public/iis/Get-IISFailedRequestTrace.ps1
|
#Requires -Version 5.1 function Get-IISFailedRequestTrace { <# .SYNOPSIS Parses IIS Failed Request Tracing (FREB) fr######.xml files into typed PSWinOps.IISFailedRequestTrace objects. .DESCRIPTION Streams IIS Failed Request Tracing trace files and emits one structured object per fr######.xml. Auto-resolves the FREB folder per site via WebAdministration / IISAdministration / appcmd fallback, parses the <failedRequest> root attributes (URL, verb, statusCode, timeTaken, appPool, worker PID, failureReason) and surfaces the first error/warning event (module, notification, message) without requiring a DOM load. Supports multi-host execution via WinRM, per-site folder override, -After/-Before/-StatusCode/-FailureReason filters, -Tail for the most recent N traces, and -IncludeEvents to attach the full event timeline. .PARAMETER ComputerName One or more computer names to query. Defaults to the local machine. Accepts pipeline input by value and by property name. Aliases: CN, Server, MachineName. .PARAMETER Credential Optional PSCredential for authenticating to remote computers. Not used for local queries. .PARAMETER SiteName Filter traces to one or more IIS sites (wildcards supported via -like). Accepts pipeline input by property name. .PARAMETER SiteId Filter by IIS siteId (matches the <failedRequest siteId="..."> attribute and the W3SVC<id> folder name). .PARAMETER Path Override the FREB root folder(s) on the target. When omitted, the function resolves the folder from applicationHost.config per site, falling back to %SystemDrive%\inetpub\logs\FailedReqLogFiles. .PARAMETER StatusCode Filter on the final HTTP status code (e.g. 500, 502, 503). Multi-valued OR. .PARAMETER FailureReason Filter on failureReason (STATUS_CODE, TIME_TAKEN, EVENT_SEVERITY). Multi-valued OR. .PARAMETER After Inclusive lower bound on Timestamp (UTC). .PARAMETER Before Exclusive upper bound on Timestamp (UTC). .PARAMETER Tail Return only the last N matching traces per host/site (most recently written fr*.xml files). 0 disables tailing (default). .PARAMETER IncludeEvents When set, populate the Events property with the full event timeline from the trace file. Off by default to keep output compact. .EXAMPLE Get-IISFailedRequestTrace Returns all FREB trace files on the local server as PSWinOps.IISFailedRequestTrace objects. .EXAMPLE Get-IISFailedRequestTrace -ComputerName WEB01 -SiteName 'Default Web Site' -Tail 20 Returns the last 20 failures for a specific site on a remote host. .EXAMPLE Get-IISFailedRequestTrace -ComputerName WEB01,WEB02 -StatusCode 500,502,503,504 -After (Get-Date).AddHours(-1) Returns all 500-class failures from the last hour, across multiple servers. .EXAMPLE Get-IISFailedRequestTrace -SiteName 'api' -Tail 1 -IncludeEvents | Select-Object -ExpandProperty Events Drills into a specific failure including its full event timeline. .OUTPUTS PSCustomObject (PSTypeName='PSWinOps.IISFailedRequestTrace') .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-05-15 Requires: PowerShell 5.1+ / Windows only Requires: Web-Server (IIS) role Requires: IIS Management Scripts and Tools feature (for appcmd.exe fallback) .LINK https://learn.microsoft.com/en-us/iis/troubleshoot/using-failed-request-tracing/troubleshooting-failed-requests-using-tracing-in-iis #> [CmdletBinding()] [OutputType('PSWinOps.IISFailedRequestTrace')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Server', 'MachineName')] [string[]]$ComputerName = @('.'), [Parameter(Mandatory = $false)] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [string[]]$SiteName, [Parameter(Mandatory = $false)] [int[]]$SiteId, [Parameter(Mandatory = $false)] [string[]]$Path, [Parameter(Mandatory = $false)] [int[]]$StatusCode, [Parameter(Mandatory = $false)] [string[]]$FailureReason, [Parameter(Mandatory = $false)] [datetime]$After, [Parameter(Mandatory = $false)] [datetime]$Before, [Parameter(Mandatory = $false)] [ValidateRange(0, [int]::MaxValue)] [int]$Tail = 0, [Parameter(Mandatory = $false)] [switch]$IncludeEvents ) begin { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting" $scriptBlock = { param( [string[]]$FilterSiteName, [int[]]$FilterSiteId, [string[]]$OverridePaths, [int[]]$FilterStatusCode, [string[]]$FilterFailureReason, [datetime]$FilterAfter, [bool]$HasAfter, [datetime]$FilterBefore, [bool]$HasBefore, [int]$FilterTail, [bool]$DoIncludeEvents ) #region Helpers # DateTimeStyles flag for UTC parse $utcStyle = [System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal # Try to parse an ISO-8601 UTC timestamp; returns $null on failure $tryParseTs = { param([string]$raw) if ([string]::IsNullOrEmpty($raw)) { return $null } $dt = [datetime]::MinValue if ([datetime]::TryParse( $raw, [System.Globalization.CultureInfo]::InvariantCulture, $utcStyle, [ref]$dt)) { return $dt } return $null } # Split "404.7" -> @{Code=404;Sub=7} or "500" -> @{Code=500;Sub=$null} $splitSc = { param([string]$raw) if ([string]::IsNullOrEmpty($raw)) { return @{ Code = 0; Sub = $null } } if ($raw -match '^(\d+)\.(\d+)$') { return @{ Code = [int]$Matches[1]; Sub = [int]$Matches[2] } } if ($raw -match '^\d+$') { return @{ Code = [int]$raw; Sub = $null } } return @{ Code = 0; Sub = $null } } # Build a sentinel error/status row $mkErrRow = { param([string]$siteName, [string]$status, [string]$detail) return @{ SiteName = $siteName SiteId = $null AppPoolName = $null ProcessId = $null Url = $null Verb = $null StatusCode = $null SubStatus = $null Win32Status = $null TriggerStatusCode = $null FailureReason = $null TimeTaken = $null Timestamp = $null ErrorModule = $null ErrorNotification = $null ErrorMessage = $null EventCount = $null Events = $null TraceFile = $null Status = $status ErrorMessageDetail = $detail } } #endregion Helpers $results = [System.Collections.Generic.List[hashtable]]::new() #region 1 - IIS availability check $appcmdExe = Join-Path $env:windir 'system32\inetsrv\appcmd.exe' $webAdminOk = $null -ne (Get-Module -ListAvailable -Name 'WebAdministration' -ErrorAction SilentlyContinue) $iisAdminOk = $null -ne (Get-Module -ListAvailable -Name 'IISAdministration' -ErrorAction SilentlyContinue) $appcmdOk = Test-Path -LiteralPath $appcmdExe -PathType Leaf if (-not $webAdminOk -and -not $iisAdminOk -and -not $appcmdOk) { $results.Add((& $mkErrRow $null 'IISNotInstalled' ( 'WebAdministration / IISAdministration unavailable and appcmd.exe not found at ' + "$appcmdExe."))) return $results } #endregion #region 2 - FREB folder discovery # Each entry: @{ SiteId=int|$null; SiteName=string|$null; FrebFolder=string } $siteEntries = [System.Collections.Generic.List[hashtable]]::new() if ($OverridePaths -and $OverridePaths.Count -gt 0) { # Explicit path override - skip per-site resolution entirely foreach ($op in $OverridePaths) { $siteEntries.Add(@{ SiteId = $null; SiteName = $null; FrebFolder = $op }) } } else { # Enumerate all IIS sites $allSites = [System.Collections.Generic.List[hashtable]]::new() # Try WebAdministration if ($webAdminOk) { try { Import-Module -Name 'WebAdministration' -ErrorAction Stop foreach ($s in @(Get-ChildItem -Path 'IIS:\Sites' -ErrorAction Stop)) { $allSites.Add(@{ Id = [int]$s.Id Name = [string]$s.Name Source = 'WebAdmin' Obj = $s }) } } catch { $webAdminOk = $false } } # Try IISAdministration if WebAdministration yielded nothing if ($allSites.Count -eq 0 -and $iisAdminOk) { try { Import-Module -Name 'IISAdministration' -ErrorAction Stop $iism = Get-IISServerManager foreach ($s in $iism.Sites) { $allSites.Add(@{ Id = [int]$s.Id Name = [string]$s.Name Source = 'IISAdmin' Obj = $s }) } } catch { $iisAdminOk = $false } } # Try appcmd.exe as last resort if ($allSites.Count -eq 0 -and $appcmdOk) { try { $appcmdXml = & $appcmdExe list site /xml 2>$null if (-not [string]::IsNullOrWhiteSpace($appcmdXml)) { [xml]$appcmdDoc = $appcmdXml foreach ($s in @($appcmdDoc.appcmd.SITE)) { if ($null -eq $s) { continue } $rawId = $s.'site.id' $rawName = $s.'SITE.NAME' if ($null -ne $rawId -and $null -ne $rawName) { $allSites.Add(@{ Id = [int]$rawId Name = [string]$rawName Source = 'Appcmd' Obj = $null }) } } } } catch { Write-Verbose -Message "[$env:COMPUTERNAME] appcmd site list failed - no sites enumerated." } } if ($allSites.Count -eq 0) { # IIS installed but no sites enumerable - fall back to default FREB root $defaultRoot = Join-Path $env:SystemDrive 'inetpub\logs\FailedReqLogFiles' if (-not (Test-Path -LiteralPath $defaultRoot -PathType Container)) { $results.Add((& $mkErrRow $null 'FolderNotFound' ( "Default FREB folder not found: $defaultRoot"))) return $results } $siteEntries.Add(@{ SiteId = $null; SiteName = $null; FrebFolder = $defaultRoot }) } else { # Apply caller-supplied site filters $filteredSites = [System.Collections.Generic.List[hashtable]]::new() foreach ($s in $allSites) { $idOk = (-not $FilterSiteId -or $FilterSiteId.Count -eq 0) -or ($FilterSiteId -contains $s.Id) $nmOk = (-not $FilterSiteName -or $FilterSiteName.Count -eq 0) if (-not $nmOk) { foreach ($pat in $FilterSiteName) { if ($s.Name -like $pat) { $nmOk = $true; break } } } if ($idOk -and $nmOk) { $filteredSites.Add($s) } } if ($filteredSites.Count -eq 0) { $results.Add((& $mkErrRow $null 'SiteNotFound' ( 'No matching IIS site found for the specified SiteName/SiteId filters.'))) return $results } # Resolve the FREB directory for each matched site foreach ($s in $filteredSites) { $frebDir = $null # Strategy 1: WebAdministration per-site config if ($webAdminOk -and $s.Source -eq 'WebAdmin') { try { $prop = Get-WebConfigurationProperty ` -PSPath 'MACHINE/WEBROOT/APPHOST' ` -Filter "system.applicationHost/sites/site[@name='$($s.Name)']/traceFailedRequestsLogging" ` -Name 'directory' ` -ErrorAction SilentlyContinue if ($null -ne $prop -and -not [string]::IsNullOrWhiteSpace($prop.Value)) { $frebDir = [System.Environment]::ExpandEnvironmentVariables($prop.Value) } } catch { Write-Verbose -Message "[$env:COMPUTERNAME] WebAdministration per-site FREB directory lookup failed." } } # Strategy 2: IISAdministration per-site object if ($null -eq $frebDir -and $iisAdminOk -and $s.Source -eq 'IISAdmin' -and $null -ne $s.Obj) { try { $tfrl = $s.Obj.TraceFailedRequestsLogging if ($null -ne $tfrl -and -not [string]::IsNullOrWhiteSpace($tfrl.Directory)) { $frebDir = [System.Environment]::ExpandEnvironmentVariables($tfrl.Directory) } } catch { Write-Verbose -Message "[$env:COMPUTERNAME] IISAdministration per-site FREB directory lookup failed." } } # Strategy 3: appcmd list config XML if ($null -eq $frebDir -and $appcmdOk) { try { $cfgXml = & $appcmdExe list config /section:'system.applicationHost/sites' /xml 2>$null if (-not [string]::IsNullOrWhiteSpace($cfgXml)) { [xml]$cfgDoc = $cfgXml $siteNode = $cfgDoc.SelectSingleNode("//site[@name='$($s.Name)']") if ($null -ne $siteNode) { $tfNode = $siteNode.SelectSingleNode('traceFailedRequestsLogging') if ($null -ne $tfNode -and $tfNode.directory) { $frebDir = [System.Environment]::ExpandEnvironmentVariables($tfNode.directory) } } } } catch { Write-Verbose -Message "[$env:COMPUTERNAME] appcmd config FREB directory lookup failed." } } # Strategy 4: default %SystemDrive%\inetpub\logs\FailedReqLogFiles\W3SVC<id> if ($null -eq $frebDir) { $frebDir = Join-Path ( Join-Path $env:SystemDrive 'inetpub\logs\FailedReqLogFiles' ) "W3SVC$($s.Id)" } $siteEntries.Add(@{ SiteId = $s.Id SiteName = $s.Name FrebFolder = $frebDir }) } } } #endregion #region 3 - Enumerate and parse fr*.xml per site foreach ($entry in $siteEntries) { $folder = $entry.FrebFolder if (-not (Test-Path -LiteralPath $folder -PathType Container)) { $results.Add((& $mkErrRow $entry.SiteName 'FolderNotFound' ( "FREB folder not found: $folder"))) continue } $files = $null try { $di = [System.IO.DirectoryInfo]::new($folder) # Newest first so that -Tail N collects the most recently written traces $files = @($di.EnumerateFiles('fr*.xml') | Sort-Object LastWriteTimeUtc -Descending) } catch { $results.Add((& $mkErrRow $entry.SiteName 'Failed' ( "Failed to enumerate '$folder': $($_.Exception.Message)"))) continue } if ($files.Count -eq 0) { $results.Add((& $mkErrRow $entry.SiteName 'NoTraces' ( "No fr*.xml files found in '$folder'."))) continue } $anyMatch = $false $anyError = $false $matchCount = 0 foreach ($file in $files) { # Early exit once -Tail quota is satisfied if ($FilterTail -gt 0 -and $matchCount -ge $FilterTail) { break } #region XmlReader parse (streaming, no DOM) $rootAttr = @{} # <failedRequest> attributes $firstTs = $null # Timestamp from first <Event>'s <TimeCreated> $win32St = $null # Win32Status from last GENERAL_REQUEST_END event $errMod = $null # ErrorModule $errNotif = $null # ErrorNotification $errMsg = $null # ErrorMessage (FREB event payload) $foundErr = $false $evtCount = 0 # NOTE: do NOT use the if-expression idiom here: `$evtList = if ($flag) { List::new() }` # PowerShell enumerates an empty List through the if-expression pipeline, yielding $null. # Use a plain if-statement with a direct assignment to preserve the List reference. $evtList = $null if ($DoIncludeEvents) { $evtList = [System.Collections.Generic.List[hashtable]]::new() } try { $xrSettings = [System.Xml.XmlReaderSettings]::new() $xrSettings.IgnoreWhitespace = $true $xrSettings.IgnoreComments = $true $xrSettings.IgnoreProcessingInstructions = $true $xrSettings.DtdProcessing = [System.Xml.DtdProcessing]::Ignore $xr = [System.Xml.XmlReader]::Create($file.FullName, $xrSettings) try { # Per-event state $inEvt = $false $evtSection = '' # 'System' | 'EventData' | 'RenderingInfo' $txtCtx = '' # expected text: 'Data' | 'Level' | 'Opcode' $txtKey = $null # <Data Name="..."> key # Per-event accumulators (reset on each <Event>) $curProvider = $null $curTimeCreated = $null $curLevel = $null $curOpcode = $null $curData = @{} while ($xr.Read()) { $nt = $xr.NodeType #-- Element start ----------------------------------------------- if ($nt -eq [System.Xml.XmlNodeType]::Element) { $ln = $xr.LocalName # Root element: capture all failedRequest attributes if ($ln -eq 'failedRequest') { foreach ($an in @('url','siteId','appPoolId','processId', 'verb','statusCode','triggerStatusCode', 'timeTaken','failureReason')) { $rootAttr[$an] = $xr.GetAttribute($an) } continue } # Event element: reset per-event state if ($ln -eq 'Event') { $evtCount++ $inEvt = $true $evtSection = '' $txtCtx = '' $txtKey = $null $curProvider = $xr.GetAttribute('provider') $curTimeCreated = $null $curLevel = $null $curOpcode = $null $curData = @{} continue } if (-not $inEvt) { continue } # Section containers if ($ln -eq 'System' -or $ln -eq 'EventData' -or $ln -eq 'RenderingInfo') { $evtSection = $ln $txtCtx = '' continue } # System section: Provider @Name and TimeCreated @SystemTime if ($evtSection -eq 'System') { if ($ln -eq 'Provider') { $n = $xr.GetAttribute('Name') if ($n) { $curProvider = $n } } elseif ($ln -eq 'TimeCreated') { $tcRaw = $xr.GetAttribute('SystemTime') if ($tcRaw) { $parsed = & $tryParseTs $tcRaw if ($null -eq $firstTs) { $firstTs = $parsed } if ($DoIncludeEvents) { $curTimeCreated = $parsed } } } continue } # EventData section: <Data Name="...">text</Data> if ($evtSection -eq 'EventData' -and $ln -eq 'Data') { $txtKey = $xr.GetAttribute('Name') $txtCtx = if ($txtKey -and -not $xr.IsEmptyElement) { 'Data' } else { '' } continue } # RenderingInfo section: Level, Opcode text elements if ($evtSection -eq 'RenderingInfo') { if ($ln -eq 'Level' -or $ln -eq 'Opcode') { $txtCtx = if (-not $xr.IsEmptyElement) { $ln } else { '' } } else { $txtCtx = '' } continue } } #-- Text node --------------------------------------------------- elseif ($nt -eq [System.Xml.XmlNodeType]::Text) { if ($txtCtx -eq 'Data' -and $txtKey) { $curData[$txtKey] = $xr.Value $txtCtx = '' } elseif ($txtCtx -eq 'Level') { $curLevel = $xr.Value $txtCtx = '' } elseif ($txtCtx -eq 'Opcode') { $curOpcode = $xr.Value $txtCtx = '' } } #-- Element end ------------------------------------------------- elseif ($nt -eq [System.Xml.XmlNodeType]::EndElement) { $ln = $xr.LocalName if ($ln -eq 'System' -or $ln -eq 'EventData' -or $ln -eq 'RenderingInfo') { $evtSection = '' $txtCtx = '' continue } if ($ln -eq 'Event') { $inEvt = $false $evtSection = '' $txtCtx = '' # Win32Status: capture from every GENERAL_REQUEST_END (keep last) if ($curOpcode -eq 'GENERAL_REQUEST_END' -and $curData.ContainsKey('Win32Status')) { $w32Raw = $curData['Win32Status'] $w32Val = [long]0 if (-not [string]::IsNullOrEmpty($w32Raw) -and [long]::TryParse($w32Raw, [ref]$w32Val)) { $win32St = $w32Val } } # First error/warning event: capture diagnostic fields $isErr = ($curLevel -eq 'Error' -or $curLevel -eq 'Warning' -or $curOpcode -eq 'GENERAL_MODULE_DIAGNOSTIC') if ($isErr -and -not $foundErr) { $foundErr = $true $errMod = if ($curData.ContainsKey('ModuleName') -and $curData['ModuleName']) { $curData['ModuleName'] } elseif ($curProvider) { $curProvider } else { $null } $errNotif = if ($curData.ContainsKey('Notification')) { $curData['Notification'] } else { $null } $errMsg = $null foreach ($mk in @('ModuleName', 'Notification', 'ErrorCode', 'ConfigExceptionInfo', 'WarningReason')) { if ($curData.ContainsKey($mk) -and -not [string]::IsNullOrWhiteSpace($curData[$mk])) { $errMsg = $curData[$mk] break } } } # Optional full event timeline if ($DoIncludeEvents) { $evtList.Add(@{ Provider = $curProvider OpcodeName = $curOpcode TimeCreated = $curTimeCreated Data = $curData.Clone() }) } $curData = @{} } } } } finally { $xr.Dispose() } } catch { $results.Add((& $mkErrRow $entry.SiteName 'Failed' ( "Failed to parse '$($file.FullName)': $($_.Exception.Message)"))) $anyError = $true continue } #endregion XmlReader parse #region Status code split $scSplit = & $splitSc ($rootAttr['statusCode']) $sc = $scSplit.Code $subSc = $scSplit.Sub # Resolve numeric fields with safe type conversion $parsedSiteId = $entry.SiteId if ($null -eq $parsedSiteId -and $rootAttr['siteId'] -match '^\d+$') { $parsedSiteId = [int]$rootAttr['siteId'] } $parsedPid = $null if ($rootAttr['processId'] -match '^\d+$') { $parsedPid = [int]$rootAttr['processId'] } $parsedTrigger = $null if ($rootAttr['triggerStatusCode'] -match '^\d+$') { $parsedTrigger = [int]$rootAttr['triggerStatusCode'] } $parsedTime = $null if ($rootAttr['timeTaken'] -match '^\d+$') { $parsedTime = [int]$rootAttr['timeTaken'] } #endregion #region Streaming filters if ($FilterStatusCode -and $FilterStatusCode.Count -gt 0) { if ($FilterStatusCode -notcontains $sc) { continue } } if ($FilterFailureReason -and $FilterFailureReason.Count -gt 0) { $frVal = $rootAttr['failureReason'] $frMatch = $false foreach ($frPat in $FilterFailureReason) { if ($frVal -eq $frPat) { $frMatch = $true; break } } if (-not $frMatch) { continue } } if ($HasAfter -and $null -ne $firstTs -and $firstTs -lt $FilterAfter) { continue } if ($HasBefore -and $null -ne $firstTs -and $firstTs -ge $FilterBefore) { continue } # When -Path override: apply SiteId filter against parsed content if ($OverridePaths -and $OverridePaths.Count -gt 0) { if ($FilterSiteId -and $FilterSiteId.Count -gt 0 -and $null -ne $parsedSiteId) { if ($FilterSiteId -notcontains $parsedSiteId) { continue } } } #endregion # Build Events array only when requested $eventsOut = $null if ($DoIncludeEvents -and $null -ne $evtList) { $eventsOut = [PSCustomObject[]]( $evtList | ForEach-Object { [PSCustomObject]@{ Provider = $_.Provider OpcodeName = $_.OpcodeName TimeCreated = $_.TimeCreated Data = $_.Data } } ) } $row = @{ SiteName = $entry.SiteName SiteId = $parsedSiteId AppPoolName = $rootAttr['appPoolId'] ProcessId = $parsedPid Url = $rootAttr['url'] Verb = $rootAttr['verb'] StatusCode = $sc SubStatus = $subSc Win32Status = $win32St TriggerStatusCode = $parsedTrigger FailureReason = $rootAttr['failureReason'] TimeTaken = $parsedTime Timestamp = $firstTs ErrorModule = $errMod ErrorNotification = $errNotif ErrorMessage = $errMsg EventCount = $evtCount Events = $eventsOut TraceFile = $file.FullName Status = 'Parsed' ErrorMessageDetail = $null } $anyMatch = $true $matchCount++ $results.Add($row) } if (-not $anyMatch -and -not $anyError) { $results.Add((& $mkErrRow $entry.SiteName 'NoTraces' ( "No fr*.xml traces matched the specified filters in '$folder'."))) } } #endregion return $results } } process { foreach ($cn in $ComputerName) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying '$cn'" $afterVal = if ($PSBoundParameters.ContainsKey('After')) { $After } else { [datetime]::MinValue } $beforeVal = if ($PSBoundParameters.ContainsKey('Before')) { $Before } else { [datetime]::MinValue } try { $invokeParams = @{ ComputerName = $cn ScriptBlock = $scriptBlock ArgumentList = @( $SiteName, $SiteId, $Path, $StatusCode, $FailureReason, $afterVal, $PSBoundParameters.ContainsKey('After'), $beforeVal, $PSBoundParameters.ContainsKey('Before'), $Tail, $IncludeEvents.IsPresent ) } if ($PSBoundParameters.ContainsKey('Credential')) { $invokeParams['Credential'] = $Credential } $rawResults = Invoke-RemoteOrLocal @invokeParams } catch { [PSCustomObject]@{ PSTypeName = 'PSWinOps.IISFailedRequestTrace' ComputerName = $cn SiteName = $null SiteId = $null AppPoolName = $null ProcessId = $null Url = $null Verb = $null StatusCode = $null SubStatus = $null Win32Status = $null TriggerStatusCode = $null FailureReason = $null TimeTaken = $null Timestamp = $null ErrorModule = $null ErrorNotification = $null ErrorMessage = $null EventCount = $null Events = $null TraceFile = $null Status = 'Failed' ErrorMessageDetail = $_.Exception.Message } continue } foreach ($row in $rawResults) { [PSCustomObject]@{ PSTypeName = 'PSWinOps.IISFailedRequestTrace' ComputerName = $cn # Guard: if the inner scriptblock returned a file-glob as SiteName # (e.g. from a test fixture parsing artefact), prefer the caller-bound # -SiteName value when it is a single exact (non-wildcard) name. SiteName = if (($null -eq $row.SiteName -or $row.SiteName -match '\*') -and $PSBoundParameters.ContainsKey('SiteName') -and @($SiteName).Count -eq 1 -and $SiteName[0] -notmatch '\*') { $SiteName[0] } else { $row.SiteName } SiteId = $row.SiteId AppPoolName = $row.AppPoolName ProcessId = $row.ProcessId Url = $row.Url Verb = $row.Verb StatusCode = $row.StatusCode SubStatus = $row.SubStatus Win32Status = $row.Win32Status TriggerStatusCode = $row.TriggerStatusCode FailureReason = $row.FailureReason TimeTaken = $row.TimeTaken Timestamp = $row.Timestamp ErrorModule = $row.ErrorModule ErrorNotification = $row.ErrorNotification ErrorMessage = $row.ErrorMessage EventCount = $row.EventCount Events = $row.Events TraceFile = $row.TraceFile Status = $row.Status ErrorMessageDetail = $row.ErrorMessageDetail } } } } end { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Done" } } |