Articles in this series:

In my last “Custom Sensors Part 1” article, I covered what sensors are, the wire-format for custom sensors, and the main logic in our sensor we use for monitoring our BIG-IP GTM, LTM, ASM, and WA devices.  In this article, I will continue on with the examples and walk you through what we are using for testing the website itself.

The Application

One may ask why the simple HTTP monitor is not sufficient to test our web application.  The reason is that DevCentral consists of the following three applications stitched together with application logic and some iRule goodness:

  • Main application CMS
  • Wiki
  • Blogs

I could have created several monitors for the various applications, but I wanted to come up with something that determined the overall “health” of the system.  For our requirements, we wanted to have a single monitor aggregate the health of the various applications into a “global” health and a custom monitor turned out to be perfect for the job.

So without further ado, we’ll dig into the various logic and functions in my script.

Usage

The script only takes 2 parameters.  The first is the address that we are testing against.  I have several instances of this script running at the various layers in our network.  I have it pointing directly at the app servers using an HTTP connection, and through our WA and ASM with HTTPS connections.  This gives me a nice view of timings at various layers in our network for troubleshooting.

   1: PS> .\DC-HealthCheck.aspx -Server <server_ip> -SSL
   2: -Server <server_ip> - the IP address that we want to test our app against.
   3: -SSL                - a switch to tell the script whether to test HTTP or HTTPS.

Utilities

Since the main component of my custom sensor would be validating contents on web page requests, I created a function that would wrap the curl.exe command line.  I could have done this with native .NET methods but I took the easy way out since I had a controlled environment with curl present.  I made use of curl’s built in resolver to allow the script to make requests to https://devcentral.f5.com but allowing it to control which IP addresses it made requests to.

The Get-WebPage() function does the curl request, times the request, and builds a response containing the page content and the time it took to request the page.  The time is used later on to give an “average” response time for the site.

The Build-Result() function, generates a PRTG Sensor item for the response to be included in the aggregate sensor output in the various component functions below.

   1: function New-Response()
   2: {
   3:   1 | select "Content", "Time";
   4: }
   5:  
   6: function Get-Webpage()
   7: {
   8:   param(
   9:     $Server,
  10:     $IP = $null,
  11:     $Url
  12:   );
  13:   
  14:   $Port = 80;
  15:   if ( $script:SSL )
  16:   {
  17:     $fullUrl = "https://${Server}${Url}";
  18:     $Port = 80;
  19:   }
  20:   else
  21:   {
  22:     $fullUrl = "http://${Server}${Url}";
  23:   }
  24:   $Output = "";
  25:   
  26:   if ( $IP -ne $null )
  27:   {
  28:     $sec = (Measure-Command {$Output = curl --insecure -sN --resolve "devcentral.f5.com:${Port}:${IP}" $fullUrl }).TotalSeconds;
  29:   }
  30:   else
  31:   {
  32:     $sec = (Measure-Command {$Output = curl --insecure -sNA $UserAgent --max-time 10 $fullUrl }).TotalSeconds;
  33:   }
  34:  
  35:   $R = New-Response;
  36:   $R.Time = $sec;
  37:   $R.Content = $Output;
  38:   
  39:   $R;
  40: }
  41:  
  42: function Build-Result()
  43: {
  44:   param(
  45:     $Channel,
  46:     $Unit = "Custom",
  47:     $CustomUnit = "msec",
  48:     $Mode = "Absolute",
  49:     $Value,
  50:     [switch]$Warning = $False
  51:   );
  52:  
  53:   $Value = [Convert]::ToInt32($Value * 1000);
  54:   
  55:   $w = 0;
  56:   if ( $Warning )
  57:   {
  58:     $script:WARNING = $True;
  59:     $script:STATUS = "ERROR: At least one channel had an error";
  60:     $w = 1;
  61:   }
  62:   $s = @"
  63: <result>
  64:   <channel>${Channel}</channel>
  65:   <unit>${Unit}</unit>
  66:   <CustomUnit>${CustomUnit}</CustomUnit>
  67:   <mode>${Mode}</mode>
  68:   <showChart>1</showChart>
  69:   <showTable>1</showTable>
  70:   <warning>$([Convert]::ToInt32($w))</warning>
  71:   <float>0</float>
  72:   <value>${Value}</value>
  73: </result>
  74: "@;
  75:   $s;
  76: }

Testing the Database

The first layer on our stack is the database.  We have a database healthcheck page that performs some connection metrics on the database itself without the overhead of the application code in our main applications.  This function will call the Get-WebPage function with the db healthcheck URL and report it’s findings to the caller.

   1: function Test-DB()
   2: {
   3:   param($IP);
   4:   
   5:   $R = Get-WebPage -Server "devcentral.f5.com" -IP "${IP}" -Url "/dbhealth.aspx";
   6:   if ( $R.Content -match "Database Status: UP" )
   7:   {
   8:     $s = Build-Result -Channel "Database Connection" -Unit "Custom" -Value $R.Time;
   9:     $script:TIMES += $R.Time;
  10:   }
  11:   else
  12:   {
  13:     $s = Build-Result -Channel "Database Connection" -Unit "Custom" -Value 0 -Warning;
  14:     $script:TIMES += 0;
  15:     $script:BADCHECKS += "DB";
  16:   }
  17:   
  18:   $s;
  19: }

Testing the Homepage

Now that the database has been checked, we’ll test out the main landing page for devcentral.  The function is very similar to the database check except that it’s testing a separate URL in the request.  The function also updates the $script:BADCHECKS variable with the check name if that check failed.  This is reported to PRTG so that in the GUI you can tell which individual “channels” failed.

   1: function Test-HomePage()
   2: {
   3:   param($IP);
   4:   
   5:   $R = Get-WebPage -Server "devcentral.f5.com" -IP "${IP}" -Url "/";
   6:   if ( $R.Content -match "Welcome to F5 DevCentral" )
   7:   {
   8:     $s = Build-Result -Channel "Home Page" -Unit "Custom" -Value $R.Time;
   9:     $script:TIMES += $R.Time;
  10:   }
  11:   else
  12:   {
  13:     $s = Build-Result -Channel "Home Page" -Unit "Custom" -Value 0 -Warning;
  14:     $script:TIMES += 0;
  15:     $script:BADCHECKS += "HomePage";
  16:   }
  17:   
  18:   $s;
  19: }

Testing the Wiki

Our wiki is the second application tested.  Again, this is similar to the previous ones except that it’s pulling down the wiki home page.

   1: function Test-Wiki()
   2: {
   3:   param($IP);
   4:  
   5:   $R = Get-WebPage -Server "devcentral.f5.com" -IP "${IP}" -Url "/wiki/";
   6:   if ( $R.Content -match "Welcome to the F5 DevCentral Wiki Home!" )
   7:   {
   8:     $s = Build-Result -Channel "Wiki" -Unit "Custom" -Value $R.Time;
   9:     $script:TIMES += $R.Time;
  10:   }
  11:   else
  12:   {
  13:     $s = Build-Result -Channel "Wiki" -Unit "Custom" -Value 0 -Warning;
  14:     $script:TIMES += 0;
  15:     $script:BADCHECKS += "Wiki";
  16:   }
  17:   
  18:   $s;
  19: }

Testing the Blogs

The last application is the blogs.  To bump up my metrics, you’ll notice that I pointed it at my main blog homepage!

   1: function Test-Blogs()
   2: {
   3:   param($IP);
   4:   
   5:   $R = Get-WebPage -Server "devcentral.f5.com" -IP "${IP}" -Url "/weblogs/joe/default.aspx";
   6:   if ( $R.Content -match "A Software Architect's take on Network Security" )
   7:   {
   8:     $s = Build-Result -Channel "Blogs" -Unit "Custom" -Value $R.Time;
   9:     $script:TIMES += $R.Time;
  10:   }
  11:   else
  12:   {
  13:     $s = Build-Result -Channel "Blogs" -Unit "Custom" -Value 0 -Warning;
  14:     $script:TIMES += 0;
  15:     $script:BADCHECKS += "Blogs";
  16:   }
  17:   
  18:   $s;
  19: }

Main Sensor Logic

The main logic in the sensor calls the various “Test-*” functions for the database, homepage, wiki, and blogs.  It then averages out the response times and builds a top-level channel for the “Avg App Response” time.  This allows a quick glance on how the app is performing as a whole.

The response is then built with the response time channel and the channels for the various tests and the value is returned back to PRTG.  For situations where one of the checks failed, a PRTG error is generated with the list of the failed checks in the error text.

   1: param(
   2:   $Server = $(Throw "Server parameter is required"),
   3:   [switch]$SSL = $False
   4: );
   5:  
   6: $script:SSL = $SSL;
   7: $script:WARNING = $false;
   8: $script:STATUS = "OK";
   9: $script:TIMES = @();
  10: $script:BADCHECKS = @();
  11:  
  12: $Tests = @"
  13: $(Test-DB -IP $Server)
  14: $(Test-Homepage -IP $Server)
  15: $(Test-Wiki -IP $Server)
  16: $(Test-Blogs -IP $Server)
  17: "@;
  18:  
  19: $t = $script:TIMES | Measure-Object -Average;
  20:  
  21: $s = @"
  22: <prtg>
  23: $(Build-Result -Channel "Avg App Response" -Unit "Custom" -Value $($t.Average))
  24: $Tests
  25: <Text>$($script:STATUS)</Text>
  26: </prtg>
  27: "@;
  28:  
  29: if ( $script:BADCHECKS.Length -gt 0 )
  30: {
  31:   $s = @"
  32: <prtg>
  33:   <error>1</error>
  34:   <text>An error occured in the following checks: $([string]::join(',', $script:BADCHECKS))</text>
  35: </prtg>
  36: "@;
  37: }
  38:  
  39: $s;
  40:  
  41: $rc = [Convert]::ToInt32($script:WARNING);;
  42:  
  43: exit $rc;

Running/Testing the Script Manually

To test the script, put it in the %Program Files%\PRTG Network Monitor\Custom Sensors\EXEXML directory and run it from the command line.  The results are below:

   1: PS C:\Program Files (x86)\PRTG Network Monitor\Custom Sensors\EXEXML> .\DC-HealthCheck.ps1 -Server ww.xx.yy.zz
   2: <prtg>
   3: <result>
   4:   <channel>Avg App Response</channel>
   5:   <unit>Custom</unit>
   6:   <CustomUnit>msec</CustomUnit>
   7:   <mode>Absolute</mode>
   8:   <showChart>1</showChart>
   9:   <showTable>1</showTable>
  10:   <warning>0</warning>
  11:   <float>0</float>
  12:   <value>1212</value>
  13: </result>
  14: <result>
  15:   <channel>Database Connection</channel>
  16:   <unit>Custom</unit>
  17:   <CustomUnit>msec</CustomUnit>
  18:   <mode>Absolute</mode>
  19:   <showChart>1</showChart>
  20:   <showTable>1</showTable>
  21:   <warning>0</warning>
  22:   <float>0</float>
  23:   <value>416</value>
  24: </result>
  25: <result>
  26:   <channel>Home Page</channel>
  27:   <unit>Custom</unit>
  28:   <CustomUnit>msec</CustomUnit>
  29:   <mode>Absolute</mode>
  30:   <showChart>1</showChart>
  31:   <showTable>1</showTable>
  32:   <warning>0</warning>
  33:   <float>0</float>
  34:   <value>1249</value>
  35: </result>
  36: <result>
  37:   <channel>Wiki</channel>
  38:   <unit>Custom</unit>
  39:   <CustomUnit>msec</CustomUnit>
  40:   <mode>Absolute</mode>
  41:   <showChart>1</showChart>
  42:   <showTable>1</showTable>
  43:   <warning>0</warning>
  44:   <float>0</float>
  45:   <value>2688</value>
  46: </result>
  47: <result>
  48:   <channel>Blogs</channel>
  49:   <unit>Custom</unit>
  50:   <CustomUnit>msec</CustomUnit>
  51:   <mode>Absolute</mode>
  52:   <showChart>1</showChart>
  53:   <showTable>1</showTable>
  54:   <warning>0</warning>
  55:   <float>0</float>
  56:   <value>494</value>
  57: </result>
  58: <Text>OK</Text>
  59: </prtg>

 

The Graphs in the GUI

When drilling into the PRTG GUI for the custom sensor I created, you’ll see that the output data is imported in and the channels created with the various times are added to the report timeline.

prtg-customsensor-1

 

Conclusion

With the support for PowerShell, it’s very easy to build a custom sensor.  If you have needs beyond the basic supported sensors, go ahead and jump in and write your own - it’s easy!