Home » Customization » Get Hooked on Hooks: Create a Custom Hook in Sitecore

Get Hooked on Hooks: Create a Custom Hook in Sitecore

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

Tweets

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:

  1. Sitecore.Diagnostics.HealthMonitorHook – a hook that launches a pipeline periodically to log cache, memory, and performance counter information to the Sitecore log.
  2. 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:

size-stats-logged

As shown in my log file entries, my hook along with its supporting classes were all hooked up correctly. 🙂

Advertisements

Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: