Sunday, February 12, 2017

Delete IIS Logs Remotely using PowerShell

There are many scripts out there using a variety of methods to delete IIS logs from servers using PowerShell. These scripts are usually written to run locally on servers, which can have two drawbacks:
  1. In environments with a large number of servers, scheduling and maintaining these scripts can require a significant amount of work;
  2. Depending on the security restrictions on production servers, administrators might not be able to schedule this script to run whether they are logged on to the server or not:

This was the scenario I found recently. In order to overcome both obstacles, I decided to run the script from a “script server” where security settings did not prevent cached credentials. Additionally, by deleting these logs remotely I could have a single script targeting multiple servers, making it much easier to manage!

As I have mentioned, there are many ways of deleting IIS logs from a server. The method I have been using lately uses the WebAdministration module and the Get-WebSite cmdlet to get a list of all websites on the local server:

For each website, we can easily check where its logs are being saved to:

So both websites are saving their logs to the same location?! No :) If we use IIS Manager, this path is what we see in the config of the website:

But remember that IIS then creates a subfolder named W3SVC1 (for example) and saves the logs there. This way, each website has a unique log folder. The W3SVCx number refers to the website’s ID. For example, the Default Web Site is usually ID 1, so the log directory would be W3SVC1. The Exchange Back End site will be ID 2 (W3SVC2) and so on. This can be verified in the previous screenshot.

So now, all we have to do is append “\W3SVC” plus the website ID to construct the file path:

It’s also worth adding “.Replace("%SystemDrive%", $env:SystemDrive)” in case the logs are stored in the default location. This way the full path will be “C:\inetpub\logs\LogFiles\W3SVC1” instead of “%SystemDrive%\inetpub\logs\LogFiles\W3SVC1”.

Once we know the location of the files, we can easily delete all that are older than 7 days using the following code:
Get-ChildItem -Path  -Recurse | Where {$_.LastWriteTime -lt (Get-Date).addDays(-7)} | ForEach {del $_.FullName -Confirm:$False}


At the end, the basic script looks like this:
Import-Module WebAdministration

ForEach($webSite in $(Get-WebSite)) {
$dir = "$($webSite.logFile.directory)\W3SVC$($webSite.ID)".Replace("%SystemDrive%", $env:SystemDrive)

 Write-Host "Deleting IIS logs from $dir" -ForegroundColor Green
 Get-ChildItem -Path $dir -Recurse | Where {$_.LastWriteTime -lt (Get-Date).addDays(-7)} | ForEach {del $_.FullName -Confirm:$False}
}


This is how we would delete IIS logs for all websites on a local server. But what about if we want to delete those logs remotely? Easy! We use Invoke-Command.

The Invoke-Command cmdlet runs commands on a local or remote computer and returns all output from the commands, including errors. By using a single Invoke-Command command, we can run commands on multiple computers.

Using this cmdlet, we easily run the code above against multiple servers using the following code:
Invoke-Command -ComputerName “server1”, “server2”, “server3” -ScriptBlock {
  Import-Module WebAdministration

  ForEach($webSite in $(Get-WebSite)) {
  $dir = "$($webSite.logFile.directory)\W3SVC$($webSite.ID)".Replace("%SystemDrive%", $env:SystemDrive)

    Write-Host "Deleting IIS logs from $dir" -ForegroundColor Green
    Get-ChildItem -Path $dir -Recurse | Where {$_.LastWriteTime -lt (Get-Date).addDays(-7)} | ForEach {del $_.FullName -Confirm:$False}
  }
}

Simple as that! :)



This code can be significantly improved by passing credentials to the Invoke-Command, by checking if a server is reachable before trying to run a cmdlet against it, and by adding some logging and error handling.

In my case, instead of passing a list of servers using the -ComputerName parameter, I chose to create an array with all my servers, and then process them one by one so I could more easily test connectivity to the server and deal with any errors.
We could also make this part of user input to allow users to specify which servers to action on more easily. Modules would be the next step :)

The final script, which is also available in TechNet Gallery, looks like this:
<#
.SYNOPSIS
Delete IIS log files from remote server

.DESCRIPTION
The script retrieves the location of IIS logs for all websites on a remote server and deletes those older than $Days days.

.PARAMETER Days
Specifies the number of days’ worth of IIS logs to keep on the server

.EXAMPLE
Deletes IIS logs older than 14 days from all servers manually specified within the script's $excServers array
.\Delete-IISlogs.ps1 -Days 14


.NOTES
Name:     Delete-IISlogs.ps1
Author:   Nuno Mota

.LINK
https://letsexchange.blogspot.com
https://gallery.technet.microsoft.com/Delete-IIS-Logs-Remotely-9d269a30
#>



[CmdletBinding()]
Param (
 [Parameter(Position = 0, Mandatory = $False)]
 [Int] $Days = 7
)


Function Write-Log {
 [CmdletBinding()]
 Param ([String] $Type, [String] $Message)

 # Create a log file in the same location as the script containing all the actions taken
 $Logfile = $PSScriptRoot + "\Delete-IISlogs_Log_$(Get-Date -f 'yyyyMMdd').txt"
 If (!(Test-Path $Logfile)) {New-Item $Logfile -Force -ItemType File | Out-Null}

 $timeStamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
 "$timeStamp $Type $Message" | Out-File -FilePath $Logfile -Append
 
 Write-Verbose $Message
}



#################################################################
# Script Start
#################################################################

[Array] $excServers = @("server1", "server2", "server3", "server4")
ForEach ($server in $excServers) {
 Write-Log -Type "INF" -Message "Processing $server"
 
 If (Test-Connection -ComputerName $server -BufferSize 16 -Count 1 -ErrorAction 0 -Quiet) {
  Try {
   $countDel = Invoke-Command -ComputerName $server -ArgumentList $Days, $server -ScriptBlock {
    param($Days, $server)
    
    [Int] $countDel = 0
    Import-Module WebAdministration
    ForEach($webSite in $(Get-WebSite)) {
        $dir = "$($webSite.logFile.directory)\W3SVC$($webSite.ID)".Replace("%SystemDrive%", $env:SystemDrive)
     
     Write-Host "Checking IIS logs in $dir on $server" -ForegroundColor Green
     Get-ChildItem -Path $dir -Recurse | ? {$_.LastWriteTime -lt (Get-Date).addDays(-$Days)} | ForEach {
      Write-Host "Deleting", $_.FullName
      del $_.FullName -Confirm:$False
      $countDel++
     }
    }
    
    Return $countDel
   }
   
   Write-Log -Type "INF" -Message "Deleted $countDel logs from server $server"
  } Catch {
   Write-Log -Type "ERR" -Message "Unable to connect to $($server): $($_.Exception.Message)"
   Send-MailMessage -From "ExchangeAdmin@domain.com” -To "user@domain.com" -Subject "ERROR – Delete IIS Logs" -Body "Unable to connect to $($server): $($_.Exception.Message)" -SmtpServer smtp.domain.com -Priority "High"
  }
 } Else {
  Write-Log -Type "ERR" -Message "Unable to connect to $server"
  Send-MailMessage -From "ExchangeAdmin@domain.com” -To "user@domain.com" -Subject "ERROR – Delete IIS Logs" -Body "Unable to connect to $server" -SmtpServer smtp.domain.com -Priority "High"
 }
}

Please be aware that we could add even more error handling for cases where we are unable to load the WebAdministration module or delete the files for example.

No comments:

Post a Comment