IIS Application Pool Monitor

At work we run Windows Servers and IIS. Occasionally, one of those app pools will misbehave and shut down without anyone knowing until the client calls in and says their site is down with a 503 error. Embarrassing.

We needed a method of monitoring these and letting us know. In researching the solution, I found that we could even restart them.

Getting Started

Start with an empty Windows Service project and add the installer

create new Windows Service

Next, add your BackgroundWorker and Timer objects.

const string connString = "Data Source=localhost;Initial Catalog=dbname;User ID=dbuser;Password=dbpass;Integrated Security=false;Max Pool Size=200";
BackgroundWorker worker;
Timer timer = new Timer(300_000);

In C# 7, you can use the underscore as a thousands separator. So in this case, I've set it to every 5 minutes. The connString is only needed if you want to use a database for keeping track of app pool starts and make use of notifications. If you plan on notifying every time, then the connection string isn't really needed, either.

Next modify the OnStart method to look like this and create the callback for the BackgroundWorker:

protected override void OnStart(string[] args)
{
    worker = new BackgroundWorker();
    worker.DoWork += Worker_DoWork;
    timer.Elapsed += Timer_Elapsed;
    timer.Start();
}

private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
    worker.RunWorkerAsync();
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
}

Install Dependencies

For the next part, you'll need to install Microsoft.Web.Administration from NuGet. A word of caution - v10 (latest at time of writing) has been rebuilt to use .NET Standard and so will load a lot of extra libraries. This is normal for .NET Standard. I was unable to get this version to work as it kept being unable to load a Services library. Downgrading to v7 and back to .NET Framework fixed this issue. There are probably ways around that, but I haven't taken the time to explore it and v7 fits my needs just fine.

Finish Up

Once that's installed, add this to the body of your Worker_DoWork method (you'll need to add the using Microsoft.Web.Administration; statement):

try
{
    ServerManager manager = new ServerManager();
    ApplicationPoolCollection applicationPoolCollection = manager.ApplicationPools;

    //List<string> pools = new List<string>(); // uncomment to keep track of app pools that you need to be notified about
    foreach (ApplicationPool applicationPool in applicationPoolCollection)
    {
        if (applicationPool.State == ObjectState.Stopped && !applicationPool.Name.ToLower().Contains("trunk"))
        {
            ObjectState state = applicationPool.Start();
            //AddStarted(applicationPool.Name); // record in whatever medium that the app pool has been started
            //if (NeedsNotified(applicationPool.Name)) // determine if service need to notify.
            //    pools.Add(applicationPool.Name);
        }
    }

    // send a notification, using list of pools as the body of the message
}
catch (Exception ex)
{
    // record error to Windows Event Log
    EventLog.WriteEntry("AppPoolMonitor", $"{ex.Message}\n\n{ex.StackTrace}", EventLogEntryType.Error);
}

Build, install, and run!

One other thing to note - a number of the solutions I found included using ServerManager.OpenRemote(machineName); and that didn't work for me. Using the local machine's name it would return null and throw an exception. removing that line fixed it, as I was running the service on the same server as IIS.

Final Thoughts

There's one "gotcha" to be aware of.

I noticed that in testing it, IIS wouldn't register that the app pool was running again when I stopped it manually and the service restarted it for me (the site was back online, IIS just thought it was still stopped). This hasn't been an issue in production - the app pools appear running while running when IIS turns them off automatically.

Our system will notify us if the app pool has been started 5 or more times in the last 30 days. Your mileage may vary.

The table structure I used for the tracking: