Skip to content

Instantly share code, notes, and snippets.

@hermanussen
Created October 3, 2015 14:37
Show Gist options
  • Save hermanussen/9d4ea1b77602e02609cc to your computer and use it in GitHub Desktop.
Save hermanussen/9d4ea1b77602e02609cc to your computer and use it in GitHub Desktop.
This is a simple page that can be dropped into a Sitecore CM or CD server so you can easily monitor its health.
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="Sitecore.ContentSearch" %>
<%@ Import Namespace="Sitecore.Data" %>
<%@ Import Namespace="Sitecore.Data.Items" %>
<script runat="server">
#region Configure actual checks here
ChecksSet ChecksRun { get; set; }
string[] Tags { get; set; }
bool Authorized { get; set; }
protected void Page_Load(object sender, EventArgs e)
{
// Optional: uncomment whichever ways you feel are appropriate for security
// Ensure that your monitoring tool matches these
if (!(true
//&& Request.IsSecureConnection // Checks if using HTTPS
//&& Request.QueryString["authkey"] == @"secret"
&& Request.UserHostAddress == "127.0.0.1"
//&& Sitecore.Context.IsLoggedIn
//&& Sitecore.Context.IsAdministrator
//&& Sitecore.Context.User.IsInRole(@"sitecore\Developer")
))
{
Response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
Response.Write("You are not authorized to view this page");
Response.TrySkipIisCustomErrors = true;
Response.End();
return;
}
ChecksRun = new ChecksSet();
Tags = (Request.QueryString["tags"] ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
if (Tags.Contains("fail", StringComparer.InvariantCultureIgnoreCase))
{
ChecksRun.Checks.Add(new AlwaysFailCheck());
}
// Add your checks here
ChecksRun.Checks.Add(new ItemExistsCheck("Folder template", "{A87A00B1-E6DB-45AB-8B54-636FEC3B5523}", new[] { "master", "web" }, null, false));
ChecksRun.Checks.Add(new ItemExistsCheck("Launchpad has layout", "{6B846FBD-8549-4C91-AE6B-18286EFE82D2}", new[] { "core" }, new string[] { "en" }, true));
ChecksRun.Checks.Add(new SearchIndexSizeCheck("Master index", "sitecore_master_index", 1000, null));
ChecksRun.Checks.Add(new UrlCheck("Google is reachable", "http://www.google.com/"));
ChecksRun.Checks.Add(new ConfigCheck("Check if master database is available", "/sitecore/databases/database[@id='master']", 1));
ChecksRun.Process();
if (!ChecksRun.IsSuccess)
{
Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
Response.TrySkipIisCustomErrors = true;
}
this.DataBind();
}
#endregion
#region Core classes
interface ISitecoreCheck
{
string Name { get; }
bool IsProcessed { get; }
bool IsSuccess { get; }
bool AbortOnFail { get; }
string Message { get; }
void Process();
long Duration { get; set; }
}
abstract class SitecoreCheckBase : ISitecoreCheck
{
public string Name { get; private set; }
public bool IsProcessed { get; private set; }
public bool IsSuccess { get; private set; }
public virtual bool AbortOnFail
{
get { return false; }
}
public string Message { get; protected set; }
public long Duration { get; set; }
protected SitecoreCheckBase(string name)
{
Name = name;
}
public void Process()
{
IsProcessed = true;
IsSuccess = DoProcess();
}
protected abstract bool DoProcess();
}
class ChecksSet
{
public List<ISitecoreCheck> Checks { get; private set; }
public bool IsSuccess
{
get
{
return Checks.All(c => c.IsProcessed && c.IsSuccess);
}
}
public ChecksSet()
{
Checks = new List<ISitecoreCheck>();
}
public void Process()
{
Stopwatch timer = new Stopwatch();
foreach (var check in Checks)
{
timer.Reset();
timer.Start();
check.Process();
timer.Stop();
check.Duration = timer.ElapsedMilliseconds;
if (!check.IsSuccess && check.AbortOnFail)
{
break;
}
}
}
public string Message
{
get
{
return string.Format("{0}: {1} out of {2} checks processed with {3} failures",
IsSuccess ? "SUCCESS" : "FAILURE",
Checks.Count(c => c.IsProcessed),
Checks.Count,
Checks.Count(c => c.IsProcessed && ! c.IsSuccess));
}
}
}
#endregion
#region actual checks
/// <summary>
/// For debugging purposes only; if added, the check will always fail.
/// </summary>
class AlwaysFailCheck : SitecoreCheckBase
{
public AlwaysFailCheck() : base("Always fail")
{
}
protected override bool DoProcess()
{
Message = "This check always fails (on purpose). Use for debugging purposes only";
return false;
}
public override bool AbortOnFail { get { return true; } }
}
/// <summary>
/// Checks if a certain item exists in Sitecore.
/// </summary>
class ItemExistsCheck : SitecoreCheckBase
{
private static readonly string[] defaultDatabases = new[] { "master", "web" };
private static readonly string[] defaultLanguages = new[] { "en" };
private string PathOrId { get; set; }
private string[] Databases { get; set; }
private string[] Languages { get; set; }
private bool CheckPresentation { get; set; }
/// <summary>
/// Give a path or id and optionally other parameters (by default: master and web dbs will be checked, in language 'en' and no presentation settings will be checked).
/// </summary>
/// <param name="name"></param>
/// <param name="pathOrId"></param>
/// <param name="databases"></param>
/// <param name="languages"></param>
/// <param name="checkPresentation"></param>
public ItemExistsCheck(string name, string pathOrId, string[] databases = null, string[] languages = null, bool checkPresentation = false)
: base(name)
{
PathOrId = pathOrId;
Databases = databases ?? defaultDatabases;
Languages = languages ?? defaultLanguages;
CheckPresentation = checkPresentation;
}
protected override bool DoProcess()
{
ID id = Sitecore.Data.ID.Null;
if (Sitecore.Data.ID.IsID(PathOrId))
{
id = Sitecore.Data.ID.Parse(PathOrId);
}
string otherIdentifier = "no other identifier found";
List<string> messages = new List<string>();
foreach (string dbStr in Databases)
{
Database db = Database.GetDatabase(dbStr);
if(db == null)
{
messages.Add(string.Format("Unable to find db '{0}'", dbStr));
continue;
}
Item item = id != Sitecore.Data.ID.Null ? db.GetItem(id) : db.GetItem(PathOrId);
if (item == null)
{
messages.Add(string.Format("Unable to find item '{0}' in db '{1}'", PathOrId, dbStr));
continue;
}
otherIdentifier = id != Sitecore.Data.ID.Null ? item.Paths.FullPath : item.ID.ToString();
foreach (var langStr in Languages)
{
using (new Sitecore.Globalization.LanguageSwitcher(langStr))
{
item = id != Sitecore.Data.ID.Null ? db.GetItem(id) : db.GetItem(PathOrId);
if (item.Versions.Count <= 0)
{
messages.Add(string.Format("Item '{0}' ({1}) in db '{2}' does not have a version in language '{3}'",
PathOrId,
otherIdentifier,
dbStr,
langStr));
}
if (CheckPresentation)
{
if (string.IsNullOrWhiteSpace(item[Sitecore.FieldIDs.FinalLayoutField])
&& string.IsNullOrWhiteSpace(item[Sitecore.FieldIDs.LayoutField]))
{
messages.Add(string.Format("Item '{0}' ({1}) in db '{2}' does not have presentation/layout set in language '{3}'",
PathOrId,
otherIdentifier,
dbStr,
langStr));
}
}
}
}
}
if (messages.Any())
{
Message = string.Concat("Check failed:<br />", string.Join("<br />", messages));
return false;
}
else
{
Message = string.Format("Item <i>'{0}'</i> ({1}) exists in db's <i>{2}</i>, in languages <i>{3}</i>{4}",
PathOrId,
otherIdentifier,
string.Join("+", Databases),
string.Join("+", Languages),
CheckPresentation ? " and has presentation/layout" : string.Empty);
return true;
}
}
}
/// <summary>
/// Checks to see if the number of items in a search index falls within a specified range.
/// </summary>
class SearchIndexSizeCheck : SitecoreCheckBase
{
public string IndexName { get; private set; }
public int? RangeStart { get; private set; }
public int? RangeEnd { get; private set; }
/// <summary>
/// Create a new check.
/// </summary>
/// <param name="name"></param>
/// <param name="indexName"></param>
/// <param name="rangeStart"></param>
/// <param name="rangeEnd"></param>
public SearchIndexSizeCheck(string name, string indexName, int? rangeStart, int? rangeEnd) : base(name)
{
IndexName = indexName;
RangeStart = rangeStart;
RangeEnd = rangeEnd;
}
protected override bool DoProcess()
{
if (!RangeStart.HasValue && !RangeEnd.HasValue)
{
Message = "No range was specified";
return false;
}
try
{
using (var context = ContentSearchManager.GetIndex(IndexName).CreateSearchContext())
{
var searchResults = context.GetQueryable<Sitecore.ContentSearch.SearchTypes.SearchResultItem>();
int total = searchResults.Count();
if ((!RangeStart.HasValue || RangeStart.Value <= total) && (!RangeEnd.HasValue || RangeEnd.Value >= total))
{
Message = string.Format("Search index <i>{0}</i> has {1} items, which falls within the specified range", IndexName, total);
return true;
}
else
{
Message = string.Format("Search index <i>{0}</i> has {1} items, which does not fall within the specified range", IndexName, total);
return false;
}
}
}
catch (Exception e)
{
Message = string.Format("An exception occurred when trying to search index {0} (more details in the log): {1}", IndexName, e.Message);
Sitecore.Diagnostics.Log.Error(Message, e, this.GetType());
return false;
}
}
}
/// <summary>
/// Check if a request to a certain page (perhaps an external system, like a REST api) is available and does not produce an error).
/// </summary>
class UrlCheck : SitecoreCheckBase
{
public string Url { get; private set; }
/// <summary>
/// Create a url check.
/// </summary>
/// <param name="name"></param>
/// <param name="url"></param>
public UrlCheck(string name, string url) : base(name)
{
Url = url;
}
protected override bool DoProcess()
{
try
{
using (var client = new WebClient())
{
string result = client.DownloadString(Url);
Message = string.Format("Succeeded in requesting <i>{0}</i>", Url);
return true;
}
}
catch (WebException w)
{
Message = string.Format("The request to <i>{0}</i> produced an error (more details in the log): {1}", Url, w.Message);
Sitecore.Diagnostics.Log.Error(Message, w, this.GetType());
return false;
}
catch (Exception e)
{
Message = string.Format("An exception occurred when trying to request <i>{0}</i> (more details in the log): {1}", Url, e.Message);
Sitecore.Diagnostics.Log.Error(Message, e, this.GetType());
return false;
}
}
}
/// <summary>
/// Check if a certain configuration setting is correct.
/// </summary>
class ConfigCheck : SitecoreCheckBase
{
public string XPathQuery { get; private set; }
public int Count { get; private set; }
/// <summary>
/// Create a new config check.
/// </summary>
/// <param name="name"></param>
/// <param name="xPathQuery">XPath query to be performed on SC configuration</param>
/// <param name="count">The amount of nodes that should be found</param>
public ConfigCheck(string name, string xPathQuery, int count) : base(name)
{
this.XPathQuery = xPathQuery;
this.Count = count;
}
protected override bool DoProcess()
{
int actualCount = Sitecore.Configuration.Factory.GetConfigNodes(XPathQuery).Count;
if (actualCount == Count)
{
Message = string.Format("Succeeded in finding {0} node(s) with xpath <i>{1}</i>", Count, XPathQuery);
return true;
}
Message = string.Format("Actually found {0} node(s) instead of {1} with xpath <i>{2}</i>", actualCount, Count, XPathQuery);
return false;
}
}
#endregion
</script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sitecore checks monitoring page</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="well">
<div class="alert alert-<%# ChecksRun.IsSuccess ? "success" : "danger" %>">
<asp:PlaceHolder runat="server" Visible="<%# ! ChecksRun.IsSuccess %>">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
</asp:PlaceHolder>
<%# ChecksRun.Message %>
<span class="badge"><%# ChecksRun.Checks.Where(c => c.IsProcessed).Sum(c => c.Duration) %>ms in total</span>
</div>
<asp:PlaceHolder runat="server" Visible="<%# Tags.Any() %>">
<div class="alert alert-info">
<span class="glyphicon glyphicon-tags" aria-hidden="true"></span>
<span>Tags used:</span>
<div class="btn-group" role="group" aria-label="...">
<asp:Repeater runat="server" Datasource="<%# Tags %>">
<ItemTemplate>
<span class="label label-primary"><%# Container.DataItem %></span>
</ItemTemplate>
</asp:Repeater>
</div>
</div>
</asp:PlaceHolder>
<asp:Repeater runat="server" Datasource="<%# ChecksRun.Checks %>">
<ItemTemplate>
<div class="panel panel-<%# ((ISitecoreCheck) Container.DataItem).IsProcessed ? (((ISitecoreCheck) Container.DataItem).IsSuccess ? "success" : "danger") : "default" %>">
<div class="panel-heading">
<h3 class="panel-title">
<%# ((ISitecoreCheck) Container.DataItem).Name %> (<%# Container.DataItem.GetType().Name %>)
<asp:PlaceHolder runat="server" Visible="<%# ((ISitecoreCheck) Container.DataItem).IsProcessed %>">
<span class="badge"><%# ((ISitecoreCheck) Container.DataItem).Duration %>ms</span>
</asp:PlaceHolder>
</h3>
</div>
<div class="panel-body">
<%# ((ISitecoreCheck) Container.DataItem).IsProcessed ? ((ISitecoreCheck) Container.DataItem).Message : "- Not processed -" %>
</div>
</div>
</ItemTemplate>
</asp:Repeater>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>
@daniiiol
Copy link

daniiiol commented Oct 7, 2015

Hi Robin

If you are interested, I have extend your snipped with an additional Check-Class:

if (Tags.Contains("cm", StringComparer.InvariantCultureIgnoreCase))
        {
            ChecksRun.Checks.Add(new DatabaseConnectionCheck("Check if master database is reachable", "master"));
        }
    /// <summary>
    /// Checks the connection to a SQL-Database
    /// </summary>
    class DatabaseConnectionCheck : SitecoreCheckBase
    {
        private string _databaseName;
        public DatabaseConnectionCheck(string message, string database) : base(message)
        {
            _databaseName = database;
        }

        protected override bool DoProcess()
        {
            try
            {
                using (System.Data.SqlClient.SqlConnection connection = new System.Data.SqlClient.SqlConnection(ConfigurationManager.ConnectionStrings[_databaseName].ConnectionString))
                {
                    connection.Open();
                    if(connection.State == System.Data.ConnectionState.Open)
                    {
                        Message = "Connection was succesfully established";
                        return true;
                    }
                }
            }
            catch(Exception ex)
            {
                Message = ex.Message;
                return false;
            }

            Message = "Cannot connect to the database: " + _databaseName;
            return false;

        }
        public override bool AbortOnFail { get { return true; } }
    }

By the way: Very great job and usefull tool!!

Best regards
Dani

@rubenVerschueren
Copy link

I've added mail functionality (see my fork) that will only send if the checks fail. Might be useful to automate the checks via a webjob or something.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment