Skip to content

Instantly share code, notes, and snippets.

@gregwiechec
Created August 18, 2015 14:57
Show Gist options
  • Save gregwiechec/f7fcdb9bfcc85d4075f7 to your computer and use it in GitHub Desktop.
Save gregwiechec/f7fcdb9bfcc85d4075f7 to your computer and use it in GitHub Desktop.
Search Monitoring tool
<%@ Page Language="c#" Codebehind="SearchMonitor.aspx.cs" AutoEventWireup="False" Inherits="EpiServerThumbnail.Views.plugins.SearchMonitor" Title="IndexingService Health Monitor" %>
<asp:Content ContentPlaceHolderID="HeaderContentRegion" runat="server">
<style type="text/css">
.epi-contentContainer {
width: 900px;
}
.epi-contentContainer h2 {
padding-top: 30px;
}
.search-monitor tr.warning {
background: yellow;
}
.epi-contentContainer table {
width: 900px;
padding: 0;
margin: 0;
}
.epi-contentContainer table.search-monitor td {
padding: 7px 2px 7px 5px;
}
.search-monitor td div {
cursor: pointer;
}
.search-monitor td .description {
padding: 5px 0 0 0;
margin: 0;
border: none;
white-space: pre-wrap;
max-width: 400px;
color: #555;
background: none;
display: none;
}
.search-monitor .status {
max-width: 500px;
overflow-x: auto;
}
</style>
<script type="text/javascript">
$(document).ready(function() {
$('.search-monitor td div.name').click(function () {
$(this).parent().find('.description').show();
});
});
</script>
</asp:Content>
<asp:Content ContentPlaceHolderID="MainRegion" runat="server">
<h2>Automatic checks</h2>
<table class="epi-default search-monitor" cellpadding="0" cellspacing="0">
<colgroup>
<col class="name"/>
<col class="status" />
</colgroup>
<thead>
<th>Test</th>
<th>Status</th>
</thead>
<asp:Repeater runat="server" ID="rptItems">
<ItemTemplate>
<tr <%# this.GetCss((bool)Eval("IsValid")) %>>
<td><div class="name"><%# Eval("Name") %></div><pre class="description"><%# HttpUtility.HtmlEncode(Eval("Description")) %></pre></td>
<td><div class="status"><%# Eval("Status") %></div></td>
</tr>
</ItemTemplate>
</asp:Repeater>
</table>
<h2>Force website reindex</h2>
<p>EPiServer provide a tool for manual site reindex. It's not available under menus. Here is the link to the tool: <a href="<%# this.ReindexUrl %>">reindex</a></p>
<h2>Additional info</h2>
<p>You could also try to run in command prompt : SystemServiceModelReg.exe. The tool should found '%SystemRoot%\Microsoft.Net\Framework\v3.0\Windows Communication Foundation\directory'.</p>
</asp:Content>
using System;
using System.IO;
using System.Linq;
using EPiServer;
using EPiServer.Configuration;
using EPiServer.PlugIn;
using EPiServer.Shell.WebForms;
using EpiServerThumbnail.SearchIndexMonitor;
namespace EpiServerThumbnail.Views.plugins
{
[GuiPlugIn(DisplayName = "Search Monitor", Description = Description, Area = PlugInArea.AdminMenu, Url = "~/Templates/plugins/SearchMonitor.aspx")]
public partial class SearchMonitor : WebFormsBase
{
public const string Description = "Helps troubleshooting problems with search service";
protected override void OnPreInit(EventArgs e)
{
base.OnPreInit(e);
this.MasterPageFile = UriSupport.ResolveUrlFromUIBySettings("MasterPages/EPiServerUI.master");
this.SystemMessageContainer.Heading = "Indexing Service monitoring tool";
this.SystemMessageContainer.Description = Description;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
var checkerStatuses = IndexingServiceCheckCollection.GetAllCheckers().Select(chk=>chk.RunSafe()).ToList();
checkerStatuses.ForEach(el =>
{
el.Status = el.Status.Replace(Environment.NewLine, "<br/>");
//el.Description = el.Description.Replace(Environment.NewLine, "<br/>");
});
this.rptItems.DataSource = checkerStatuses;
this.DataBind();
}
protected string ReindexUrl
{
get
{
var url = this.ResolveUrl(Path.Combine(Settings.Instance.UIUrl.ToString(), "Admin/IndexContent.aspx"));
return url;
}
}
protected string GetCss(bool isValid)
{
return isValid ? "" : "class='warning'";
}
}
}
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace EpiServerThumbnail.Views.plugins {
public partial class SearchMonitor {
/// <summary>
/// rptItems control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
protected global::System.Web.UI.WebControls.Repeater rptItems;
}
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.AccessControl;
using System.Security.Principal;
using System.ServiceModel.Configuration;
using System.Text;
using System.Web;
using System.Web.Configuration;
using EPiServer.Cms.Shell.Search;
using EPiServer.Core;
using EPiServer.Search.Configuration;
using EPiServer.Search.IndexingService.Configuration;
using EPiServer.ServiceLocation;
using EPiServer.Shell.Search;
using EpiServerThumbnail.Business;
using EpiServerThumbnail.Models.Pages;
using log4net;
using log4net.Core;
namespace EpiServerThumbnail.SearchIndexMonitor
{
public class IndexingServiceCheckCollection
{
public static IEnumerable<IIndexingServiceCheckBase> GetAllCheckers()
{
return new IIndexingServiceCheckBase[]
{
new WebConfigDefaultServiceNameCheck(),
new WebConfigSearchActiveCheck(),
new WebConfigMultipleSiteBindingsEnabledCheck(),
new WebConfigSslCheck(),
new AppPoolNameCheck(),
new AppPoolPermissionsCheck(),
new IndexSvcExistsCheck(),
new EndpointRespondCheck(),
new EPiServerLogsEnabledCheck(),
new QuickSearchByIdCheck(),
new QuickSearchByNameCheck()
};
}
}
public abstract class IndexingServiceCheckBase : IIndexingServiceCheckBase
{
protected string Name { get; private set; }
protected virtual string Description {
get { return string.Empty; }
}
protected IndexingServiceCheckBase(string name)
{
this.Name = name;
}
protected abstract bool RunInternal(out string status);
public CheckerStatus Run()
{
string checkerStatus;
var isValid = this.RunInternal(out checkerStatus);
return new CheckerStatus
{
Name = this.Name,
Description = this.Description,
Status = checkerStatus,
IsValid = isValid
};
}
public CheckerStatus RunSafe()
{
try
{
return this.Run();
}
catch (Exception e)
{
return new CheckerStatus
{
Name = this.Name,
Description = this.Description,
Status = "Exception occured when run the check: " + e.Message,
IsValid = false
};
}
}
protected IndexingServiceSection GetIndexingServiceConfigSection()
{
return (IndexingServiceSection) WebConfigurationManager.GetSection("episerver.search.indexingservice");
}
protected SearchSection GetSearchConfiguration()
{
var searchSection = (SearchSection)WebConfigurationManager.GetSection("episerver.search");
return searchSection;
}
protected WindowsIdentity GetAppPoolIdentity()
{
return WindowsIdentity.GetCurrent();
}
protected string GetDefaultServiceUrl()
{
var indexingServiceSection = this.GetSearchConfiguration();
var serviceName = indexingServiceSection.NamedIndexingServices.DefaultService;
var defaultService = indexingServiceSection.NamedIndexingServices.NamedIndexingServices
.OfType<NamedIndexingServiceElement>()
.FirstOrDefault(s => string.Compare(s.Name, serviceName, StringComparison.InvariantCultureIgnoreCase) == 0);
var url = defaultService == null ? "[URL is null]" : defaultService.BaseUri.ToString();
return url;
}
}
public class WebConfigDefaultServiceNameCheck : IndexingServiceCheckBase
{
public WebConfigDefaultServiceNameCheck() : base("Configuration - Service name")
{
}
protected override string Description
{
get { return "Default indexing service name from configuration"; }
}
protected override bool RunInternal(out string status)
{
var indexingServiceSection = this.GetSearchConfiguration();
status = indexingServiceSection.NamedIndexingServices.DefaultService;
return true;
}
}
public class WebConfigMultipleSiteBindingsEnabledCheck : IndexingServiceCheckBase
{
public WebConfigMultipleSiteBindingsEnabledCheck()
: base("Configuration - serviceHostingEnvironment")
{
}
protected override string Description
{
get { return "Check if multipleSiteBindingsEnabled is enabled in configuration. Usually it should be enabled"; }
}
protected override bool RunInternal(out string status)
{
var hostingSection =
(ServiceHostingEnvironmentSection)
WebConfigurationManager.GetSection("system.serviceModel/serviceHostingEnvironment");
if (hostingSection == null)
{
status = "Section not found";
return false;
}
status = string.Format("MultipleSiteBindingsEnabled: {0}, AspNetCompatibilityEnabled: {1}",
hostingSection.MultipleSiteBindingsEnabled, hostingSection.AspNetCompatibilityEnabled);
return hostingSection.MultipleSiteBindingsEnabled;
}
}
public class WebConfigSslCheck : IndexingServiceCheckBase
{
public WebConfigSslCheck()
: base("Configuration - SSL")
{
}
protected override string Description
{
get { return
@"Check configuration against SSL search service. The most important is security part:
<security mode=""Transport\"">
<transport clientCredentialType=""None""></transport>
</security>"; }
}
protected override bool RunInternal(out string status)
{
var bindingsSection = (BindingsSection)WebConfigurationManager.GetSection("system.serviceModel/bindings");
var bindingCollectionElements = bindingsSection.BindingCollections;
const string bindingName = "IndexingServiceCustomBinding";
var bindings =
bindingCollectionElements.Where(
b =>
b.ConfiguredBindings.Any(
cb =>
string.Compare(cb.Name, bindingName,
StringComparison.InvariantCultureIgnoreCase) == 0)).ToList();
if (bindings.Count == 0)
{
status = string.Format("Binding '{0}' not found", bindingName);
return false;
}
var sb = new StringBuilder();
if (bindings.Count > 1)
{
sb.AppendLine(string.Format("Found {0} bindings", bindings.Count));
}
var isValid = true;
foreach (var binding in bindings)
{
sb.AppendLine(string.Format("Name: {0}, Type: {1}", binding.BindingName, binding.BindingType));
var configuredBinding = binding.ConfiguredBindings.FirstOrDefault(
cb => string.Compare(cb.Name, bindingName, StringComparison.InvariantCultureIgnoreCase) == 0);
var webHttpBindingElement = configuredBinding as WebHttpBindingElement;
if (webHttpBindingElement != null)
{
isValid &= WriteSettings(sb, "MaxBufferPoolSize", webHttpBindingElement.MaxBufferPoolSize.ToString(), "1073741824");
isValid &= WriteSettings(sb, "MaxReceivedMessageSize", webHttpBindingElement.MaxReceivedMessageSize.ToString(), "2147483647");
isValid &= WriteSettings(sb, "MaxBufferSize", webHttpBindingElement.MaxBufferSize.ToString(), "2147483647");
isValid &= WriteSettings(sb, "Security.Mode", webHttpBindingElement.Security.Mode.ToString(), "Transport");
isValid &= WriteSettings(sb, "Security.Transport.ClientCredentialType", webHttpBindingElement.Security.Transport.ClientCredentialType.ToString(), "None");
isValid &= WriteSettings(sb, "ReaderQuotas.MaxStringContentLength",
webHttpBindingElement.ReaderQuotas.MaxStringContentLength.ToString(), "10000000");
}
if (bindings.Count > 1)
{
sb.Append("-----");
}
}
status = sb.ToString();
return isValid;
}
private static bool WriteSettings(StringBuilder sb, string name, string value, string expectedValue)
{
sb.AppendLine(string.Format("{0}: '{1}' [{2}]", name, value, expectedValue));
return string.Compare(value, expectedValue, StringComparison.InvariantCultureIgnoreCase) == 0;
}
}
public interface IIndexingServiceCheckBase
{
CheckerStatus RunSafe();
}
public class AppPoolNameCheck : IndexingServiceCheckBase
{
public AppPoolNameCheck()
: base("App Pool user name")
{
}
protected override string Description
{
get { return "The Application Pool name used by application. You should double check if you use the correct one."; }
}
protected override bool RunInternal(out string status)
{
var windowsIdentity = this.GetAppPoolIdentity();
if (windowsIdentity == null)
{
status = "Unable to get App Pool name";
return false;
}
status = windowsIdentity.Name;
return true;
}
}
public class AppPoolPermissionsCheck : IndexingServiceCheckBase
{
public AppPoolPermissionsCheck()
: base("App pool permisions")
{
}
protected override string Description
{
get { return "Check if application pool account has access to index directory"; }
}
protected override bool RunInternal(out string status)
{
var result = new StringBuilder();
var searchSection = this.GetIndexingServiceConfigSection();
var defaultIndex = searchSection.NamedIndexesElement.DefaultIndex;
var namedIndex = searchSection.NamedIndexesElement.NamedIndexes
.OfType<NamedIndexElement>()
.FirstOrDefault(
i => string.Compare(i.Name, defaultIndex, StringComparison.InvariantCultureIgnoreCase) == 0);
if (namedIndex == null)
{
result.Append("Default index not found").Append(Environment.NewLine);
status = result.ToString();
return false;
}
var directoryPath = namedIndex.DirectoryPath;
result.AppendFormat("Index directory: {0}", directoryPath).Append(Environment.NewLine);
var convertedDirectoryPath = namedIndex.GetDirectoryPath();
if (convertedDirectoryPath != directoryPath)
{
result.AppendLine("Physical path:");
result.AppendFormat(" [{0}]", convertedDirectoryPath);
result.AppendLine();
}
if (Directory.Exists(convertedDirectoryPath) == false)
{
result.Append("Index directory not exists").Append(Environment.NewLine);
status = result.ToString();
return false;
}
var windowsIdentity = this.GetAppPoolIdentity();
if (windowsIdentity != null)
{
var hasAccessTo = HasAccessTo(convertedDirectoryPath, windowsIdentity);
result.AppendFormat("Acces to directory: {0}", hasAccessTo).Append(Environment.NewLine);
status = result.ToString();
return true;
}
result.Append("[Unable to get Application pool name]").Append(Environment.NewLine);
status = result.ToString();
return false;
}
private static bool HasAccessTo(string fileSystemPath, WindowsIdentity windowsIdentity)
{
// from codeplex
if (windowsIdentity.Groups == null)
{
return false;
}
try
{
DirectorySecurity dSecurity = Directory.GetAccessControl(fileSystemPath, AccessControlSections.Access);
foreach (
FileSystemAccessRule accessRule in dSecurity.GetAccessRules(true, true, typeof (SecurityIdentifier))
)
{
if (FileSystemRights.ReadData == (accessRule.FileSystemRights & FileSystemRights.ReadData))
{
if (windowsIdentity.Groups.Contains(accessRule.IdentityReference))
{
if (AccessControlType.Deny == accessRule.AccessControlType)
{
return false;
}
else if (AccessControlType.Allow == accessRule.AccessControlType)
{
return true;
}
}
}
}
}
// This is very annoying. The whole purpose of this method is to find out if the current user has permission
// but there are some directories to which the current user has access to, but they need to run elevated to
// get the AccessRules. At the moment I can't find an API to query to know if querying the access rules of
// an item requires UAC elevation
catch (UnauthorizedAccessException)
{
}
return false;
}
}
public class IndexSvcExistsCheck : IndexingServiceCheckBase
{
public IndexSvcExistsCheck()
: base("Index SVC file exists")
{
}
protected override string Description
{
get { return "Check if 'IndexingService.svc' files exists on the disk."; }
}
protected override bool RunInternal(out string status)
{
var url = this.GetDefaultServiceUrl();
var uri = new Uri(url, UriKind.Absolute);
var localFilename = HttpContext.Current.Server.MapPath("~" + uri.LocalPath);
var result = new StringBuilder();
result.AppendFormat("Trying to map: '{0}'", localFilename).AppendLine();
var fileExists = File.Exists(localFilename);
result.AppendLine("File exists: " + fileExists);
status =result.ToString();
return fileExists;
}
}
public class EndpointRespondCheck : IndexingServiceCheckBase
{
public EndpointRespondCheck()
: base("WCF service GET response")
{
}
protected override string Description
{
get
{
return
@"Run GET request against WCF search service and see the result. It should be 'Endpoint not found', and 'Method not allowed'.
If you don't get expected results, then try to reconfigure service to localhost, like localhost:112233/indexingservice/indexingservice.svc;";
}
}
protected override bool RunInternal(out string status)
{
var url = this.GetDefaultServiceUrl();
var updateUrl = url + "/update/?accesskey=local";
var sb = new StringBuilder();
string message;
var isValid = this.GetWebRequest(url, HttpStatusCode.NotFound, out message);
sb.Append(url).Append(": ").AppendLine(message);
isValid &= this.GetWebRequest(updateUrl, HttpStatusCode.MethodNotAllowed, out message);
sb.Append(updateUrl).Append(": ").AppendLine(message);
status = sb.ToString();
return isValid;
}
private bool GetWebRequest(string url, HttpStatusCode expectedStatusCode, out string message)
{
var webRequest = WebRequest.Create(url );
try
{
using (var webResponse = webRequest.GetResponse())
{
using (var objStream = webResponse.GetResponseStream())
{
if (objStream == null)
{
message = "Unable to get response";
return false;
}
using (var objReader = new StreamReader(objStream))
{
var result = objReader.ReadToEnd().Trim();
message = result;
return false;
}
}
}
}
catch (WebException exception)
{
var httpWebResponse = (HttpWebResponse)exception.Response;
var result = new StringBuilder();
result.AppendFormat("Status code: {0} - {1} [expected: {2}]", (int)httpWebResponse.StatusCode,
httpWebResponse.StatusDescription, (int)expectedStatusCode);
message = result.ToString();
return httpWebResponse.StatusCode == expectedStatusCode;
}
}
}
public class EPiServerLogsEnabledCheck : IndexingServiceCheckBase
{
public EPiServerLogsEnabledCheck()
: base("EPiServer logs enabled")
{
}
protected override string Description
{
get { return "Check if the log4net logging is enabled."; }
}
protected override bool RunInternal(out string status)
{
var loggers = LogManager.GetRepository().GetCurrentLoggers();
var logger =
loggers.FirstOrDefault(
l =>
string.Compare(l.Name, "EPiServer.Search.IndexingService.IndexingService",
StringComparison.InvariantCultureIgnoreCase) == 0);
if (logger == null)
{
status = "Logger not enabled";
return false;
}
var enabledLevels = new List<string>();
var notEnabledLevels = new List<string>();
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Off);
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Emergency);
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Fatal);
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Alert);
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Warn);
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Error);
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Info);
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Trace);
CheckLevel(enabledLevels, notEnabledLevels, logger, Level.Debug);//TODO: które włączone a które nie
status = "ON: " + string.Join(", ", enabledLevels) + Environment.NewLine + "OFF: " +
string.Join(",", notEnabledLevels);
return true;
}
private void CheckLevel(ICollection<string> enabledLevels, ICollection<string> notEnabledLEvels, ILogger logger, Level level)
{
if (logger.IsEnabledFor(level))
{
enabledLevels.Add(level.Name);
}
else
{
notEnabledLEvels.Add(level.Name);
}
}
}
public class QuickSearchByIdCheck : IndexingServiceCheckBase
{
public QuickSearchByIdCheck()
: base("Search StartPage by Id")
{
}
protected override string Description
{
get { return "Check if searching by StartPage ID is working. It could be that search by ID is working while searchby text not."; }
}
protected override bool RunInternal(out string status)
{
return SearchHelper.Search(ContentReference.StartPage.ID.ToString(CultureInfo.InvariantCulture), out status);
}
}
public class QuickSearchByNameCheck : IndexingServiceCheckBase
{
public QuickSearchByNameCheck()
: base("Search StartPage by Name")
{
}
protected override string Description
{
get { return "Check if searching by StartPage page name is working."; }
}
protected override bool RunInternal(out string status)
{
return SearchHelper.Search(ContentReference.StartPage.Get<StartPage>().Name, out status);
}
}
public class WebConfigSearchActiveCheck : IndexingServiceCheckBase
{
public WebConfigSearchActiveCheck()
: base("Configuration - episerver.search Search is Active")
{
}
protected override string Description
{
get { return "Check if search is enabled in web.config - episerver.search section"; }
}
protected override bool RunInternal(out string status)
{
var searchSection = this.GetSearchConfiguration();
status = searchSection.Active.ToString();
return searchSection.Active;
}
}
public static class SearchHelper
{
public static bool Search(string text, out string message)
{
var provider = ServiceLocator.Current.GetInstance<SearchProvidersManager>();
var pageProvider =
provider.GetEnabledProvidersByPriority(ContentSearchProviderConstants.PageArea, true).FirstOrDefault();
if (pageProvider == null)
{
message = "Provider not found";
return false;
}
var searchResults = pageProvider.Search(new Query(text)
{
MaxResults = 10,
SearchRoots = new[] { ContentReference.RootPage.ID.ToString(CultureInfo.InvariantCulture) },
FilterOnCulture = false
}).ToList();
if (searchResults.Any() == false)
{
message = string.Format("Nothing found ['{0}']", text);
return false;
}
message = string.Format("Found ['{0}']: {1}", text, string.Join(",", searchResults.Select(p => p.Title)));
return true;
}
}
public class CheckerStatus
{
public string Name { get; set; }
public string Description { get; set; }
public string Status { get; set; }
public bool IsValid { get; set; }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment