Instrumentation is an important and integral part of every system because it allows (among some other things) to monitor the health of the system and/or components of it. On a recent engagement, I had to architect a SharePoint solution that integrates with a core LOB system. The SharePoint solution comprises a few workflows that in turn do specific operations with the external system. Even when SEH, Tracing and Logging was implemented I needed something else, performance counters to provide IT staff with real-time information of the solution and in doing so, I leveraged one feature in the operating system as well as giving metrics on the performance of the solution.
Performance counters have been available in .NET since 1.0, however, it’s unusual to use them in a SharePoint solution. The way I approached this is described below:
- I usually use DI/IOC in my solutions (please note, not every solution benefits from this pattern though so implementing it might cause more headaches than expected results). Therefore, I defined my instrumentation capabilities in an interface IInstrumentationManager
public interface IInstrumentationManager { /// <summary> /// Gets the perf counters. /// </summary> /// <value> /// The perf counters. /// </value> IPerfCounters PerfCounters { get; } /// <summary> /// Gets the logger. /// </summary> /// <value> /// The logger. /// </value> ILogger<ILogEntry> Logger { get; } }
- The performance counters are defined in IPerfCounters
public interface IPerfCounters { /// <summary> /// Gets the created configuration. /// </summary> /// <value> /// The created configuration. /// </value> DateTime CreatedOn { get; } /// <summary> /// Gets the instance unique identifier. /// </summary> /// <value> /// The instance unique identifier. /// </value> Guid InstanceId { get; } /// <summary> /// Gets the <see cref="PerformanceCounter"/> with the specified counter name. /// </summary> /// <value> /// The <see cref="PerformanceCounter"/>. /// </value> /// <param name="counterName">Name of the counter.</param> /// <returns></returns> PerformanceCounter this[PerfCounter counterName] { get; } }
- Please note I have an indexer that uses an enum to access a specific performance counter. The concrete class implements IPerfcounters and IDisposable.
public class PerfCounters : IPerfCounters, IDisposable { /// <summary> /// Gets the created configuration. /// </summary> /// <value> /// The created configuration. /// </value> public DateTime CreatedOn { get; private set; } /// <summary> /// Gets the instance unique identifier. /// </summary> /// <value> /// The instance unique identifier. /// </value> public Guid InstanceId { get; private set; } /// <summary> /// Gets the <see cref="PerformanceCounter" /> with the specified counter name. /// </summary> /// <value> /// The <see cref="PerformanceCounter" />. /// </value> /// <param name="counterName">Name of the counter.</param> /// <returns></returns> public PerformanceCounter this[PerfCounter counterName] { get { var retval = CountersAvailable.ContainsKey(counterName) ? CountersAvailable[counterName] : null; return retval; } } /// <summary> /// Gets or sets the counters available. /// </summary> /// <value> /// The counters available. /// </value> protected Dictionary<PerfCounter, PerformanceCounter> CountersAvailable { get; set; } /// <summary> /// Gets the counter data collection. /// </summary> /// <value> /// The counter data collection. /// </value> protected CounterCreationDataCollection CounterDataCollection { get; private set; } /// <summary> /// The disposed /// </summary> /// <value> /// <c>true</c> if this instance is disposed; otherwise, <c>false</c>. /// </value> protected bool IsDisposed { get; private set; } /// <summary> /// Gets or sets the configuration manager. /// </summary> /// <value> /// The configuration manager. /// </value> protected IConfigManager ConfigManager { get; set; } /// <summary> /// Gets or sets the impersonate helper. /// </summary> /// <value> /// The impersonate helper. /// </value> protected IImpersonateHelper ImpersonateHelper { get; set; } /// <summary> /// Initializes a new instance of the <see cref="PerfCounters" /> class. /// </summary> /// <param name="configMgr">The configuration MGR.</param> /// <param name="impersonateHelper">The impersonate helper.</param> public PerfCounters(IConfigManager configMgr, IImpersonateHelper impersonateHelper) { CreatedOn = DateTime.Now; ConfigManager = configMgr; InstanceId = Guid.NewGuid(); ImpersonateHelper = impersonateHelper; CounterDataCollection = new CounterCreationDataCollection(); CountersAvailable = new Dictionary<PerfCounter, PerformanceCounter>(); CreateOrInitializePerfCounters(); } /// <summary> /// Finalizes an instance of the <see cref="PerfCounters"/> class. /// </summary> ~PerfCounters() { Dispose(false); } /// <summary> /// Creates the original initialize perf counters. /// </summary> protected void CreateOrInitializePerfCounters() { NetworkCredential credential; var localAdmin = ConfigManager[Consts.LocalAdminKey]; if (!string.IsNullOrEmpty(localAdmin) && (credential = localAdmin.ConvertToNetworkCredential()) != null) ImpersonateHelper.Impersonate(CreatePerfCounters, credential); AddCountersToInstance(); } /// <summary> /// Creates the perf counters. /// </summary> private void CreatePerfCounters() { // Let's create Performance counter category if (!PerformanceCounterCategory.Exists(Consts.PerformanceCounterCategory)) { CounterDataCollection.AddRange(new[] { new CounterCreationData(Consts.TotalErrorCounter, string.Empty, PerformanceCounterType.NumberOfItems64), new CounterCreationData(Consts.TotalUnhandledErrorCounter, string.Empty, PerformanceCounterType.NumberOfItems64), new CounterCreationData(Consts.TotalTicketsProcessedCounter, string.Empty, PerformanceCounterType.NumberOfItems64), }); PerformanceCounterCategory.Create(Consts.PerformanceCounterCategory, Consts.PerformanceCounterCategory, PerformanceCounterCategoryType.MultiInstance, CounterDataCollection); } } /// <summary> /// Adds the counters to instance. /// </summary> private void AddCountersToInstance() { // Let's ensure category exists and create custom performance counters if (PerformanceCounterCategory.Exists(Consts.PerformanceCounterCategory)) { CountersAvailable.Add(PerfCounter.ErrorCount, new PerformanceCounter(Consts.PerformanceCounterCategory, Consts.TotalErrorCounter, Consts.PerformanceCounterCategory, false)); CountersAvailable.Add(PerfCounter.UnhandledExceptions, new PerformanceCounter(Consts.PerformanceCounterCategory, Consts.TotalUnhandledErrorCounter, Consts.PerformanceCounterCategory, false)); CountersAvailable.Add(PerfCounter.TotalTicketsProcessed, new PerformanceCounter(Consts.PerformanceCounterCategory, Consts.TotalTicketsProcessedCounter, Consts.PerformanceCounterCategory, false)); } } /// <summary> /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// </summary> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// Releases unmanaged and - optionally - managed resources. /// </summary> /// <param name="isDisposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> protected virtual void Dispose(bool isDisposing) { if (!IsDisposed) { if (isDisposing) { if (CountersAvailable != null && CountersAvailable.Count > 0) CountersAvailable.ToList().ForEach(x => { x.Value.Close(); x.Value.Dispose(); }); } IsDisposed = true; } } }
- Performance counters must be created and registered on the computer before they can be used, and in order to do that the user creating them must meet a certain criteria, this is usually done as a separate step in the deployment of the solution using the performance counters, however, I was able to do it by storing encrypted credentials of user with right permissions on a SharePoint List, and this list entry was present then performance counters were created through impersonation of this user, if the list entry was missing nothing occurred. The code responsible for impersonating user was passed in as a callback
public class ImpersonateHelper : IImpersonateHelper { #region "Consts" private const int Logon32Interactive = 2; private const int Logon32ProviderDefault = 0; #endregion #region "Imported functions" /// <summary> /// Logons the user. /// </summary> /// <param name="lpszUsername">The LPSZ username.</param> /// <param name="lpszDomain">The LPSZ domain.</param> /// <param name="lpszPassword">The LPSZ password.</param> /// <param name="dwLogonType">Type of the dw logon.</param> /// <param name="dwLogonProvider">The dw logon provider.</param> /// <param name="phToken">The ph token.</param> /// <returns></returns> [DllImport("advapi32.dll", SetLastError = true)] private static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken); /// <summary> /// Closes the handle. /// </summary> /// <param name="handle">The handle.</param> /// <returns></returns> [DllImport("kernel32.dll", CharSet = CharSet.Auto)] private extern static bool CloseHandle(IntPtr handle); #endregion #region "Impersonate code" #endregion /// <summary> /// Impersonates the specified code to impersonate. /// </summary> /// <param name="codeToImpersonate">The code to impersonate.</param> /// <param name="credential">The credential.</param> /// <returns></returns> /// <exception cref="System.NotImplementedException"></exception> public IExecutionResult Impersonate(Action codeToImpersonate, NetworkCredential credential) { var tokenHandle = IntPtr.Zero; var retval = new ExecutionResult() {Success = false}; if (codeToImpersonate != null && credential != null) { try { if (LogonUser(credential.UserName, credential.Domain, credential.Password, Logon32Interactive, Logon32ProviderDefault, ref tokenHandle)) { using (var newId = new WindowsIdentity(tokenHandle)) { using (var impersonatedUser = newId.Impersonate()) { codeToImpersonate.Invoke(); impersonatedUser.Undo(); } } retval.Success = true; } } catch (Exception ex) { retval.ErrorCondition = ex; } finally { CloseHandle(tokenHandle); } } return retval; } }
- Now, we’ve got so far out performance counters (creation and registration) but you might be wondering, where are we going to store it? Answer is very simple – in the AppDomain. AppDomain is created when the CLR starts a new instance of any .NET application, at the same time developers can store information that’s not persisted once the application has been terminated, so it’s kind of a “property bag” in our application’s process memory.
var defaultCounters = new PerfCounters(container.Resolve<IConfigManager>(), container.Resolve<IImpersonateHelper>()); RegisterPerfCounters(defaultCounters); container.RegisterInstance(typeof(IPerfCounters), defaultCounters); /// <summary> /// Registers the perf counters. // </summary> /// <typeparam name="T"></typeparam> /// <param name="instance">The instance.</param> /// <param name="instanceName">Name of the instance.</param> private void RegisterPerfCounters<T>(T instance, string instanceName = Consts.PerfCounterInstanceName) where T : IPerfCounters { if (AppDomain.CurrentDomain.GetData(instanceName) == null) AppDomain.CurrentDomain.SetData(instanceName, instance); }
- The InstrumentationManager then retrieves our IPerfCounters instance from the AppDomain through Unity
public class InstrumentationManager : IInstrumentationManager { /// <summary> /// Gets the perf counters. /// </summary> /// <value> /// The perf counters. /// </value> /// <exception cref="System.NotImplementedException"></exception> public IPerfCounters PerfCounters { get { return AppDomain.CurrentDomain .GetData(Consts.PerfCounterInstanceName) as IPerfCounters; } } /// <summary> /// Gets the logger. /// </summary> /// <value> /// The logger. /// </value> /// <exception cref="System.NotImplementedException"></exception> public ILogger<ILogEntry> Logger { get { return GlobalService.Current.Container.Resolve<ILogger<ILogEntry>>(); } } }
- Then we increment or decrement our performance counter by accessing the right counter through an indexer that takes an enum
var InstrumentationManager = GlobalService.Current .Container.Resolve<IInstrumentationManager>(); InstrumentationManager.PerfCounters[PerfCounter.TotalTicketsProcessed] .Increment();
When our solution is running, we can then open perfmon and see how it’s behaving