Public/iis/Watch-IISLog.ps1
|
#Requires -Version 5.1 function Watch-IISLog { <# .SYNOPSIS Streams new entries from a live IIS site log in real time (tail -f), parsing each line into a PSWinOps.IISLogEntry object as it is written. .DESCRIPTION Resolves the active W3C log file of a given IIS site from its configuration (WebAdministration provider, falling back to Microsoft.Web.Administration or appcmd.exe), opens it with FileShare.ReadWrite|Delete so as not to disturb IIS, and emits each new data line as a structured PSWinOps.IISLogEntry, the same shape produced by Get-IISParsedLog. Honours mid-file #Fields re-detection (post-recycle) and optionally follows daily log rollover via -FollowRollover. Filtering parameters (-Method/-Status/-UriLike/-ClientIP/ -MinStatus) are applied during streaming. Use -InitialLines to replay the last N entries before entering follow mode, and -Duration/-MaxEntries to bound a run (recommended for remote sessions). .PARAMETER ComputerName One or more computer names to tail. Defaults to the local machine. Accepts pipeline input by value and by property name. .PARAMETER Credential Optional PSCredential for authenticating to remote computers. Not used for local queries. .PARAMETER SiteName IIS site whose active log file is to be followed. Resolved via the WebAdministration provider, Microsoft.Web.Administration, or appcmd.exe. .PARAMETER LogFormat Log format in use. Only W3C is supported. IIS, NCSA and Custom formats are rejected with a clear error message. .PARAMETER InitialLines Replay the last N matching data lines from the current log file before entering follow mode (tail -n N -f semantics). 0 means pure follow mode: only entries written after the cmdlet starts are emitted. .PARAMETER FollowRollover Detect the daily log rotation (IIS rolls u_exYYMMDD.log) and reopen the new file once it appears. Without this switch the cmdlet exits cleanly when the current file is rotated away. .PARAMETER PollIntervalMs Sleep interval in milliseconds between read attempts when at end of stream. Defaults to 1000 ms (matching IIS default flush cadence). .PARAMETER Duration Maximum wall-clock duration of the tail. Whichever cap hits first (-Duration or -MaxEntries) ends the stream. .PARAMETER MaxEntries Hard cap on the number of emitted entries (after filtering). Pairs with -Duration to bound remote runs safely. .PARAMETER Method Filter on cs-method (e.g. GET, POST). Case-insensitive, multi-valued OR. .PARAMETER Status Filter on sc-status (e.g. 500, 502). Multi-valued OR. .PARAMETER UriLike Wildcard pattern matched against UriStem via -like. .PARAMETER ClientIP Filter on c-ip. Exact match, case-insensitive, multi-valued OR. .PARAMETER MinStatus Emit only entries with sc-status >= N (e.g. 400 to surface all errors). .EXAMPLE Watch-IISLog -SiteName 'Default Web Site' Tails the Default Web Site log in real time, emitting parsed entries. .EXAMPLE Watch-IISLog -SiteName 'Default Web Site' -InitialLines 50 -MinStatus 400 Replays the last 50 error-or-worse entries then follows for new ones. .EXAMPLE Watch-IISLog -SiteName 'www.contoso.com' -FollowRollover -Duration (New-TimeSpan -Hours 1) Follows the site log including daily rollover for one hour. .EXAMPLE 'WEB01' | Watch-IISLog -SiteName 'api' -Credential (Get-Credential) -MaxEntries 1000 Remote tail with credentials, capped at 1000 entries. .EXAMPLE Watch-IISLog -SiteName 'api' -Method POST -UriLike '/api/*' | Where-Object TimeTaken -gt 2000 Streams slow POST requests to the /api/* path in real time. .OUTPUTS PSCustomObject (PSTypeName='PSWinOps.IISLogEntry') One object per parsed data line. Properties absent from the active #Fields directive are emitted as $null. .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: WebAdministration module, Microsoft.Web.Administration, or appcmd.exe IIS writes log files in UTC by default. Timestamps are always returned as UTC DateTime objects regardless of the local system timezone. The parser handles mid-file #Fields re-declarations that IIS emits after a log rotation or w3wp.exe recycle within a single file. For remote sessions, always supply -Duration or -MaxEntries to bound execution; otherwise Ctrl-C is the only way to stop the remote stream. .LINK https://learn.microsoft.com/en-us/iis/manage/provisioning-and-managing-iis/configure-logging-in-iis #> [CmdletBinding()] [OutputType('PSWinOps.IISLogEntry')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string]$SiteName, [Parameter(Mandatory = $false)] [ValidateSet('W3C')] [string]$LogFormat = 'W3C', [Parameter(Mandatory = $false)] [ValidateRange(0, 1000000)] [int]$InitialLines = 0, [Parameter(Mandatory = $false)] [switch]$FollowRollover, [Parameter(Mandatory = $false)] [ValidateRange(100, 60000)] [int]$PollIntervalMs = 1000, [Parameter(Mandatory = $false)] [timespan]$Duration, [Parameter(Mandatory = $false)] [ValidateRange(1, 2147483647)] [int]$MaxEntries, [Parameter(Mandatory = $false)] [string[]]$Method, [Parameter(Mandatory = $false)] [int[]]$Status, [Parameter(Mandatory = $false)] [string]$UriLike, [Parameter(Mandatory = $false)] [string[]]$ClientIP, [Parameter(Mandatory = $false)] [int]$MinStatus ) begin { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting" # Capture bound-parameter flags in begin{} so process{} can reference them # without re-calling ContainsKey on every loop iteration. $hasDuration = $PSBoundParameters.ContainsKey('Duration') $hasMaxEntries = $PSBoundParameters.ContainsKey('MaxEntries') $hasMethod = $PSBoundParameters.ContainsKey('Method') $hasStatus = $PSBoundParameters.ContainsKey('Status') $hasUriLike = $PSBoundParameters.ContainsKey('UriLike') $hasClientIP = $PSBoundParameters.ContainsKey('ClientIP') $hasMinStatus = $PSBoundParameters.ContainsKey('MinStatus') # ===================================================================== # Remote scriptblock -- MUST be self-contained (no PSWinOps helpers). # Dispatched via Invoke-RemoteOrLocal with an ArgumentList bundle. # ===================================================================== $scriptBlock = { param( [string] $ArgSiteName, [int] $ArgInitialLines, [bool] $ArgFollowRollover, [int] $ArgPollIntervalMs, [object] $ArgDuration, # [timespan] or $null [object] $ArgMaxEntries, # [int] or $null [string[]] $ArgFilterMethod, [int[]] $ArgFilterStatus, [object] $ArgFilterUriLike, # [string] or $null [string[]] $ArgFilterClientIP, [object] $ArgFilterMinStatus # [int] or $null ) # ----------------------------------------------------------------- # W3C field name -> output property name (mirrors Get-IISParsedLog) # ----------------------------------------------------------------- $fieldMap = @{ 's-sitename' = 'SiteName' 's-computername' = 'ServerName' 's-ip' = 'ServerIP' 'cs-method' = 'Method' 'cs-uri-stem' = 'UriStem' 'cs-uri-query' = 'UriQuery' 's-port' = 'ServerPort' 'cs-username' = 'UserName' 'c-ip' = 'ClientIP' 'cs(User-Agent)' = 'UserAgent' 'cs(Referer)' = 'Referer' 'sc-status' = 'HttpStatus' 'sc-substatus' = 'HttpSubStatus' 'sc-win32-status' = 'Win32Status' 'sc-bytes' = 'BytesSent' 'cs-bytes' = 'BytesReceived' 'time-taken' = 'TimeTaken' } $intProps = [System.Collections.Generic.HashSet[string]]::new( [string[]]@('ServerPort', 'HttpStatus', 'HttpSubStatus', 'TimeTaken'), [System.StringComparer]::Ordinal ) $longProps = [System.Collections.Generic.HashSet[string]]::new( [string[]]@('Win32Status', 'BytesSent', 'BytesReceived'), [System.StringComparer]::Ordinal ) # ----------------------------------------------------------------- # Helper: parse one W3C data line -> PSWinOps.IISLogEntry # ----------------------------------------------------------------- function ConvertFrom-IISW3CLine { param( [string] $RawLine, [string[]] $ActiveColumns, [hashtable]$FieldPropertyMap, [System.Collections.Generic.HashSet[string]]$IntPropNames, [System.Collections.Generic.HashSet[string]]$LongPropNames, [string] $FilePath, [int] $LineNum ) $tokens = $RawLine -split '\s+' $props = [ordered]@{ PSTypeName = 'PSWinOps.IISLogEntry' Timestamp = $null LogFile = $FilePath LineNumber = $LineNum ComputerName = $env:COMPUTERNAME SiteName = $null ServerName = $null ServerIP = $null ServerPort = $null Method = $null UriStem = $null UriQuery = $null UserName = $null ClientIP = $null UserAgent = $null Referer = $null HttpStatus = $null HttpSubStatus = $null Win32Status = $null BytesSent = $null BytesReceived = $null TimeTaken = $null } $rawDate = $null $rawTime = $null for ($i = 0; $i -lt $ActiveColumns.Count; $i++) { $col = $ActiveColumns[$i] $raw = if ($i -lt $tokens.Count) { $tokens[$i] } else { '-' } if ($col -eq 'date') { $rawDate = $raw; continue } if ($col -eq 'time') { $rawTime = $raw; continue } if (-not $FieldPropertyMap.ContainsKey($col)) { continue } $propName = $FieldPropertyMap[$col] if ($raw -eq '-') { $props[$propName] = $null; continue } if ($IntPropNames.Contains($propName)) { $intVal = 0 $props[$propName] = if ([int]::TryParse($raw, [ref]$intVal)) { $intVal } else { $null } } elseif ($LongPropNames.Contains($propName)) { $longVal = [long]0 $props[$propName] = if ([long]::TryParse($raw, [ref]$longVal)) { $longVal } else { $null } } elseif ($propName -eq 'UserAgent' -or $propName -eq 'Referer') { $props[$propName] = $raw -replace '\+', ' ' } else { $props[$propName] = $raw } } if ($null -ne $rawDate -and $null -ne $rawTime -and $rawDate -ne '-' -and $rawTime -ne '-') { try { $props['Timestamp'] = [datetime]::ParseExact( "$rawDate $rawTime", 'yyyy-MM-dd HH:mm:ss', [System.Globalization.CultureInfo]::InvariantCulture, ([System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal) ) } catch { Write-Verbose -Message "Watch-IISLog: could not parse timestamp '$rawDate $rawTime' -- Timestamp set to null." } } return [PSCustomObject]$props } # ----------------------------------------------------------------- # Helper: locate the lexicographically latest u_ex*.log in a folder # ----------------------------------------------------------------- function Find-LatestIISLogFile { param([string]$Directory) if (-not (Test-Path -LiteralPath $Directory -PathType Container)) { return $null } $found = Get-ChildItem -LiteralPath $Directory -Filter 'u_ex*.log' -File ` -ErrorAction SilentlyContinue | Sort-Object -Property Name -Descending | Select-Object -First 1 return if ($found) { $found.FullName } else { $null } } # ----------------------------------------------------------------- # Helper: test whether an entry passes the streaming filters. # Filter parameters are passed explicitly so that PSScriptAnalyzer # can resolve usage of the outer scriptblock Arg* variables. # ----------------------------------------------------------------- function Test-IISLogEntryFilter { param( [PSObject] $Entry, [string[]] $FilterMethod, [int[]] $FilterStatus, [object] $FilterUriLike, # [string] or $null [string[]] $FilterClientIP, [object] $FilterMinStatus # [int] or $null ) if ($FilterMethod -and $FilterMethod.Count -gt 0 -and $null -ne $Entry.Method -and ($FilterMethod -inotcontains $Entry.Method)) { return $false } if ($FilterStatus -and $FilterStatus.Count -gt 0 -and $null -ne $Entry.HttpStatus -and ($FilterStatus -notcontains $Entry.HttpStatus)) { return $false } if ($null -ne $FilterUriLike -and $null -ne $Entry.UriStem -and $Entry.UriStem -notlike $FilterUriLike) { return $false } if ($FilterClientIP -and $FilterClientIP.Count -gt 0 -and $null -ne $Entry.ClientIP -and ($FilterClientIP -inotcontains $Entry.ClientIP)) { return $false } if ($null -ne $FilterMinStatus -and $null -ne $Entry.HttpStatus -and $Entry.HttpStatus -lt $FilterMinStatus) { return $false } return $true } # ----------------------------------------------------------------- # Resolve the IIS log directory for $ArgSiteName # Priority: WebAdministration -> Microsoft.Web.Administration -> appcmd.exe # ----------------------------------------------------------------- $logDir = $null $resolveOk = $false # Method 1: WebAdministration PS provider if (-not $resolveOk) { try { if (Get-Module -Name WebAdministration -ListAvailable -ErrorAction SilentlyContinue) { Import-Module -Name WebAdministration -ErrorAction Stop $iisItem = Get-Item -Path "IIS:\Sites\$ArgSiteName" -ErrorAction Stop $logFmt = $iisItem.logFile.logFormat if ($logFmt -ne 'W3C') { Write-Error -Message ( "Watch-IISLog: site '$ArgSiteName' uses log format '$logFmt', not W3C. " + 'Enable W3C logging in IIS Manager.') -ErrorAction Continue return } $rawDir = [System.Environment]::ExpandEnvironmentVariables($iisItem.logFile.directory) $siteId = $iisItem.id $logDir = Join-Path -Path $rawDir -ChildPath "W3SVC$siteId" $resolveOk = $true } } catch [System.Management.Automation.ItemNotFoundException] { Write-Error -Message "Watch-IISLog: site '$ArgSiteName' not found in IIS configuration." -ErrorAction Continue return } catch { Write-Verbose -Message "Watch-IISLog: WebAdministration provider unavailable -- $($_.Exception.Message)" } } # Method 2: Microsoft.Web.Administration assembly if (-not $resolveOk) { try { $mwaPath = Join-Path -Path $env:SystemRoot -ChildPath 'system32\inetsrv\Microsoft.Web.Administration.dll' if (Test-Path -LiteralPath $mwaPath -PathType Leaf) { if (-not ('Microsoft.Web.Administration.ServerManager' -as [type])) { Add-Type -LiteralPath $mwaPath -ErrorAction Stop } $serverMgr = [Microsoft.Web.Administration.ServerManager]::new() $mwaSite = $serverMgr.Sites | Where-Object { $_.Name -eq $ArgSiteName } | Select-Object -First 1 if ($null -eq $mwaSite) { $serverMgr.Dispose() Write-Error -Message "Watch-IISLog: site '$ArgSiteName' not found in IIS configuration." -ErrorAction Continue return } $logFmt = $mwaSite.LogFile.LogFormat.ToString() if ($logFmt -ne 'W3C') { $serverMgr.Dispose() Write-Error -Message ( "Watch-IISLog: site '$ArgSiteName' uses log format '$logFmt', not W3C. " + 'Enable W3C logging in IIS Manager.') -ErrorAction Continue return } $rawDir = [System.Environment]::ExpandEnvironmentVariables($mwaSite.LogFile.Directory) $siteId = $mwaSite.Id $serverMgr.Dispose() $logDir = Join-Path -Path $rawDir -ChildPath "W3SVC$siteId" $resolveOk = $true } } catch { Write-Verbose -Message "Watch-IISLog: Microsoft.Web.Administration assembly unavailable -- $($_.Exception.Message)" } } # Method 3: appcmd.exe if (-not $resolveOk) { $appcmdPath = Join-Path -Path $env:SystemRoot -ChildPath 'system32\inetsrv\appcmd.exe' if (-not (Test-Path -LiteralPath $appcmdPath -PathType Leaf)) { Write-Error -Message ( "Watch-IISLog: IIS management tools not found on '$env:COMPUTERNAME'. " + "Install the 'IIS Management Scripts and Tools' Windows feature " + 'or the WebAdministration module.') -ErrorAction Continue return } try { $appcmdRaw = & $appcmdPath list site $ArgSiteName /config:* /xml 2>$null if ([string]::IsNullOrWhiteSpace($appcmdRaw)) { Write-Error -Message "Watch-IISLog: site '$ArgSiteName' not found via appcmd.exe." -ErrorAction Continue return } [xml]$appcmdXml = $appcmdRaw $siteNode = $appcmdXml.appcmd.SITE if ($null -eq $siteNode) { Write-Error -Message "Watch-IISLog: site '$ArgSiteName' not found via appcmd.exe." -ErrorAction Continue return } $logFileNode = $siteNode.site.logFile $logFmt = $logFileNode.logFormat if ($logFmt -ne 'W3C') { Write-Error -Message ( "Watch-IISLog: site '$ArgSiteName' uses log format '$logFmt', not W3C.") -ErrorAction Continue return } $rawDir = [System.Environment]::ExpandEnvironmentVariables($logFileNode.directory) $siteId = $siteNode.id $logDir = Join-Path -Path $rawDir -ChildPath "W3SVC$siteId" $resolveOk = $true } catch { Write-Error -Message ( "Watch-IISLog: unable to resolve log directory for site '$ArgSiteName': " + $_.Exception.Message) -ErrorAction Continue return } } if (-not $resolveOk -or [string]::IsNullOrEmpty($logDir)) { Write-Error -Message ( "Watch-IISLog: unable to resolve IIS log directory for site '$ArgSiteName'. " + "Install the 'IIS Management Scripts and Tools' feature or the WebAdministration module.") ` -ErrorAction Continue return } # ----------------------------------------------------------------- # Wait for the first log file to appear (site may have no traffic yet) # ----------------------------------------------------------------- $startTime = [datetime]::UtcNow $currentLogFile = Find-LatestIISLogFile -Directory $logDir while ($null -eq $currentLogFile) { if ($null -ne $ArgDuration -and (([datetime]::UtcNow - $startTime) -ge $ArgDuration)) { Write-Verbose -Message "Watch-IISLog: duration elapsed before any log file appeared in '$logDir'." return } Start-Sleep -Milliseconds $ArgPollIntervalMs $currentLogFile = Find-LatestIISLogFile -Directory $logDir } # ----------------------------------------------------------------- # Open the active log file with FileShare.ReadWrite|Delete so that # IIS (which holds an exclusive write lock) can continue appending. # ----------------------------------------------------------------- $emittedCount = 0 $activeColumns = [string[]]@() $dataLineCount = 0 $fileStream = $null $streamReader = $null try { $fileStream = [System.IO.FileStream]::new( $currentLogFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, ([System.IO.FileShare]::ReadWrite -bor [System.IO.FileShare]::Delete) ) $streamReader = [System.IO.StreamReader]::new( $fileStream, [System.Text.Encoding]::UTF8, $true # detectEncodingFromByteOrderMarks ) # ------------------------------------------------------------- # Phase 1: scan existing content to EOF # - track every #Fields directive (header re-detection support) # - when InitialLines > 0: buffer the last N matching entries # ------------------------------------------------------------- $tailQueue = $null if ($ArgInitialLines -gt 0) { $tailQueue = [System.Collections.Generic.Queue[PSObject]]::new() } while ($null -ne ($scanLine = $streamReader.ReadLine())) { if ($scanLine.StartsWith('#Fields:')) { $activeColumns = ($scanLine.Substring(8).Trim()) -split '\s+' continue } if ($scanLine.StartsWith('#') -or [string]::IsNullOrWhiteSpace($scanLine)) { continue } $dataLineCount++ if ($null -eq $tailQueue -or $activeColumns.Count -eq 0) { continue } try { $scannedEntry = ConvertFrom-IISW3CLine ` -RawLine $scanLine -ActiveColumns $activeColumns ` -FieldPropertyMap $fieldMap -IntPropNames $intProps ` -LongPropNames $longProps ` -FilePath $currentLogFile -LineNum $dataLineCount if (Test-IISLogEntryFilter -Entry $scannedEntry ` -FilterMethod $ArgFilterMethod ` -FilterStatus $ArgFilterStatus ` -FilterUriLike $ArgFilterUriLike ` -FilterClientIP $ArgFilterClientIP ` -FilterMinStatus $ArgFilterMinStatus) { $tailQueue.Enqueue($scannedEntry) if ($tailQueue.Count -gt $ArgInitialLines) { [void]$tailQueue.Dequeue() } } } catch { Write-Warning -Message "Watch-IISLog: failed to parse line $dataLineCount in '$currentLogFile': $_" } } # Emit the tail buffer before entering follow mode if ($null -ne $tailQueue) { foreach ($initialEntry in $tailQueue) { $initialEntry $emittedCount++ if ($null -ne $ArgMaxEntries -and $emittedCount -ge $ArgMaxEntries) { return } } $tailQueue = $null } # ------------------------------------------------------------- # Phase 2: follow loop -- positioned at EOF after Phase 1 # ------------------------------------------------------------- while ($true) { if ($null -ne $ArgDuration -and (([datetime]::UtcNow - $startTime) -ge $ArgDuration)) { break } if ($null -ne $ArgMaxEntries -and $emittedCount -ge $ArgMaxEntries) { break } $followLine = $streamReader.ReadLine() if ($null -eq $followLine) { # EOF -- check for log rollover when requested if ($ArgFollowRollover) { $newerFile = Find-LatestIISLogFile -Directory $logDir if ($null -ne $newerFile -and $newerFile -ne $currentLogFile) { $streamReader.Dispose() $fileStream.Dispose() $fileStream = $null $streamReader = $null $currentLogFile = $newerFile $dataLineCount = 0 $activeColumns = [string[]]@() $fileStream = [System.IO.FileStream]::new( $currentLogFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, ([System.IO.FileShare]::ReadWrite -bor [System.IO.FileShare]::Delete) ) $streamReader = [System.IO.StreamReader]::new( $fileStream, [System.Text.Encoding]::UTF8, $true ) continue } } Start-Sleep -Milliseconds $ArgPollIntervalMs continue } # Handle directive lines -- mid-stream #Fields re-detection if ($followLine.StartsWith('#Fields:')) { $activeColumns = ($followLine.Substring(8).Trim()) -split '\s+' continue } if ($followLine.StartsWith('#') -or [string]::IsNullOrWhiteSpace($followLine)) { continue } $dataLineCount++ if ($activeColumns.Count -eq 0) { Write-Warning -Message ( "Watch-IISLog: data line $dataLineCount before any #Fields directive " + "in '$currentLogFile' -- skipped.") continue } try { $liveEntry = ConvertFrom-IISW3CLine ` -RawLine $followLine -ActiveColumns $activeColumns ` -FieldPropertyMap $fieldMap -IntPropNames $intProps ` -LongPropNames $longProps ` -FilePath $currentLogFile -LineNum $dataLineCount if (Test-IISLogEntryFilter -Entry $liveEntry ` -FilterMethod $ArgFilterMethod ` -FilterStatus $ArgFilterStatus ` -FilterUriLike $ArgFilterUriLike ` -FilterClientIP $ArgFilterClientIP ` -FilterMinStatus $ArgFilterMinStatus) { $liveEntry $emittedCount++ } } catch { Write-Warning -Message "Watch-IISLog: failed to parse line $dataLineCount in '$currentLogFile': $_" } } } finally { if ($null -ne $streamReader) { try { $streamReader.Dispose() } catch { Write-Verbose -Message "Watch-IISLog: error disposing streamReader -- $($_.Exception.Message)" } } if ($null -ne $fileStream) { try { $fileStream.Dispose() } catch { Write-Verbose -Message "Watch-IISLog: error disposing fileStream -- $($_.Exception.Message)" } } } } } process { foreach ($cn in $ComputerName) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Tailing IIS log for site '$SiteName' on '$cn'" try { $invokeParams = @{ ComputerName = $cn ScriptBlock = $scriptBlock ArgumentList = @( $SiteName, $InitialLines, $FollowRollover.IsPresent, $PollIntervalMs, $(if ($hasDuration) { $Duration } else { $null }), $(if ($hasMaxEntries) { $MaxEntries } else { $null }), $(if ($hasMethod) { $Method } else { [string[]]@() }), $(if ($hasStatus) { $Status } else { [int[]]@() }), $(if ($hasUriLike) { $UriLike } else { $null }), $(if ($hasClientIP) { $ClientIP } else { [string[]]@() }), $(if ($hasMinStatus) { $MinStatus } else { $null }) ) } if ($PSBoundParameters.ContainsKey('Credential')) { $invokeParams['Credential'] = $Credential } Invoke-RemoteOrLocal @invokeParams } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] Failed to tail IIS log on '$cn': $($_.Exception.Message)" } } } end { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed" } } |