Sitecore PowerShell Extensions History engine?

As many of your know Sitecore PowerShell Extensions is a valuable tool in the Sitecore ecosystem. I cannot recall how many times SPE has saved me from repeated and tedious tasks.

For the longest time I wanted to store the command history for audit purposes. Finally I got some time to get into some code hacking to get this done.

One, you have to realize that SPE relies on Microsoft PowerShell and so we cannot mold it as we want. Ideally it would be great if we could develop a SPE Command pipeline.

So we start off by setting up MongoDB. Part of this blog post depends on work in Use MongoDB to store Sitecore log entries centrally!

Lets start by adding a connection string to a new MongoDB collection:

Setup a class called SPECommand to hold the structure of the record. Here is the sample code:

using MongoDB.Bson;
using System;

namespace SPE.Logging.Log
{
    public class SPECommand
    {
        public ObjectId Id { get; set; }
        public string UserName { get; set; }
        public string Command { get; set; }
        public DateTime Date { get; set; }
    }
}

Create a repository to interact with MongoDB, here is the sample code:

using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
using MongoDB.Driver.Linq;
using Sitecore.Diagnostics;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SPE.Logging.Log
{
    public class CommandHistoryRepository
    {
        internal const string CollectionName = "specommandhistory";
        private readonly MongoCollection _commandHistoryCollection;

        public CommandHistoryRepository(string connectionString)
        {
            _commandHistoryCollection = GetCollection(connectionString, CollectionName);
            Assert.IsNotNull(_commandHistoryCollection, "commandHistoryCollection");
        }

        public IEnumerable GetAll()
        {
            return _commandHistoryCollection.FindAllAs();
        }

        public SPECommand Get(ObjectId id)
        {
            return _commandHistoryCollection.FindOneAs(GetIDQuery(id));
        }

        public bool Set(SPECommand SPECommand)
        {
            return _commandHistoryCollection.Insert(SPECommand).Ok;
        }

        public IEnumerable GetByDateRange(DateTime startDate, int numberOfDays)
        {
            return _commandHistoryCollection.AsQueryable().Where(c => c.Date >= startDate && c.Date < startDate.AddDays(numberOfDays)).ToArray();
        }

        protected IMongoQuery GetIDQuery(ObjectId id)
        {
            return Query.EQ(c => c.Id, id);
        }

        private static MongoCollection GetCollection(string connectionString, string collectionName)
        {
            var url = new MongoUrl(connectionString);
            return new MongoClient(url).GetServer().GetDatabase(url.DatabaseName).GetCollection(collectionName);
        }
    }
}

Now that we have everything setup to write to Mongo, lets get into the depths of the SPE code to find the place where we can log the commands being executed.

In the Cognifide.PowerShell project go into Core > Host > ScriptSession.cs file. In the ExecuteScriptPart function on like 599, you can add the following to log the command being executed. Here is the sample code:

            LogUtils.Info("Executing a Sitecore PowerShell Extensions script." + script, this);

            CommandHistoryRepository commandHistoryRepository = new CommandHistoryRepository(ConfigurationManager.ConnectionStrings["specommandhistory"].ConnectionString);
            var logCommand = new SPECommand
            {
                UserName = Sitecore.Context.GetUserName(),
                Command = script,
                Date = DateTime.Now
            };
            commandHistoryRepository.Set(logCommand);

Also in the Measure method we could add additional logging on who executed the command for the log entry.

            //We need to modify the Measure method
            return SpeTimer.Measure("script execution", () =>
            {
                try
                {
                    using (powerShell = NewPowerShell())
                    {
                        powerShell.Commands.AddScript(script);
                        return ExecuteCommand(stringOutput, marshalResults);
                    }
                }
                finally
                {
                    powerShell = null;
                }
            });

Add in — {Sitecore.Context.GetUserName()} to the LogUtile.Info so that we record the user who executed that command.

    public static class SpeTimer
    {
        public static T Measure(string message, Func action) where T : class
        {
            try
            {
                var stopWatch = new Stopwatch();
                stopWatch.Start();

                var result = action();

                stopWatch.Stop();
                
                LogUtils.Info($"The {message} completed in {stopWatch.ElapsedMilliseconds} ms. -- {Sitecore.Context.GetUserName()}", typeof(SpeTimer));
                return result;
            }
            catch (Exception ex)
            {
                LogUtils.Error(ex.Message, typeof(SpeTimer));
                throw;
            }
        }

This might not be the best way to add the History Engine to SPE but its a start. We could also have additional features like success of the command, script status monitor and a report pulling all of this Mongo information.

spe3

spe2

spe1

If you have any questions or concerns, please get in touch with me. (@akshaysura13 on twitter or on Slack).

Leave a Reply

Your email address will not be published. Required fields are marked *