Created
October 3, 2015 14:37
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<%@ 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> |
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
Hi Robin
If you are interested, I have extend your snipped with an additional Check-Class:
By the way: Very great job and usefull tool!!
Best regards
Dani