PortiBlog

System report of your SharePoint farm

15 juli 2016

To help out a client I created this script to send out an email with information about the health and status of the SharePoint Farm. I've created 2 scripts to complete the e-mail.

The first one will gather the current amount of memory used, which should run every 15 minutes. You can provision this by making a new Scheduled task. 

Let the task run every 15 minutes for the whole day. If you change this amount, the report will show less or more bars in the chart. 15 minutes would give a good feel on the memory usage.

The second script can be scheduled too, but should only run once a day, I would suggest at 23:47. This way you enclose all the data of that day, and the report will be send to you before any new data is saved by the first script. 

As input for both scripts we need to create an input file called List.txt. This file should contain all your servers in the Farm, including your Office Web Apps Server. Make sure all servers are on a separate line and the service account running the scripts has access to the servers (Local admin). Save the file in the same directory as the script, ex.: D:\Scripts\Systemreport.

GetMemory.ps1

This is the script that should run every 15 minutes. It will save the current usage of memory in a text file on the server the script is running, one file for every server in the input file.

$list = $args[0] #This accepts the argument you add to your scheduled task for the list of servers. i.e. list.txt
$computers = get-content $list #grab the names of the servers/computers to check from the list.txt file.
$location = 'D:\Scripts\Systemreport\' #Location the file will be saved to
$time = Get-Date -Format HHmm

foreach ($computer in $computers) {
$OS = (Get-WmiObject Win32_OperatingSystem -computername $computer).caption
$SystemInfo = Get-WmiObject -Class Win32_OperatingSystem -computername $computer | Select-Object Name, TotalVisibleMemorySize, FreePhysicalMemory
$TotalRAM = $SystemInfo.TotalVisibleMemorySize/1MB
$FreeRAM = $SystemInfo.FreePhysicalMemory/1MB
$UsedRAM = $TotalRAM - $FreeRAM
$FreeRAM = [Math]::Round($FreeRAM, 2)
$UsedRAM = [Math]::Round($UsedRAM, 2)

$ToOutput = "$time=$UsedRAM"
$ReportLocation = $location + $computer

Out-File -FilePath "$ReportLocation-memory.txt" -InputObject $ToOutput -Append
}

Call the script in the scheduled task as follows: D:\Scripts\Systemreport\GetMemory.ps1 D:\Scripts\Systemreport\list.txt

Systemsreport.ps1

Now we will make a new PowerShell script, which will gather all the information and send it to the provided mail addresses.

In this section we will set the variables and get the arguments for the rest of the file:

# PowerShell Systems Report
# Example usage: D:\Scripts\Systemreport\SystemsReport.ps1 D:\Scripts\Systemreport\list.txt
# Remember that list.txt is the file containing a list of Server names to run this against

Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue

#region Variables and Arguments
$users = "someone@mail.com" # List of users to email your report to (separate by comma)
$fromemail = "sender@mail.com"
$subject = "Systems Report of Farm X"
$server = "mailserver" #enter your own SMTP server DNS name / IP address here
$list = $args[0] #This accepts the argument you add to your scheduled task for the list of servers. i.e. list.txt
$computers = get-content $list #grab the names of the servers/computers to check from the list.txt file.
$OWAServer = "OWA01" #Office Web Apps server, empty when none is used

# Set free disk space threshold below in percent (default at 10%)
$thresholdspace = 20
$location = 'D:\Scripts\Systemreport\'  #Location the file will be saved to
[int]$EventNum = 3
[int]$ProccessNumToFetch = 10
$ListOfAttachments = @()
$Report = @()
$CurrentTime = Get-Date
#endregion

If you don't use Office Web Apps, clear the variable and it will not be mentioned in the mail.

We use 2 functions for making different charts, and a function to calculate the up-time of the server:


Function Create-TableChart() {
	param([string]$Report)
		
	[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
	[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")
	
	#Create our chart object 
	$Chart = New-object System.Windows.Forms.DataVisualization.Charting.Chart 
	$Chart.Width = 800
	$Chart.Height = 300 
	$Chart.Left = 10
	$Chart.Top = 10

	#Create a chartarea to draw on and add this to the chart 
	$ChartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
    $ChartArea.AxisY.Maximum =$TotalRAM
	$Chart.ChartAreas.Add($ChartArea) 
	[void]$Chart.Series.Add("Data") 

    # add data to chart
    $data = [ordered]@{}
    $ReportLocation = $location + $Report
    $data += ConvertFrom-StringData ([io.file]::ReadAllText("$ReportLocation-memory.txt")) 
    $hash = [ordered]@{}

    foreach ($h in $data.GetEnumerator() | Sort -Property name) { $hash.add($h.key, $h.value) }

	#Add a datapoint for each value specified in the arguments (args) 
	$Chart.Series["Data"].Points.DataBindXY($hash.Keys, $hash.Values)

    $Chart.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right -bor 
                    [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left 

    $Stripline1 = New-object System.Windows.Forms.DataVisualization.Charting.StripLine
    $Stripline1.BackColor = [System.Drawing.Color]::Red 
    $Stripline1.BackSecondaryColor = [System.Drawing.Color]::Red	
    $Stripline1.Text = "Advised max usage"
    $Stripline1.IntervalOffset = $TotalRAM*0.8
    $Stripline1.Interval = 0
    $Stripline1.StripWidth = 0.05

    $ChartArea.AxisY.StripLines.Add($StripLine1)

	#Set the title of the Chart to the current date and time 
	[void]$Chart.Titles.Add("Memory used last 24 hours") 
    $ChartArea.AxisX.Title = "Hours" 
    $ChartArea.AxisY.Title = "Usage"
    
	#Save the chart to a file
	$Chart.SaveImage("$ReportLocation-chart.png","png")
    Remove-Item "$ReportLocation-memory.txt"
}

Function Create-PieChart() {
	param([string]$Report)
		
	[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
	[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")
	
	#Create our chart object 
	$Chart = New-object System.Windows.Forms.DataVisualization.Charting.Chart 
	$Chart.Width = 300
	$Chart.Height = 200 
	$Chart.Left = 10
	$Chart.Top = 10

	#Create a chartarea to draw on and add this to the chart 
	$ChartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
	$Chart.ChartAreas.Add($ChartArea) 
	[void]$Chart.Series.Add("Data") 

    # add data to chart
	#Add a datapoint for each value specified in the arguments (args) 
	$Chart.Series["Data"].ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Pie
    $Chart.Series["Data"].Points.DataBindXY($DiskItemsData,$DiskItems)
    $Chart.Series["Data"]["PieLabelStyle"] = "Outside"
    $Chart.Series["Data"]["PieLineColor"] = "Black"
    $Chart.Series["Data"]["PieDrawingStyle"] = "Concave"

    $Chart.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right -bor 
                    [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left 


	#Set the title of the Chart to the current date and time 
	[void]$Chart.Titles.Add("Disk usage $DiskID") 
    $Chart.Titles[0].Font = "ariel,14pt"
    $Chart.Titles[0].Alignment = "topCenter"
	#Save the chart to a file
    $ReportLocation = $location + $Report
	$Chart.SaveImage("$ReportLocation-disk-" + $DiskID + ".png","png")
}

Function Get-HostUptime {
	param ([string]$ComputerName)
	$Uptime = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName
	$LastBootUpTime = $Uptime.ConvertToDateTime($Uptime.LastBootUpTime)
	$Time = (Get-Date) - $LastBootUpTime
	Return '{0:00} Days, {1:00} Hours, {2:00} Minutes, {3:00} Seconds' -f $Time.Days, $Time.Hours, $Time.Minutes, $Time.Seconds
}
 

Now lets get all the SharePoint Health analyzer items:


# Get the health of the SharePoint farm
$ReportsList = [Microsoft.SharePoint.Administration.Health.SPHealthReportsList]::Local
$FormUrl = '{0}{1}?id=' -f $ReportsList.ParentWeb.Url, $ReportsList.Forms.List.DefaultDisplayFormUrl
 
[array]$body = $Null

$ReportsList.Items | Where-Object {$_['Severity'] -ne '4 - Success'}  | ForEach-Object {
 
   $item = New-Object PSObject 
   $item | Add-Member NoteProperty Severity $_['Severity']
   $item | Add-Member NoteProperty Category $_['Category']
   $item | Add-Member NoteProperty Description "$($_['Title'])"
  #$item | Add-Member NoteProperty Explanation $_['Explanation']
  #$item | Add-Member NoteProperty Modified $_['Modified']
   $item | Add-Member NoteProperty FailingServers $_['Failing Servers']
   $item | Add-Member NoteProperty FailingServices $_['Failing Services']
  #$item | Add-Member NoteProperty Remedy $_['Remedy']
   
   $body += $item
    
   }
 
# creating clickable HTML links
$body = $body.GetEnumerator() | Sort Severity, Category
$body = $body | ConvertTo-Html | Out-String 
$body = $body -replace '<','' -replace '"','"'
 

If you use Office Web Apps, get the machine information (including the health which we use further on):


if ($OWAServer)
{  
    $owahealth = Invoke-Command -ComputerName $OWAServer -ScriptBlock { Get-officeWebAppsMachine }
}
Pre set HTML settings, for the markup of the e-mail:

$HTMLMiddle = ""
# Assemble the HTML Header and CSS for our Report
$HTMLHeader = @"

<title>My Systems Report</title>

<!--
body {
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
}

    #report { width: 835px; }

    table{
	border-collapse: collapse;
	border: none;
	font: 10pt Verdana, Geneva, Arial, Helvetica, sans-serif;
	color: black;
	margin-bottom: 10px;
}

    table td{
	font-size: 12px;
	padding-left: 0px;
	padding-right: 20px;
	text-align: left;
}

    table th {
	font-size: 12px;
	font-weight: bold;
	padding-left: 0px;
	padding-right: 20px;
	text-align: left;
}

h2{ clear: both; font-size: 130%; }

h3{
	clear: both;
	font-size: 115%;
	margin-left: 20px;
	margin-top: 30px;
}

p{ margin-left: 20px; font-size: 12px; }

table.list{ float: left; }

    table.list td:nth-child(1){
	font-weight: bold;
	border-right: 1px grey solid;
	text-align: right;
}

table.list td:nth-child(2){ padding-left: 7px; }
table tr:nth-child(even) td:nth-child(even){ background: #CCCCCC; }
table tr:nth-child(odd) td:nth-child(odd){ background: #F2F2F2; }
table tr:nth-child(even) td:nth-child(odd){ background: #DDDDDD; }
table tr:nth-child(odd) td:nth-child(even){ background: #E5E5E5; }
div.column { width: 320px; float: left; }
div.first{ padding-right: 20px; border-right: 1px  grey solid; }
div.second{ margin-left: 30px; }
table{ margin-left: 20px; }
-->
"@
 

Now lets loop though every server and gather information.
Get disk information for every server. This will also create the charts for the disks:


foreach ($computer in $computers) {

	$DiskInfo= Get-WMIObject -ComputerName $computer Win32_LogicalDisk | Where-Object{$_.DriveType -eq 3}
    $Disks = @()
    foreach ($disk in $DiskInfo)
    {
        $DiskSize = [Math]::Round($disk.Size/1GB, 2)
        $FreeDisk = [Math]::Round($disk.FreeSpace/1GB, 2)
        $UsedDisk = $DiskSize - $FreeDisk
        $DiskItems = @($FreeDisk, $UsedDisk)
        $DiskItemsData = @("Free $FreeDisk GB","Used $UsedDisk GB")
        $DiskID = $disk.DeviceID.Replace(":","")
      	$Disks += $DiskID
        Create-PieChart -Report $computer
	    $ListOfAttachments += $location + $computer +"-disk-$DiskID.png"
    }
    $DiskChart = ""
    foreach ($drive in $Disks)
    {
        $DiskChart += ''+$computer+' Disk'
    }
    $DiskChart += ""
 

Get information about the memory, at this moment:


#region System Info
	$OS = (Get-WmiObject Win32_OperatingSystem -computername $computer).caption
	$SystemInfo = Get-WmiObject -Class Win32_OperatingSystem -computername $computer | Select-Object Name, TotalVisibleMemorySize, FreePhysicalMemory
	$TotalRAM = $SystemInfo.TotalVisibleMemorySize/1MB
	$FreeRAM = $SystemInfo.FreePhysicalMemory/1MB
	$UsedRAM = $TotalRAM - $FreeRAM
	$RAMPercentFree = ($FreeRAM / $TotalRAM) * 100
	$TotalRAM = [Math]::Round($TotalRAM, 2)
	$FreeRAM = [Math]::Round($FreeRAM, 2)
	$UsedRAM = [Math]::Round($UsedRAM, 2)
	$RAMPercentFree = [Math]::Round($RAMPercentFree, 2)
	#endregion
	

Get processes that use the most memory:


$TopProcesses = Get-Process -ComputerName $computer | Sort WS -Descending | Select ProcessName, Id, WS -First $ProccessNumToFetch | ConvertTo-Html -Fragment
Get all processes that should be started, but aren't:

#region Services Report
	$ServicesReport = @()
	$Services = Get-WmiObject -Class Win32_Service -ComputerName $computer | Where {($_.StartMode -eq "Auto") -and ($_.State -eq "Stopped")}

	foreach ($Service in $Services) {
		$row = New-Object -Type PSObject -Property @{
	   		Name = $Service.Name
			Status = $Service.State
			StartMode = $Service.StartMode
		}
		
	$ServicesReport += $row
	
	}
	
	$ServicesReport = $ServicesReport | ConvertTo-Html -Fragment
	#endregion
 

Get the Event Logs for System and Application:


#region Event Logs Report
	$SystemEventsReport = @()
    $Uniek = @()
	$SystemEvents = Get-EventLog -ComputerName $computer -LogName System -EntryType Error,Warning -After (Get-Date).AddDays(-$EventNum)
	foreach ($event in $SystemEvents) {
	    $row = New-Object -Type PSObject -Property @{
		    TimeGenerated = $event.TimeGenerated
		    EntryType = $event.EntryType
		    Source = $event.Source
		    Message = $event.Message
		    EventID = $event.InstanceID
	    }
        if ($Uniek -notcontains $event.InstanceID)
        { 
	        $SystemEventsReport += $row
            $Uniek += $event.InstanceID
        }
	}
			
	$SystemEventsReport = $SystemEventsReport | ConvertTo-Html -Fragment
	
	$ApplicationEventsReport = @()
    $Uniek = @()
	$ApplicationEvents = Get-EventLog -ComputerName $computer -LogName Application -EntryType Error,Warning -After (Get-Date).AddDays(-$EventNum)
	foreach ($event in $ApplicationEvents) {
        if ($event.Source -notcontains "Perflib")
        {
		    $row = New-Object -Type PSObject -Property @{
			    TimeGenerated = $event.TimeGenerated
			    EntryType = $event.EntryType
			    Source = $event.Source
			    Message = $event.Message
			    EventID = $event.InstanceID
		    }
            if ($Uniek -notcontains $event.InstanceID)
            { 
		        $ApplicationEventsReport += $row
                $Uniek += $event.InstanceID
            }
        }
	}
	
	$ApplicationEventsReport = $ApplicationEventsReport | ConvertTo-Html -Fragment
	#endregion
 

Create the chart for the memory and get the up time of the server:


	# Create the chart using our Chart Function
	Create-TableChart -Report $computer
	$ListOfAttachments += $location + $computer +"-chart.png"

	#region Uptime
	# Fetch the Uptime of the current system using our Get-HostUptime Function.
	$SystemUptime = Get-HostUptime -ComputerName $computer
	#endregion

Now lets add it as HTML:


# Create HTML Report for the current System being looped through
	$CurrentSystemHTML = @"
	<hr noshade size="3" width="100%">
	<div id="report">
	<p><h2>$computer Report</p></h2>
	<h3>System Info</h3>
	<table class="list">
	<tr>
	<td>System Uptime</td>
	<td>$SystemUptime</td>
	</tr>
	<tr>
	<td>OS</td>
	<td>$OS</td>
	</tr>
	<tr>
	<td>Total RAM (GB)</td>
	<td>$TotalRAM</td>
	</tr>
	<tr>
	<td>Free RAM (GB)</td>
	<td>$FreeRAM</td>
	</tr>
	<tr>
	<td>Percent free RAM</td>
	<td>$RAMPercentFree</td>
	</tr>
	</table>
	
	<IMG SRC="$computer-chart.png" ALT="$computer Chart">
		
	<h3>Disk Info</h3>
	<table class="normal">$DiskChart</table>
	<br></br>
	
	<div class="first column">
	<h3>System Processes - Top $ProccessNumToFetch Highest Memory Usage</h3>
	<p>The following $ProccessNumToFetch processes are those consuming the highest amount of Working Set (WS) Memory (bytes) on $computer</p>
	<table class="normal">$TopProcesses</table>
	</div>
	<div class="second column">
	
	<h3>System Services - Automatic Startup but not Running</h3>
	<p>The following services are those which are set to Automatic startup type, yet are currently not running on $computer</p>
	<table class="normal">
	$ServicesReport
	</table>
	</div>
	
	<h3>Events Report - The System/Application Log Events of the last $EventNum days that were Warnings or Errors</h3>
	<p>The following is a list of <b>unique System log</b> events of the last $EventNum days that had an Event Type of either Warning or Error on $computer</p>
	<table class="normal">$SystemEventsReport</table>

	<p>The following is a list of <b>unique Application log</b> events of the last $EventNum that had an Event Type of either Warning or Error on $computer</p>
	<table class="normal">$ApplicationEventsReport</table>
"@
	# Add the current System HTML Report into the final HTML Report body
	$HTMLMiddle += $CurrentSystemHTML
	
}
	

Conclude the HTML with the information about the OWA server and the SharePoint Health analyzer:


# Add SharePoint Health Analyzer data
$HTMLMiddle +=	'<hr noshade size="3" width="100%">'

if ($OWAServer)
{  
    $HTMLMiddle +=	'<div id="report">'
    $HTMLMiddle +=	'<p><h2>Office Web Apps health:</p></h2>'
    $HTMLMiddle +=  $owahealth.HealthStatus
    $HTMLMiddle +=  '<br><br>'
    $HTMLMiddle +=	'<hr noshade size="3" width="100%">'
    $HTMLMiddle +=	'</div>'
}

$HTMLMiddle +=	'<div id="report">'
$HTMLMiddle +=	'<p><h2>Health Analyzer</p></h2>'
$HTMLMiddle +=  $body
$HTMLMiddle +=  "<br><br><p><i>Script runned on $env:computername</i></p>"

# Assemble the closing HTML for our report.
$HTMLEnd = @"
</div>
"@

Send the mail with all information and attachments:


# Send mail
$wa = [Microsoft.SharePoint.Administration.SPAdministrationWebApplication]::Local
$smtpServer = $wa.OutboundMailServiceInstance.Parent.Name

# Assemble the final report from all our HTML sections
$HTMLmessage = $HTMLHeader + $HTMLMiddle + $HTMLEnd
# Save the report out to a file in the current path
$HTMLmessage | Out-File ($location+&#x22;report.html&#x22;)
# Email our report out
$smtpClient = New-Object Net.Mail.SmtpClient($smtpServer)

$msg = new-object Net.Mail.MailMessage
$msg.From = $fromemail
$msg.To.Add($users)
$msg.Subject = $subject
$msg.Body = $HTMLmessage

foreach ($attachment in $ListOfAttachments)
{
    $att = new-object Net.Mail.Attachment((Resolve-Path $attachment))
    $msg.Attachments.Add($att)
}

$msg.IsBodyHTML = $True
$smtpClient.Send($msg)

$att.Dispose()
 

The e-mail should look like this (top part of):
Sample

That's it. Hope this helps all SharePoint admins out there with some nice reporting through PowerShell!

Submit a comment