IIS log housekeeper service

A simple IIS log rotation Windows service with automatic/manual log location discovery and subfolder traversal for IIS 6 (Windows Server 2003), IIS 7.x (Windows Server 2008) and IIS 8.x (Windows Server 2012). The complete VB.Net source code is included in the download package.

Log rotation event log screenshot
Download IisLogHousekeeper for IIS 6.x

Download IisLogHousekeeper for IIS 7.x / 8.x

Installation instructions:
  1. Download the zip file matching the server's IIS version and extract it to the server's "Program files (x86)" or "Program files" folder.
  2. Open IisLogHousekeeper.exe.config in a text editor and adjust the schedule, the log retention period and optionally the manual log locations (see configuration reference further down this page).
  3. Install and start the service by running ServiceInstall.vbs. The service can be uninstalled using ServiceUninstall.vbs.
  4. Check the service status in the Windows Application Event log. Events will be marked with the name "IisLogHousekeeper".
Microsoft .Net framework 2.0 or newer is required for the service to install and run. During the service installation, the service exe file will be automatically compiled from the source code file using the newest .Net framework version installed on the computer.

The standard disclaimer: The program is provided 'as is' without warranty of any kind. I have done my best to incorporate graceful exception handling and to test the code in different scenarios, so it shouldn't be able to do anything bad or unintended. But I guarantee nothing.

This IIS log housekeeper service is a further development of my old VBScript automatic log housekeeping scripts. But they differ in functionality. If you also need archiving/compression of log files, they should be used instead.

IisLogHousekeeper.exe.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="SimulationMode" value="False"/>
    <add key="RunAt" value="03:00"/>
    <add key="LogRetentionDays" value="30"/>
    <add key="UseManualLogLocations" value="False"/>
    <add key="ManualLogLocations" value="C:\inetpub\logs\LogFiles|C:\WINDOWS\system32\LogFiles"/>
  </appSettings>
</configuration>

IisLogHousekeeper.vb (for IIS 6.x):

Imports System
Imports System.DirectoryServices
Imports System.Text
Imports System.IO
Imports System.Configuration
Imports System.Threading
Imports System.ServiceProcess
Imports System.Diagnostics
Imports System.Reflection

<Assembly: AssemblyTitle("IisLogHousekeeper 1.1 IIS6x edition")>
<Assembly: AssemblyDescription("IIS log housekeeping service")>
<Assembly: AssemblyCompany("www.700c.dk")>
<Assembly: AssemblyProduct("IisLogHousekeeper")>
<Assembly: AssemblyCopyright("Creative Commons Attribution (CC BY 3.0) https://creativecommons.org/licenses/by/3.0/ Reference source https://www.700c.dk/?code-iis-log-housekeeper-service")>
<Assembly: AssemblyVersion("1.1.0.0")>
<Assembly: CLSCompliant(True)>

Public Class IisLogHousekeeper

  Private Shared ServiceThread As Thread
  Private Shared KeepRunning As Boolean = True

  Protected Overrides Sub OnStart(ByVal args() As String)
    WriteToEventLog("IisLogHousekeeper starting", 1110)
    ServiceThread = New Thread(AddressOf RunWorkerThread)
    ServiceThread.Start()
  End Sub

  Protected Overrides Sub OnStop()
    KeepRunning = False
    WriteToEventLog( _
      "IisLogHousekeeper stopping. Please note: the worker process " & _
      "will live on for up to 60 seconds before it terminates.", 1110)
  End Sub

  Public Sub RunWorkerThread()
    Dim aFolders() As String = {}
    If LCase(ConfigurationManager.AppSettings("UseManualLogLocations")) = "true" Then
      aFolders = Split(ConfigurationManager.AppSettings("ManualLogLocations"), "|")
    End If
    Dim sRunResult As String = ""
    Do While KeepRunning
      If ConfigurationManager.AppSettings("RunAt") = Date.Now.ToString("HH:mm") Then
        If LCase(ConfigurationManager.AppSettings("UseManualLogLocations")) = "false" Then
          Try
            aFolders = GetIisLogFoldersThroughADSI()
          Catch e As Exception
            WriteToEventLog("Could not get IIS log locations through ADSI. Error message: " & e.Message, 1112)
          End Try
        End If
        Try
          For i As Integer = 0 to UBound(aFolders)
            sRunResult = sRunResult & _
              DeleteLogFilesRecursive(aFolders(i), CInt(ConfigurationManager.AppSettings("LogRetentionDays")))
          Next
          If sRunResult <> "" Then
            WriteToEventLog(sRunResult, 1111)
            sRunResult = ""
          End If
        Catch e As Exception
          WriteToEventLog("An error occurred while cleaning up logs: " & e.Message, 1112)
        End Try
      End If
      Thread.Sleep(60000)
    Loop
  End Sub

  Public Function WriteToEventLog(ByVal Entry As String, _
     Optional ByVal EventID As Integer = 0, _
     Optional ByVal AppName As String = "IisLogHousekeeper", _
     Optional ByVal EventType As  _
     EventLogEntryType = EventLogEntryType.Information, _
     Optional ByVal LogName As String = "Application") As Boolean
    Dim objEventLog As New EventLog()
    Try
      If Not Diagnostics.EventLog.SourceExists(AppName) Then
        Diagnostics.EventLog.CreateEventSource(AppName, LogName)
      End If
      objEventLog.Source = AppName
      objEventLog.WriteEntry(Entry, EventType, EventID)
      Return True
    Catch Ex As Exception
      Return False
    End Try
    objEventLog.Dispose()
  End Function

  Public Function GetIisLogFoldersThroughADSI() As String()
    Dim sFolderList As String = ""
    Dim deIisRootObject As New DirectoryEntry("IIS://localhost")
    Dim deIisSubItems As DirectoryEntry
    Dim deIisNodeObj As DirectoryEntry
    For Each deIisSubItems in deIisRootObject.Children
      If LCase(deIisSubItems.SchemaClassName) = "iiswebservice" Then
        Dim deIisService As New DirectoryEntry("IIS://localhost/W3SVC")
        For Each deIisNodeObj in deIisService.Children
          If LCase(deIisNodeObj.SchemaClassName) = "iiswebserver" Then
            sFolderList = sFolderList & _
              deIisNodeObj.Properties("LogFileDirectory").Value.ToString() & _
              "\W3SVC" & deIisNodeObj.Name & "|"
          End If
        Next
      ElseIf LCase(deIisSubItems.SchemaClassName) = "iissmtpservice" Then
        Dim deIisService As New DirectoryEntry("IIS://localhost/SMTPSVC")
        For Each deIisNodeObj in deIisService.Children
          If LCase(deIisNodeObj.SchemaClassName) = "iissmtpserver" Then
            sFolderList = sFolderList & _
              deIisNodeObj.Properties("LogFileDirectory").Value.ToString() & _
              "\SMTPSVC" & deIisNodeObj.Name & "|"
          End If
        Next
      ElseIf LCase(deIisSubItems.SchemaClassName) = "iisftpservice" Then
        Dim deIisService As New DirectoryEntry("IIS://localhost/MSFTPSVC")
        For Each deIisNodeObj in deIisService.Children
          If LCase(deIisNodeObj.SchemaClassName) = "iisftpserver" Then
            sFolderList = sFolderList & _
              deIisNodeObj.Properties("LogFileDirectory").Value.ToString() & _
              "\MSFTPSVC" & deIisNodeObj.Name & "|"
          End If
        Next
      End If
    Next
    If Right(sFolderList, 1) = "|" Then
      sFolderList = Left(sFolderList, Len(sFolderList) - 1)
    End If
    Return Split(sFolderList, "|")
  End Function

  Public Function DeleteLogFilesRecursive(ByVal sLogPath As String, ByVal iDelAge As Integer) As String
    Dim sReturn As String = ""
    If Directory.Exists(sLogPath)
      Dim dInfo As New DirectoryInfo(sLogPath)
      Dim fInfo as FileSystemInfo
      Dim sTempString As String = ""
      For Each fInfo In dInfo.GetFileSystemInfos()
        If fInfo.Attributes = FileAttributes.Directory Then
          sReturn = sReturn & DeleteLogFilesRecursive(fInfo.FullName, iDelAge)
        Else
          If (InStr(fInfo.FullName, "ex") > 0) And (Right(fInfo.FullName, 4) = ".log") Then
            If DateDiff("d", fInfo.LastWriteTime, Now) > iDelAge Then
              Try
                sTempString = fInfo.FullName & " deleted." & VbCrLf
                If LCase(ConfigurationManager.AppSettings("SimulationMode")) = "false" Then
                  File.Delete(fInfo.FullName)
                  sReturn = sReturn & sTempString
                Else
                  sReturn = sReturn & "[Simulation mode enabled] " & sTempString
                End If
              Catch e As Exception
                sReturn = sReturn & fInfo.FullName & " could not be deleted. Error message: " & e.Message & VbCrLf
              End Try
            End If
          End If
        End If
      Next
    End If
    Return sReturn
  End Function

End Class

<System.ComponentModel.RunInstaller(True)> _
Public Class ProjectInstaller
  Inherits System.Configuration.Install.Installer

  Public Sub New()
    MyBase.New()
    InitializeComponent()
  End Sub

  Private components As System.ComponentModel.IContainer

  Private Sub InitializeComponent()
    Me.ServiceProcessInstaller1 = New ServiceProcessInstaller
    Me.ServiceInstaller1 = New ServiceInstaller
    Me.ServiceProcessInstaller1.Account = ServiceAccount.LocalSystem
    Me.ServiceProcessInstaller1.Password = Nothing
    Me.ServiceProcessInstaller1.Username = Nothing
    Me.ServiceInstaller1.ServiceName = "IisLogHousekeeper"
    Me.ServiceInstaller1.StartType = ServiceStartMode.Automatic
    Me.Installers.AddRange(New System.Configuration.Install.Installer() {Me.ServiceProcessInstaller1, Me.ServiceInstaller1})
  End Sub
  Friend WithEvents ServiceProcessInstaller1 As ServiceProcessInstaller
  Friend WithEvents ServiceInstaller1 As ServiceInstaller

End Class

<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class IisLogHousekeeper
  Inherits System.ServiceProcess.ServiceBase

  <System.Diagnostics.DebuggerNonUserCode()> _
  Protected Overrides Sub Dispose(ByVal disposing As Boolean)
    Try
      If disposing AndAlso components IsNot Nothing Then
        components.Dispose()
      End If
    Finally
      MyBase.Dispose(disposing)
    End Try
  End Sub

  <MTAThread()> _
  <System.Diagnostics.DebuggerNonUserCode()> _
  Shared Sub Main()
    Dim ServicesToRun() As System.ServiceProcess.ServiceBase
    ServicesToRun = New System.ServiceProcess.ServiceBase() {New IisLogHousekeeper}
    System.ServiceProcess.ServiceBase.Run(ServicesToRun)
  End Sub

  Private components As System.ComponentModel.IContainer

  <System.Diagnostics.DebuggerStepThrough()> _
  Private Sub InitializeComponent()
    components = New System.ComponentModel.Container()
    Me.ServiceName = "IisLogHousekeeper"
  End Sub

End Class

IisLogHousekeeper.vb (for IIS 7.x and 8.x):

Imports System
Imports System.DirectoryServices
Imports System.Text
Imports System.IO
Imports System.Configuration
Imports System.Threading
Imports System.ServiceProcess
Imports System.Diagnostics
Imports System.Reflection
Imports Microsoft.Web.Administration

<Assembly: AssemblyTitle("IisLogHousekeeper 1.1 for IIS 7.x / 8.x")>
<Assembly: AssemblyDescription("IIS log housekeeping service (IIS 7/8 edition)")>
<Assembly: AssemblyCompany("www.700c.dk")>
<Assembly: AssemblyProduct("IisLogHousekeeper")>
<Assembly: AssemblyCopyright("Creative Commons Attribution (CC BY 3.0) https://creativecommons.org/licenses/by/3.0/ Reference source https://www.700c.dk/?code-iis-log-housekeeper-service")>
<Assembly: AssemblyVersion("1.1.0.0")>
<Assembly: CLSCompliant(True)>

Public Class IisLogHousekeeper

  Private Shared ServiceThread As Thread
  Private Shared KeepRunning As Boolean = True

  Protected Overrides Sub OnStart(ByVal args() As String)
    WriteToEventLog("IisLogHousekeeper starting", 1110)
    ServiceThread = New Thread(AddressOf RunWorkerThread)
    ServiceThread.Start()
  End Sub

  Protected Overrides Sub OnStop()
    KeepRunning = False
    WriteToEventLog( _
      "IisLogHousekeeper stopping. Please note: the worker process " & _
      "will live on for up to 60 seconds before it terminates.", 1110)
  End Sub

  Public Sub RunWorkerThread()
    Dim aFolders() As String = {}
    If LCase(ConfigurationManager.AppSettings("UseManualLogLocations")) = "true" Then
      aFolders = Split(ConfigurationManager.AppSettings("ManualLogLocations"), "|")
    End If
    Dim sRunResult As String = ""
    Do While KeepRunning
      If ConfigurationManager.AppSettings("RunAt") = Date.Now.ToString("HH:mm") Then
        If LCase(ConfigurationManager.AppSettings("UseManualLogLocations")) = "false" Then
          Try
            aFolders = GetIisLogFolders()
          Catch e As Exception
            WriteToEventLog("Could not get IIS log locations through the IIS API. Error message: " & e.Message, 1112)
          End Try
        End If
        Try
          For i As Integer = 0 to UBound(aFolders)
            sRunResult = sRunResult & _
              DeleteLogFilesRecursive(aFolders(i), CInt(ConfigurationManager.AppSettings("LogRetentionDays")))
          Next
          If sRunResult <> "" Then
            WriteToEventLog(sRunResult, 1111)
            sRunResult = ""
          End If
        Catch e As Exception
          WriteToEventLog("An error occurred while cleaning up logs: " & e.Message, 1112)
        End Try
      End If
      Thread.Sleep(60000)
    Loop
  End Sub

  Public Function WriteToEventLog(ByVal Entry As String, _
     Optional ByVal EventID As Integer = 0, _
     Optional ByVal AppName As String = "IisLogHousekeeper", _
     Optional ByVal EventType As  _
     EventLogEntryType = EventLogEntryType.Information, _
     Optional ByVal LogName As String = "Application") As Boolean
    Dim objEventLog As New EventLog()
    Try
      If Not Diagnostics.EventLog.SourceExists(AppName) Then
        Diagnostics.EventLog.CreateEventSource(AppName, LogName)
      End If
      objEventLog.Source = AppName
      objEventLog.WriteEntry(Entry, EventType, EventID)
      Return True
    Catch Ex As Exception
      Return False
    End Try
    objEventLog.Dispose()
  End Function

  Public Function GetIisLogFolders() As String()
    Dim sFolderList As String = ""
    Dim mgr As New ServerManager()
    For Each oWebsite As Site In mgr.Sites
      sFolderList = sFolderList & oWebsite.LogFile.Directory & "\W3SVC" & oWebsite.Id.ToString() & "|"
    Next
    If Right(sFolderList, 1) = "|" Then
      sFolderList = Left(sFolderList, Len(sFolderList) - 1)
    End If
    ' Yes, the following replacement is an ugly hack. The later file handling functions don't like the environment variable format the IIS API returns:
    sFolderList = Replace(sFolderList, "%SystemDrive%", "C:")
    Return Split(sFolderList, "|")
  End Function

  Public Function DeleteLogFilesRecursive(ByVal sLogPath As String, ByVal iDelAge As Integer) As String
    Dim sReturn As String = ""
    If Directory.Exists(sLogPath)
      Dim dInfo As New DirectoryInfo(sLogPath)
      Dim fInfo as FileSystemInfo
      Dim sTempString As String = ""
      For Each fInfo In dInfo.GetFileSystemInfos()
        If fInfo.Attributes = FileAttributes.Directory Then
          sReturn = sReturn & DeleteLogFilesRecursive(fInfo.FullName, iDelAge)
        Else
          If (InStr(fInfo.FullName, "ex") > 0) And (Right(fInfo.FullName, 4) = ".log") Then
            If DateDiff("d", fInfo.LastWriteTime, Now) > iDelAge Then
              Try
                sTempString = fInfo.FullName & " deleted." & VbCrLf
                If LCase(ConfigurationManager.AppSettings("SimulationMode")) = "false" Then
                  File.Delete(fInfo.FullName)
                  sReturn = sReturn & sTempString
                Else
                  sReturn = sReturn & "[Simulation mode enabled] " & sTempString
                End If
              Catch e As Exception
                sReturn = sReturn & fInfo.FullName & " could not be deleted. Error message: " & e.Message & VbCrLf
              End Try
            End If
          End If
        End If
      Next
    End If
    Return sReturn
  End Function

End Class

<System.ComponentModel.RunInstaller(True)> _
Public Class ProjectInstaller
  Inherits System.Configuration.Install.Installer

  Public Sub New()
    MyBase.New()
    InitializeComponent()
  End Sub

  Private components As System.ComponentModel.IContainer

  Private Sub InitializeComponent()
    Me.ServiceProcessInstaller1 = New ServiceProcessInstaller
    Me.ServiceInstaller1 = New ServiceInstaller
    Me.ServiceProcessInstaller1.Account = ServiceAccount.LocalSystem
    Me.ServiceProcessInstaller1.Password = Nothing
    Me.ServiceProcessInstaller1.Username = Nothing
    Me.ServiceInstaller1.ServiceName = "IisLogHousekeeper"
    Me.ServiceInstaller1.StartType = ServiceStartMode.Automatic
    Me.Installers.AddRange(New System.Configuration.Install.Installer() {Me.ServiceProcessInstaller1, Me.ServiceInstaller1})
  End Sub
  Friend WithEvents ServiceProcessInstaller1 As ServiceProcessInstaller
  Friend WithEvents ServiceInstaller1 As ServiceInstaller

End Class

<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class IisLogHousekeeper
  Inherits System.ServiceProcess.ServiceBase

  <System.Diagnostics.DebuggerNonUserCode()> _
  Protected Overrides Sub Dispose(ByVal disposing As Boolean)
    Try
      If disposing AndAlso components IsNot Nothing Then
        components.Dispose()
      End If
    Finally
      MyBase.Dispose(disposing)
    End Try
  End Sub

  <MTAThread()> _
  <System.Diagnostics.DebuggerNonUserCode()> _
  Shared Sub Main()
    Dim ServicesToRun() As System.ServiceProcess.ServiceBase
    ServicesToRun = New System.ServiceProcess.ServiceBase() {New IisLogHousekeeper}
    System.ServiceProcess.ServiceBase.Run(ServicesToRun)
  End Sub

  Private components As System.ComponentModel.IContainer

  <System.Diagnostics.DebuggerStepThrough()> _
  Private Sub InitializeComponent()
    components = New System.ComponentModel.Container()
    Me.ServiceName = "IisLogHousekeeper"
  End Sub

End Class
Tags: software
Page last updated 2013-04-12 21:18. Some rights reserved (CC by 3.0)