Parsing IIS Logs with a Small F# Script

Australian web teams running Microsoft stacks often inherit IIS servers with months of compressed log files. Whether you're maintaining intranet sites for a Brisbane council or running hosting infrastructure out of Melbourne, sifting through W3C fields by hand is tedious work. Logs arrive in u_exYYMMDD.log files inside %SystemDrive%\inetpub\logs\LogFiles, and once you have more than a handful, the built-in IIS Log Viewer in IIS Manager stops being useful. You want something scriptable, repeatable, and quick to tweak.

F# Interactive (FSI) is an excellent fit for this kind of ad-hoc task. Because the language leans on immutability and expression-based syntax, you can pipe data through transformations the way you would in a shell, but with proper types and pattern matching underneath. The .NET SDK ships FSI on every platform, and the same script can later be promoted into a small console app without rewriting the parsing logic.

This piece walks through a minimal F# script that opens an IIS log file, splits each W3C record into fields, filters by status code, and groups hits by endpoint. By the end you'll have a reusable starting point for log forensics, capacity planning, or just answering that urgent Slack message about why the API was throwing 500s at 2am AEST.

Anatomy of a W3C log entry

W3C extended format is the default IIS logging format. Each line begins with a # for comments, and the first non-comment line is a #Fields: directive that names every column. A typical IIS request looks like this:

2024-08-12 23:14:07 192.168.1.42 GET /api/widgets - 80 - 10.0.0.5 Mozilla/5.0 200 0 0 412

The fields you'll reach for most often are date, time, cs-method, cs-uri-stem, sc-status, time-taken, and c-ip. Custom fields like cs(User-Agent) or cs(Referer) appear when the site enables them. Knowing which fields your site emits matters, because the parser needs to index by position rather than name unless you build a field map.

Installing F# Interactive

Install the latest .NET SDK from Microsoft, then open a Command Prompt or PowerShell window and type dotnet fsi. You'll be dropped into an interactive REPL with a > prompt. The script you'll write here can either be typed in directly or saved to a .fsx file and executed with dotnet fsi script.fsx.

Visual Studio Code with the Ionide extension gives you syntax highlighting and inline errors while you edit the script, which is handy if you're new to F#. For one-off analysis, plain dotnet fsi in a terminal is faster — no project file, no build step, just results.

A note for sysadmins in shops where PowerShell is mandatory: FSI scripts can be invoked from PowerShell, and the .NET objects they return are usable directly. So you don't have to choose between the two stacks.

Reading the log file line by line

The first step is to stream the file rather than load it whole. IIS logs can grow into the gigabytes when a busy site has been running for months, which is common for Australian e-commerce sites during end-of-financial-year sales.

open System.IO

let lines =
    File.ReadLines("C:\\inetpub\\logs\\LogFiles\\u_ex240812.log")
    |> Seq.filter (fun l -> not (l.StartsWith "#"))

Seq.filter drops the comment lines that begin with #. At this point lines is a lazy sequence, so no work happens until you iterate. That matters when you're peeking at a 4 GB file and only want the error rows.

Parsing fields with pattern matching

F#'s pattern matching makes short work of splitting a tab-delimited line into a record. A minimal parser looks like this:

type LogEntry = {
    Timestamp: string
    Method: string
    Path: string
    Status: int
    Duration: int
}

let parse (line: string) =
    let parts = line.Split '\t'
    {
        Timestamp = parts.[0] + " " + parts.[1]
        Method = parts.[3]
        Path = parts.[4]
        Status = parts.[8] |> int
        Duration = parts.[10] |> int
    }

Because record fields are immutable, every transform downstream produces a new sequence rather than mutating shared state. That fits the way IIS logs are conceptually immutable — each request happened once and won't change.

Filtering requests by status code

Most of the time you don't want every record, you want the failures. Combining the parser with a filter:

let errors =
    lines
    |> Seq.map parse
    |> Seq.filter (fun e -> e.Status >= 500)

For an Australian retailer whose checkout flow broke at midnight AEST, this line alone is often enough to triage. You can chain additional filters for time windows, paths, or specific subnets — useful when an internal monitoring service in Sydney is hammering a path that real customers never touch.

Active patterns let you give names to the status code buckets:

let (|ClientError|ServerError|Success|Other|) code =
    match code with
    | c when c >= 500 -> ServerError
    | c when c >= 400 -> ClientError
    | c when c >= 200 -> Success
    | _ -> Other

Now a match entry.Status with | ServerError -> ... reads like English.

Aggregating hits and writing them out

Once you have parsed records, grouping by path turns the log into something resembling an analytics dashboard. From there, writing a CSV is a small step.

open System.Text

let hitsByPath =
    lines
    |> Seq.map parse
    |> Seq.groupBy (fun e -> e.Path)
    |> Seq.map (fun (path, entries) ->
        path, Seq.length entries, entries |> Seq.averageBy (fun e -> float e.Duration))
    |> Seq.sortByDescending (fun (_, n, _) -> n)

let writeCsv (rows: seq<string * int * float>) (path: string) =
    let sb = StringBuilder()
    sb.AppendLine "path,count,avg_ms" |> ignore
    for p, n, avg in rows do
        sb.AppendLine(sprintf "%s,%d,%.1f" p n avg) |> ignore
    File.WriteAllText(path, sb.ToString())

writeCsv hitsByPath "C:\\reports\\hits.csv"

Sorting by count surfaces your hottest endpoints first, which is what you want when chasing performance wins on a busy Australian media site or a government portal that's been slammed since 9am AEST. If you'd rather see error rates per endpoint, swap Seq.length for Seq.filter (fun e -> e.Status >= 500) >> Seq.length and divide.

From the CSV you can hand the file to Power BI, paste it into Excel, or feed it into Grafana via the CSV datasource. If you're running this regularly, the script can be wrapped in a small console project and dropped onto a scheduled task on the IIS box itself. Storage planning for those growing log archives is its own discipline — if you're weighing controller cards against ZFS pools for the media server next to your IIS host, hardware versus software RAID lays out the options clearly.

Practical tips for log scripts in F#

  • Open the file with File.ReadLines rather than ReadAllLines so memory stays flat regardless of file size.
  • Keep the parser pure — return a record, never print inside parse.
  • Use Seq.pipe chains (|>) so each step is independently testable.
  • Match on Result rather than throwing exceptions for malformed lines, and accumulate errors separately.
  • Prefer int.TryParse over int when the field might be -, a common value for missing cs-uri-stem.
  • Keep the field index map in a comment at the top of the script — your future self at 2am will thank you.
  • Version-control the .fsx file in the same repo as the IIS site config so changes stay aligned.

If you've been staring at IIS Manager and wishing for a saner way to slice a log, the script above is a starting point. Open a terminal, run dotnet fsi, and start piping. Once you've got it parsing your own logs, you'll find F# pulls double duty as both a quick analysis tool and a foundation for a more serious log pipeline when the time comes.

Karl's blog collects pieces on a wide range of operational and infrastructure topics — and if your interests wander further afield than HTTP status codes, you can also find a curious look at live craps payouts sitting alongside the technical writing.

Experience

Information Technology Consulting

Independent Practice

Provides IT consulting services focused on infrastructure planning, cloud migration strategy, and systems architecture. Engagements draw on years of hands-on sysadmin and development experience across Linux, Windows, and hybrid environments.

K9 Search & Rescue Volunteer

Ongoing

Active participant in K9 Search & Rescue operations, combining technical logistics skills with field support for canine search teams.

Karl Katzke's Blog

October 2006 – May 2014

Published a long-running personal technology blog covering cloud vs. in-house infrastructure, F# and Mono on OSX, hardware vendor critiques, RAID card performance analysis, and sysadmin storytelling. Notable posts include "When Sysadmins Ruled the Earth" (May 15, 2014) and "Getting Started with F# and Mono on OSX" (December 22, 2012).

Credentials

A small badge icon with a shield shape in muted blue tones on a light background

Systems Administration

Deep experience with Linux (RHEL, SLES, CentOS), high-availability clusters, and STONITH configurations.

A small badge icon with a gear shape in muted blue tones on a light background

Cloud Infrastructure

Practical knowledge of AWS EC2, reserved instances, and cost analysis for cloud vs. on-premises deployments.

A small badge icon with a code symbol in muted blue tones on a light background

Development

Proficient in F#, PHP (Symfony), and cross-platform tooling including Mono and MonoDevelop on OSX.

Studies

F# & Functional Programming

Self-directed, 2012

Explored strongly typed functional programming with F# on OSX using the Mono runtime. Published a detailed getting-started guide covering toolchain setup and cross-platform game development research.

High-Availability & Cluster Management

Professional Development, 2009

Configured and documented crm_mon email alerting for STONITH events on SLES11-HAE clusters, integrating with Nagios monitoring for production environments.

Hardware & Storage Performance

Ongoing

Conducted hands-on benchmarking of SATA/SAS RAID controllers including HighPoint RocketRaid 2740 and LSI/SuperMicro AOC-USASLP2-H8iR, comparing against software RAID configurations.

Skills

A small icon representing a server with clean geometric lines in slate blue

Linux Administration

RHEL, SLES, CentOS — package management, kernel tuning, HA clustering, and monitoring integration.

A small icon representing a cloud shape with clean geometric lines in slate blue

Cloud Architecture

AWS EC2, reserved-instance planning, cost modeling, and hybrid infrastructure strategy.

A small icon representing code brackets with clean geometric lines in slate blue

F# & .NET/Mono

Functional programming on OSX, MonoDevelop toolchain, and cross-platform game-dev exploration.

A small icon representing a database cylinder with clean geometric lines in slate blue

PHP & Symfony

Web application development with the Symfony framework and the broader PHP ecosystem.

A small icon representing a storage drive with clean geometric lines in slate blue

Storage & RAID

SATA/SAS controller evaluation, md RAID configuration, and performance benchmarking.

A small icon representing a shield with clean geometric lines in slate blue

High Availability

Pacemaker, STONITH, crm_mon alerting, and Nagios integration for production cluster monitoring.