PowerShell IIS Log Parser: Analyze Web Traffic Patterns

Ad-Hoc IIS Log Analysis with PowerShell
Log Parser Studio is powerful but requires a GUI, a separate installation, and SQL-like query syntax that not every sysadmin has memorized. PowerShell’s Import-Csv, Group-Object, and Sort-Object cmdlets can parse IIS W3C log files with custom groupings, aggregate slow requests, and export findings in under 30 lines of interactive code. When you need a quick answer about what happened on a production IIS server last Tuesday, a PowerShell one-liner against the log file is often faster than opening any dedicated tool.
Quick Answer
Strip comment lines starting with # from the log file, import it with Import-Csv -Delimiter ' ' using the fields line as a header, then pipe through Group-Object, Sort-Object, and Select-Object to answer any traffic question in seconds.
Parsing W3C Log Format with Import-Csv and Custom Delimiters
IIS W3C logs use space-delimited columns with a #Fields: comment line that lists the column names. Import-Csv cannot use those headers directly because the # prefix and the inline location confuse the parser. The correct approach is to extract the header line, strip the prefix, then feed the cleaned header as the -Header parameter.
$logFile = 'C:\inetpub\logs\LogFiles\W3SVC1\u_ex250501.log'
# Read all lines
$lines = Get-Content -Path $logFile
# Extract the column names from the #Fields: line
$headerLine = ($lines | Where-Object { $_ -match '^#Fields:' }) -replace '^#Fields:\s*', ''
$headers = $headerLine -split ' '
# Filter out ALL comment lines, then parse
$dataLines = $lines | Where-Object { $_ -notmatch '^#' }
$logData = $dataLines | ConvertFrom-Csv -Delimiter ' ' -Header $headers
Write-Host "Loaded $($logData.Count) log entries"
Loaded 284631 log entries
Using ConvertFrom-Csv on the already-filtered string array is more reliable than piping Import-Csv directly against the file when comment lines are present.
Stripping Comment Lines Starting with #
IIS log files contain multiple comment blocks — a file header at the top plus an additional comment block whenever the IIS worker process recycles during the log period. Both must be stripped. The Where-Object { $_ -notmatch '^#' } filter handles all of them in a single pass regardless of how many recycles occurred.
Top 10 Most Requested URLs with Group-Object
Once the log is parsed into objects, answering traffic questions is pipeline composition. The cs-uri-stem field contains the URL path without the query string — perfect for grouping pages by hit count.
$top10Urls = $logData |
Group-Object 'cs-uri-stem' |
Sort-Object Count -Descending |
Select-Object -First 10 |
Select-Object @{N='URL'; E={$_.Name}}, Count
$top10Urls | Format-Table -AutoSize
URL Count
--- -----
/api/health 48210
/assets/app.js 31045
/assets/styles.css 28990
/login 19884
/dashboard 15221
/api/users 12003
/favicon.ico 9811
/api/sessions 8734
/logout 4322
/admin 1044
HTTP Status Code Distribution with Group-Object
The sc-status field holds the HTTP response code. Grouping on it reveals the ratio of successful requests to client errors (4xx) and server errors (5xx) — a quick proxy for application health at any point in time.
$statusDist = $logData |
Group-Object 'sc-status' |
Sort-Object Count -Descending |
Select-Object @{N='Status'; E={$_.Name}},
Count,
@{N='Percent'; E={ '{0:P1}' -f ($_.Count / $logData.Count) }}
$statusDist | Format-Table -AutoSize
Finding Slowest Requests with Sort-Object on time-taken
The time-taken field records request duration. On some IIS versions it is in milliseconds; on others it is in seconds. Check a few values against known-slow endpoints to confirm the scale before setting thresholds. Cast to [int] for numeric sorting.
$slowRequests = $logData |
Where-Object { [int]$_.'time-taken' -gt 5000 } |
Sort-Object { [int]$_.'time-taken' } -Descending |
Select-Object -First 20 |
Select-Object 'date', 'time', 'cs-uri-stem', 'cs-uri-query',
@{N='TimeTakenMs'; E={[int]$_.'time-taken'}},
'sc-status', 'c-ip'
$slowRequests | Format-Table -AutoSize
Exporting Summary Report to CSV and HTML
For sharing findings or archiving analysis, export the summary objects to CSV and HTML. Combine the top-URL and status-distribution tables into a single HTML report using multiple ConvertTo-Html fragments joined together.
$top10Urls | Export-Csv -Path 'C:\Reports\iis-top-urls.csv' -NoTypeInformation
$statusDist | Export-Csv -Path 'C:\Reports\iis-status-dist.csv' -NoTypeInformation
$htmlBody = $top10Urls | ConvertTo-Html -Fragment -PreContent '<h2>Top 10 URLs</h2>'
$htmlBody += $statusDist | ConvertTo-Html -Fragment -PreContent '<h2>Status Distribution</h2>'
$htmlBody += $slowRequests | ConvertTo-Html -Fragment -PreContent '<h2>Top 20 Slow Requests</h2>'
ConvertTo-Html -Head '<style>body{font-family:Arial}table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:4px 8px}</style>' `
-Body $htmlBody -Title 'IIS Traffic Analysis' |
Out-File -FilePath 'C:\Reports\iis-analysis.html' -Encoding UTF8
Common Errors
- Column headers in W3C logs have a
#prefix — strip it before Import-Csv. The#Fields:line itself starts with a hash, and the field names are space-separated after the colon. Failing to strip the prefix results in the first column being named#Fields:dateinstead ofdate, which silently breaks all downstream references. time-takenis in milliseconds on some IIS versions and seconds on others. IIS 7.0 and earlier logged in seconds. IIS 7.5+ logs in milliseconds. If your slow-request threshold seems wrong, check a request you know took several seconds and verify the logged value matches expectations before setting production thresholds.
Related Cmdlets / See Also
Wrapping Up
Parsing IIS W3C logs with ConvertFrom-Csv, Group-Object, and Sort-Object gives you on-demand traffic analysis without any additional tooling. Once the comment-stripping and header-extraction boilerplate is in place, answering any traffic question is a matter of adding one more pipeline stage.


