Build a C# Web Site Monitor Service
with Email and MSN Messenger Notification
By Peter A. Bromberg, Ph.D.

Peter Bromberg

One of the most common needs of .NET developers and administrator - types is the ability to monitor a production web site and be appropriately notified when the site is down. A few years ago I looked around the marketplace and found only a couple of offerings in this regard, and they were all too "full featured" and expensive for my needs. I also looked at a couple of the online services and found they either were not reliable, didn't have longevity (e.g., if they will still be up and running two weeks from now), or else there was some "baggage" such as advertising that came along with it.So, I rolled my own in VB 6.0 using the NTSrv.ocx control.

It worked great, but I really didn't have the need for it at the time, so I just "retired the code" along with a lot of other stuff that was cluttering up my hard drives when .NET came along. Now I do have the need, and so I looked around again. Surprisingly, I didn't find too much. I did find one offering for around $200 but once again, it was way too "full featured". What I need is something simple that won't break, just to monitor a single machine or web site. I did find somebody's "freebie" offering written for .NET, but it was sloppily written and the design was awkward. What I really needed was a simple Windows Service that would be a breeze to install, would run happily in the background for long periods of time, and all you needed to do was set some basic configuration settings, start the service and you would be in business. Email notification to me if the site was unresponsive would be fine; and if I could put in notification to my MSN Messenger IM, which runs all day because I use it about 80% for business, that would be a big plus.


Well, I didn't find anything even remotely like what I needed so, here it is!

Basically what SiteMonitor offers is a lightweight, C# - based Windows Service that INSTALLS ITSELF (you only need to do "Sitemonitor.exe /install", no hassles with InstallUtil.exe) and doesn't take up more than about 15MB of memory space. To get it up and running, all you need to do is fill in the site address you want to have monitored, your email stuff, your Messenger Passport stuff, and a couple of switches about what types of notification you want to turn on, save the modified SiteMonitor.exe.config, run "SiteMonitor.exe /install", start the service, and you are done.

First, let's take a look at the basic "Service" part of the app:

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using System.Configuration.Install;
using System.Reflection;
using System.Windows.Forms;

namespace SiteMonitorService
{
 public class SiteMonitor : System.ServiceProcess.ServiceBase
 {
  private System.ComponentModel.Container components = null;
  private SiteMonitorService.Engine eng;

  public SiteMonitor()
  {    
   InitializeComponent();  
  }
  
  static void Main(string[] args)
  {    
             string opt=null;  
   if(args.Length >0 )
   { 
    opt=args[0];   
   }
   if(opt!=null && opt.ToLower()=="/install")
   {
    TransactedInstaller ti= new TransactedInstaller();
    MyInstaller mi = new MyInstaller();
    ti.Installers.Add(mi);
    String path=String.Format("/assemblypath={0}",
     System.Reflection.Assembly.GetExecutingAssembly().Location);
    String[] cmdline={path};
    InstallContext ctx = new InstallContext("",cmdline);    
    ti.Context =ctx;       
    ti.Install(new Hashtable());
   }
   else if (opt !=null && opt.ToLower()=="/uninstall")
   {
    TransactedInstaller ti=new TransactedInstaller();
    MyInstaller mi=new MyInstaller();
    ti.Installers.Add(mi);
    String path = String.Format("/assemblypath={0}",
     System.Reflection.Assembly.GetExecutingAssembly().Location);
    String[] cmdline={path};
    InstallContext ctx = new InstallContext("",cmdline);
    ti.Context=ctx;
    ti.Uninstall(null);
    
   }    

   if(opt==null)  // e.g. ,nothing on the command line
   {
    System.ServiceProcess.ServiceBase[] ServicesToRun;
    ServicesToRun = new System.ServiceProcess.ServiceBase[] { new SiteMonitor() };
    System.ServiceProcess.ServiceBase.Run(ServicesToRun);
   }
  }
  
  private void InitializeComponent()
  {
    this.eng = new Engine();   
   this.CanHandlePowerEvent = true;
   this.CanPauseAndContinue = true;
   this.CanShutdown = true;
   this.ServiceName = "SiteMonitor";
  }

  protected override void Dispose( bool disposing )
  {
   if( disposing )
   {
    if (components != null) 
    {
     components.Dispose();
    }
   }
   base.Dispose( disposing );
  }  
  protected override void OnStart(string[] args)  
  {
   // Debugger.Launch(); //use this to help debug - attach to a running service 
           eng.BeginMonitor();   
   EventLog.WriteEntry("SiteMonitorService starting. " );
  } 
  protected override void OnStop()
  {   
  }
 }
}


The above is pretty much the standard guts of a C# Windows Service. The difference here is that we parse the Command Line arguments and look for /Install or /uninstall, and call a separate TransactedInstaller to actually do 100% of the work you would do -- that is, after you finally found InstallUtil.exe, read all the switch info, made sure your environment variables were all set, and finally were ready to do it "by hand". That code is in the ProjectInstaller.cs class file:

using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;
namespace SiteMonitorService
{ 
 [RunInstaller(true)]
 public class MyInstaller : Installer
 { 
  public MyInstaller()
  {
   ServiceProcessInstaller spi=new ServiceProcessInstaller();
   spi.Account = System.ServiceProcess.ServiceAccount.LocalSystem;
   spi.Password = null;
   spi.Username = null;
   ServiceInstaller si = new ServiceInstaller();
   si.ServiceName="SiteMonitor";
   this.Installers.Add(spi);
   this.Installers.Add(si);    
  }  
 }
}

Really, the above is about all you need to make any .NET Windows Service "self installing". Due credit for this neat trick goes to Craig Andera, who has written a number of similar .NET utilities and has been generous enough to post many of them online. Notice that I prefer not to have my business stuff inside the service class - the OnStart method actually instantiates my "Engine" class and calls the BeginMonitor method on it, which kicks off the timer and so on.

Now comes the "Engine" part - which is a bit more complex, but still not rocket science for the average developer:

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Configuration;
using Quiksoft.FreeSMTP;
using MSNSender;

namespace SiteMonitorService
{ 
 public class Engine
 {
  System.Timers.Timer timerSM=new System.Timers.Timer();
  public Engine()
  {   

  }

  public void BeginMonitor()
  {
   double dblInterval = Convert.ToDouble(ConfigurationSettings.AppSettings["Interval"]);
   this.timerSM.Interval = dblInterval;
   this.timerSM.Elapsed += 
    new System.Timers.ElapsedEventHandler(timerSM_Elapsed);
   this.timerSM.Enabled = true;
  }

  public void timerSM_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  {
   CheckWebPage();
  }

  public void CheckWebPage()
  {
   string strContent=String.Empty;
   StreamReader objSR;
   WebResponse objResponse = null;
   WebRequest objRequest = 
HttpWebRequest.Create(ConfigurationSettings.AppSettings["siteUrl"]); string strSource = ConfigurationSettings.AppSettings["siteUrl"]; try { objResponse = objRequest.GetResponse(); int x = 0; objSR = new StreamReader(objResponse.GetResponseStream(),
System.Text.Encoding.ASCII); strContent = objSR.ReadToEnd(); x=strContent.Length ; strSource+= " Content-Length: " +x.ToString(); objSR.Close(); objResponse.Close(); } catch(Exception ex) { bool IsErrorCondition = true; Debug.WriteLine(ex.Message+ex.StackTrace); if ((ConfigurationSettings.AppSettings["LogToEventLog"].ToUpper() == "YES" ||
ConfigurationSettings.AppSettings["LogToEventLog"].ToUpper() =="TRUE")
&& IsErrorCondition) { writeToEventLog(strSource +"Info:" +ex.Message); } if ( (ConfigurationSettings.AppSettings["LogToEmail"].ToUpper() == "YES" ||
ConfigurationSettings.AppSettings["LogToEmail"].ToUpper() == "TRUE")
&& IsErrorCondition) { SendEmail(strSource + "Info: " + ex.Message); } if ( (ConfigurationSettings.AppSettings["useMessengerNotify"].ToUpper() == "YES" ||
ConfigurationSettings.AppSettings["useMessengerNotify"].ToUpper() == "TRUE")
&& IsErrorCondition) { MessengerNotify(strSource + "Info: " + ex.Message); } if ((ConfigurationSettings.AppSettings["LogToDb"].ToUpper() == "YES" ||
ConfigurationSettings.AppSettings["LogToDb"].ToUpper() == "TRUE")
&& IsErrorCondition ) { InsertDB(strSource +" Info: " + ex.Message ); } } } void writeToEventLog(string strResult) { try { if (!EventLog.SourceExists("Application")) { EventLog.CreateEventSource("Application", "Application", "."); } EventLog objLog = new EventLog(); string strLogName = "Application"; string message = strLogName; objLog.Source = "WebSite Monitor"; objLog.WriteEntry(strResult, EventLogEntryType.Error); objLog.Close(); objLog.Dispose(); } catch (Exception ex) { ErrorHandler(ex.Message.ToString()); } } void SendEmail(string strResult) { try { string strMailFrom; string strMailTo; string strMailSubject; string strSmtpServer; string strUserName; string strUserPassword; strResult = "Server Test Details: "+ strResult; strMailFrom = ConfigurationSettings.AppSettings["mailFrom"]; strMailTo = ConfigurationSettings.AppSettings["mailTo"]; strMailSubject = ConfigurationSettings.AppSettings["mailSubject"]; strSmtpServer = ConfigurationSettings.AppSettings["mailSMTPHost"]; strUserName=ConfigurationSettings.AppSettings["mailUserName"]; strUserPassword=ConfigurationSettings.AppSettings["mailUserPassword"]; System.Web.Mail.MailMessage objMsg = new System.Web.Mail.MailMessage(); objMsg.From = strMailFrom; objMsg.To = strMailTo; objMsg.Subject = strMailSubject; Quiksoft.FreeSMTP.SMTP.QuickSend(strSmtpServer,strMailTo,
strMailFrom,strMailSubject,strResult,BodyPartFormat.Plain); } catch (Exception ex) { Debug.WriteLine(ex.Message+ex.StackTrace+ex.InnerException.StackTrace); } } void MessengerNotify(string msg) { MessageSender ms = new MessageSender(); string userMail=ConfigurationSettings.AppSettings["SenderPassport"]; string userPass=ConfigurationSettings.AppSettings["SenderPassword"]; string recipient=ConfigurationSettings.AppSettings["ReceiverPassport"]; ms.SendIMMessage(userMail,userPass,recipient,msg); } void InsertDB(string strInfo) { try { SqlConnection objConn; SqlCommand objCMD; objConn = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]); objCMD = new SqlCommand("InsertResults", objConn); objCMD.CommandType = CommandType.StoredProcedure; objCMD.Parameters.Add(new SqlParameter("@Loginfo", SqlDbType.VarChar, 500)); objCMD.Parameters["@LogInfo"].Value = strInfo; objConn.Open(); objCMD.ExecuteNonQuery(); objConn.Close(); } catch (Exception ex) { ErrorHandler(ex.Message.ToString()+ex.StackTrace.ToString()); } } void ErrorHandler(string strMessage) { try { if (!EventLog.SourceExists(strMessage)) { EventLog.CreateEventSource("Application", "Application", "."); } EventLog objLog = new EventLog(); objLog.Source = "WebSite Monitor"; strMessage = "Time " + System.DateTime.Now + " Error Message: " + strMessage; objLog.WriteEntry(strMessage, EventLogEntryType.Error); objLog.Close(); objLog.Dispose(); } catch(Exception BTH) // "BAD THING HAPPENED" { throw new ApplicationException("Unable to Write to Event Log",BTH); } } } }

To walk through the above, here is what happens "in a nutshell":
1) BeginMonitor kicks off a timer with an elapsed time from what you set in milliseconds in your config. file
2) When the timer's elapsed event fires, it calls CheckWebPage, which simply makes a WebRequest for your site, and stores the Content length of whatever the default or specified page is from the URL in your config file.
3) If, and only if - it bombs out, this is one of those rare instances where the case can be made for putting business logic inside an exception handler, and --
4) It checks to see which of your config file items are "turned on" - Event Log , Email , Messenger, and Database.
5) it performs the required actions and waits happily until the next Elapsed event of the timer.

As long as your monitored site is up, SiteMonitor just keeps doing its thing every X minutes that you have configured, but nothing happens, because everything is "OK". The rest of the code is pretty general. I use the Free Quicksoft mail component because its free, and also because I don't much like System.Web.Mail and all of its idiosyncracies. The Messenger notification is done with DotMSN, a "not yet" open source library that unfortunately is not well supported, but after cobbling together a wrapper class "MSNSender", I was able to get it to reliably perform my basic need, which is to sign in to MSN Messenger with a separate Passport than my regular one, and have it send me a copy of the notification in real time via MSN Messenger.I haven't implemented the Database logging because I really have no need for it, but I've left it in, just in case somebody wants it. All you need to do is create a table and write a stored proc for the insert, and you are done.

Use free Teleflip.com service to send SMS message to your CellPhone!

By simply replacing the email notification address with yourCellphoneNumber@teleflip.com, you can have SiteMonitor send the notification direct to your US or Canadian cellphone number without the complexities of SMS API's and providers. The service, which was started by a man who was frustrated with the complexities of sending SMS (Short Message Service) text messages, is 100% free. You can visit teleflip.com for details.

Now of course the question comes up, "Can I run this on the same machine that serves the website(s) I am checking? Sure, you could do that. Just remember, that if something happens that makes the site go down, it may include other BTH's (Bad Thing Happened) that prevent SiteMonitor from telling you about it! Best to run this on another machine. Mine comes up in the morning when I start the box in my office. You could spend a lot of time enhancing this, but frankly, I believe in simplicity, and the way it is right now it suits my needs perfectly.

N.B. - It seems this article has been more popular than I thought, so I've added one more thing - if you are running this on the same machine that it is set to check, you can optionally tell it to run IISReset after X number of "failures". This setting is configured in the app.config and is well-commented. Just set the number of failures to "0" for "never". The code to handle this has been added to the sources in the downloadable zip file.

The download containing the Solution also includes a handy Console App that will allow you to test the functionality without having to install the whole service - another good OOP reason why your business stuff should be kept outside of the actual Service class. I hope the code is useful to you!

N.B. (9/21/2005) Since I first published this article, I received notes from a number of developers. I added a feature that myself that uses a filesystemwatcher to watch a specified folder for a "reboot.txt" file to be FTP'ed in, and will restart the computer. In particular, Clynton Caines rewrote this to support multiple websites and "fleshed" out the database logging portion. Clynton has graciously made his creation available to all, and I've provided a second link to his code below.

Download the Original Solution that accompanies this article

Download Clynton Caines' s revised code here

 


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: