Yesterday evening, I decided to fish around in my local Sitecore instance’s Web.config to look for customization opportunities — doing this definitely beats vegging out in front of the television any day — and thought it would be an interesting exercise to create a custom hook.
Before I dive into the custom hook I built, I’d like to discuss what a Sitecore hook is.
You can consider a hook to be an object containing code that you would like executed in your Sitecore instance, but see no logical place to put said code.
You’re probably saying to yourself “Mike, that is an extremely generic and ridiculous definition.” I don’t blame you for thinking this, although I can’t really define what a hook is any better than that. If you have a better definition, please leave a comment.
However, to put things into context that might aid in defining what a hook could be, a hook is usually defined as an object containing code that is executed periodically by a defined configuration setting time interval — albeit this isn’t a mandatory constraint since one could inject any code via a hook, as long as that hook implements the Sitecore.Events.Hooks.IHook interface.
This interface defines one method — the Initialize() method — which has a very simple signature: it takes in no parameters and does not return anything, thus giving you lots of freedom around how you implement your hook.
Out of the box, Sitecore employs two hooks:
- Sitecore.Diagnostics.HealthMonitorHook – a hook that launches a pipeline periodically to log cache, memory, and performance counter information to the Sitecore log.
- Sitecore.Diagnostics.MemoryMonitorHook – a hook that monitors memory periodically on the server, and clears caches/invokes the garbage collector when a defined thresholds are exceeded and settings defined in the Web.config allow for these actions.
Both hooks are defined in Sitecore.Kernel.dll.
Keeping with the monitoring theme of the prepackaged hooks, I decided to build a hook that monitors the size of my Sitecore databases after an elapsed period of time, and logs this information into my Sitecore log.
First, I created a data transfer object (DTO) that represents a size snapshot of a database:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Sitecore.Sandbox.Utilities.Database.DTO { public class DatabaseStatistics { public string Name { get; set; } public string Size { get; set; } public string UnallocatedSpace { get; set; } } }
The above DTO is created and returned by a class that gets information out of a database using the Sitecore.Data.SqlServer.SqlServerDataApi utility class used by Sitecore for database operations in MS SQL Server:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Sandbox.Utilities.Database.DTO; namespace Sitecore.Sandbox.Utilities.Database.Base { public interface IDatabaseStatisticsGatherer { DatabaseStatistics GetDatabaseStatistics(); } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Data.DataProviders.Sql; using Sitecore.Data.SqlServer; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Database.Base; using Sitecore.Sandbox.Utilities.Database.DTO; namespace Sitecore.Sandbox.Utilities.Database { public class DatabaseStatisticsGatherer : IDatabaseStatisticsGatherer { private const string GetSizeStaticsSQL = "exec sp_spaceused"; // this sproc gives database size information private SqlDataApi SqlDataApi { get; set; } private DatabaseStatisticsGatherer(string connectionString) : this(CreateNewSqlServerDataApi(connectionString)) { } private DatabaseStatisticsGatherer(SqlDataApi sqlDataApi) { SetSqlDataApi(sqlDataApi); } private void SetSqlDataApi(SqlDataApi sqlDataApi) { Assert.ArgumentNotNull(sqlDataApi, "sqlDataApi"); SqlDataApi = sqlDataApi; } public DatabaseStatistics GetDatabaseStatistics() { IEnumerable<string> columnValues = SqlDataApi.GetStringList(GetSizeStaticsSQL, new object[0]); return CreateNewDatabaseStatistics(columnValues); } private static DatabaseStatistics CreateNewDatabaseStatistics(IEnumerable<string> columnValues) { if (columnValues == null || columnValues.Count() < 1) { return null; } return new DatabaseStatistics { Name = columnValues.ElementAtOrDefault(0), Size = columnValues.ElementAtOrDefault(1), UnallocatedSpace = columnValues.ElementAtOrDefault(2) }; } private static SqlDataApi CreateNewSqlServerDataApi(string connectionString) { Assert.ArgumentNotNullOrEmpty(connectionString, "connectionString"); return new SqlServerDataApi(connectionString); } public static IDatabaseStatisticsGatherer CreateNewDatabaseStatisticsGatherer(string connectionString) { return new DatabaseStatisticsGatherer(connectionString); } public static IDatabaseStatisticsGatherer CreateNewDatabaseStatisticsGatherer(SqlDataApi sqlDataApi) { return new DatabaseStatisticsGatherer(sqlDataApi); } } }
I decided to follow the paradigm set forth by Sitecore.Diagnostics.HealthMonitorHook: having my hook invoke a pipeline after an elapsed period of time. Here are this pipeline’s DTO and the pipeline class itself:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Pipelines; namespace Sitecore.Sandbox.Pipelines.DatabaseMonitor.DTO { public class DatabaseMonitorArgs : PipelineArgs { public IEnumerable<string> Databases { get; set; } } }
using System; using System.Collections; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Text; using Sitecore.Collections; using Sitecore.Data.Sql; using Sitecore.Diagnostics; using Sitecore.Sandbox.Pipelines.DatabaseMonitor.DTO; using Sitecore.Sandbox.Utilities.Database; using Sitecore.Sandbox.Utilities.Database.Base; using Sitecore.Sandbox.Utilities.Database.DTO; namespace Sitecore.Sandbox.Pipelines.DatabaseMonitor { public class DatabaseMonitor { public void LogDatabasesSize(DatabaseMonitorArgs args) { foreach (string connectionStringKey in args.Databases) { LogDatabaseStatistics(connectionStringKey); } } private void LogDatabaseStatistics(string connectionStringKey) { LogDatabaseStatistics(GetDatabaseStatistics(connectionStringKey)); } private static DatabaseStatistics GetDatabaseStatistics(string connectionStringKey) { IDatabaseStatisticsGatherer gatherer = DatabaseStatisticsGatherer.CreateNewDatabaseStatisticsGatherer(GetConnectionString(connectionStringKey)); return gatherer.GetDatabaseStatistics(); } private void LogDatabaseStatistics(DatabaseStatistics statistics) { if (statistics == null) { return; } Log.Info(GetLogEntry(statistics), this); } private static string GetConnectionString(string connectionStringKey) { Assert.ArgumentNotNullOrEmpty(connectionStringKey, "connectionStringKey"); return ConfigurationManager.ConnectionStrings[connectionStringKey].ConnectionString; } private static string GetLogEntry(DatabaseStatistics statistics) { Assert.ArgumentNotNull(statistics, "statistics"); return string.Format("Database size statistics: '{0}' (size: {1}, unallocated space: {2})", statistics.Name, statistics.Size, statistics.UnallocatedSpace); } } }
Now, it’s time to hookup my hook. I followed how Sitecore.Diagnostics.HealthMonitorHook uses the AlarmClock class to continuously invoke code after a specified period of time:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Diagnostics; using Sitecore.Events.Hooks; using Sitecore.Pipelines; using Sitecore.Services; using Sitecore.Text; using Sitecore.Sandbox.Pipelines.DatabaseMonitor.DTO; namespace Sitecore.Sandbox.Hooks { public class DatabaseMonitorHook : IHook { private static readonly char[] Delimiters = new char[] { ',', '|' }; private static AlarmClock _alarmClock; private IEnumerable<string> Databases { get; set; } private TimeSpan Interval { get; set; } private bool Enabled { get; set; } public DatabaseMonitorHook(string databases, string interval, string enabled) { SetDatabases(databases); SetInterval(interval); SetEnabled(enabled); } private void SetDatabases(string databases) { Assert.ArgumentNotNullOrEmpty(databases, "databases"); Databases = databases.Split(Delimiters, StringSplitOptions.RemoveEmptyEntries).Select(database => database.Trim()); } private void SetInterval(string interval) { Assert.ArgumentNotNullOrEmpty(interval, "interval"); Interval = TimeSpan.Parse(interval); } private void SetEnabled(string enabled) { bool isEnabled; if (bool.TryParse(enabled, out isEnabled)) { Enabled = isEnabled; } } private void AlarmClock_Ring(object sender, EventArgs args) { Pipeline.Start("databaseMonitor", CreateNewDatabaseMonitorArgs()); } private DatabaseMonitorArgs CreateNewDatabaseMonitorArgs() { return new DatabaseMonitorArgs { Databases = Databases }; } public void Initialize() { if (Enabled && _alarmClock == null) { _alarmClock = CreateNewAlarmClock(Interval); _alarmClock.Ring += new EventHandler<EventArgs>(AlarmClock_Ring); } } private static AlarmClock CreateNewAlarmClock(TimeSpan interval) { return new AlarmClock(interval); } } }
I glued everything together via a new config file where I define my new hook and pipeline:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <hooks> <hook type="Sitecore.Sandbox.Hooks.DatabaseMonitorHook, Sitecore.Sandbox"> <param desc="databases">core, master, web</param> <param desc="interval">00:01:00</param> <param desc="enabled">true</param> </hook> </hooks> <processors> <databaseMonitor> <processor type="Sitecore.Sandbox.Pipelines.DatabaseMonitor.DatabaseMonitor, Sitecore.Sandbox" method="LogDatabasesSize"/> </databaseMonitor> </processors> </sitecore> </configuration>
After all of the above code was compiled and the configuration file was saved, I kick-started my local Sitecore instance by navigating to the home page of my site. I then walked away for a bit. When I returned, I opened up my most recent log file and saw the following:
As shown in my log file entries, my hook along with its supporting classes were all hooked up correctly. 🙂