SharePoint 2010/2013 Custom ULS

SharePoint has an awesome logging system. Anything that happens in SharePoint can be found in the ULS, and it's highly configurable as to what type of detail you see. In MVC applications, there are 100 different logging frameworks and destinations, but while you are in SharePoint, your code should log to the same place everything else does. A simple way to log would be to make a call to SPTrace, but you lose the ability to name your "product" and get full customization of the logging levels. A better option is extending the SPDiagnosticsServiceBase class with your own custom logging class. It is much easier to use in larger projects, and allows you to make full use of the configuration options in central admin. Here is the class I use, scroll below the class for some notes:

using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using System;
using System.Collections.Generic;
using System.ServiceModel;

namespace SP.Logging
{
    public class LoggingService : SPDiagnosticsServiceBase
    {
        public static string DefaultName
        {
            get { return "Custom Logging Service"; }
        }

        public static string AreaName
        {
            get { return "Custom"; }
        }

        public LoggingService() :
            base(DefaultName, SPFarm.Local) 
        {
        }

        private static LoggingService _current;
        public static LoggingService Current
        {
            get
            {
                if (_current == null)
                {
                    _current = new LoggingService();
                }
                return _current;
            }
        }

        protected override IEnumerable<SPDiagnosticsArea> ProvideAreas()
        {
            List<SPDiagnosticsArea> areas = new List<SPDiagnosticsArea>
            {
                new SPDiagnosticsArea(AreaName, new List<SPDiagnosticsCategory>
                {
                    new SPDiagnosticsCategory(DiagnosticCategory.None.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 0, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.Security.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 100, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.Deployment.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 200, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.Provisioning.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 250, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.Utility.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 300, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.Logging.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 350, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.TimerJob.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 400, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.EventReceivers.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 450, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.WebPart.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 500, false, true),
                    new SPDiagnosticsCategory(DiagnosticCategory.WebService.ToString(), null, TraceSeverity.Medium, EventSeverity.Information, 0, 600, false, true)
                })
            };

            return areas;
        }

        public void WriteMessage(string message, TraceSeverity severity, DiagnosticCategory category)
        {
            try
            {
                SPDiagnosticsCategory diagCategory = this.Areas[AreaName].Categories[category.ToString()];
                this.WriteTrace(diagCategory.Id, diagCategory, severity, message);
            }
            catch (Exception ex)
            {
                HandleError(ex, "Unexpected error writing message to log file.", DiagnosticCategory.Logging, false);
            }
        }

        public void HandleError(Exception exception, string additionalMessage, DiagnosticCategory category, bool throwError)
        {
            try
            {
                SPDiagnosticsCategory diagCategory = this.Areas[AreaName].Categories[category.ToString()];

                string msg = "";
                if (exception.GetType() == typeof(FaultException<ExceptionDetail>))
                {
                    FaultException<ExceptionDetail> typedEx = exception as FaultException<ExceptionDetail>;
                    msg += " EXCEPTION.TOSTRING: " + typedEx.ToString();
                    msg += "  EXCEPTION.STACKTRACE: " + typedEx.StackTrace;
                    if (typedEx.Detail != null)
                    {
                        msg += "   EXCEPTION.DETAIL: " + typedEx.Detail.ToString();
                    }
                }
                else
                {
                    msg = exception.ToString();
                }

                this.WriteTrace(diagCategory.Id, diagCategory, TraceSeverity.Unexpected, additionalMessage + " :: " + Environment.NewLine + msg);

                //  Finally, see if you want to throw the exception back
                if (throwError)
                {
                    throw new SPException(additionalMessage, exception);
                }
            }
            catch (Exception ex)
            {
                if (throwError)
                {
                    throw ex;
                }
                // just let it die at this point, things have gone very wrong
            }
        }        
    }

    public enum DiagnosticCategory
    {
        None = 0,
        Security = 100,
        Deployment = 200,
        Provisioning = 250,
        Utility = 300,
        Logging = 350,
        TimerJob = 400,
        EventReceivers = 450,
        WebPart = 500,
        WebService = 600
    }
}

Couple things things about it, I created an enum of the different categories that I want to log too. This makes things easy when logging to say what part of my code the message is coming from. You can add or remove categories to suit your project, just make sure you update the ProvideAreas() override to match! Also, HandleError automatically sets the severity to Unexpected so you can always see it, but WriteMessage allows you to pass in the severity so you can match how detailed of information you are logging which is important for filtering.

The class above makes it really easy to pass in errors:

    LoggingService.Current.HandleError(ex, "Extra detail about the exception", DiagnosticCategory.WebPart, false);

Or write out trace type logs of where in a function you are:

    LoggingService.Current.WriteMessage("End of helper function GetTime", TraceSeverity.Verbose, DiagnosticCategory.Utility);

One thing we are missing at this point is configuration of levels. Right now, only events of medium level or higher will log to ULS which is fine for normal, but what if you need help tracking down a bug and need verbose detail of what is happening? Logging everything to medium or higher can cause a performance hit on your environment, so just like SharePoint, we are going to add a farm level Feature that allows us to set the verbosity level. Add the feature to whatever wsp you are deploying, and then add an event receiver to the feature:

using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.Win32;
using SP.Logging;
using System;
using System.Runtime.InteropServices;

namespace SP.ULS
{
    /// <summary>
    /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
    /// </summary>
    /// <remarks>
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// </remarks>
    [Guid("ce33bc89-14ca-4c66-8661-dc6d375966ed")]
    public class EventReceiver : SPFeatureReceiver
    {
        const string EventLogApplicationRegistryKeyPath = @"SYSTEM\CurrentControlSet\services\eventlog\Application";

        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPFarm farm = properties.Definition.Farm;

            if (farm != null)
            {
                LoggingService.Current.Update();

                if (LoggingService.Current.Status != SPObjectStatus.Online)
                {
                    LoggingService.Current.Provision();
                }

                foreach (SPServer server in farm.Servers)
                {
                    RegistryKey baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, server.Address);

                    if (baseKey != null)
                    {
                        RegistryKey eventLogKey = baseKey.OpenSubKey(EventLogApplicationRegistryKeyPath, true);

                        if (eventLogKey != null)
                        {
                            RegistryKey loggingServiceKey = eventLogKey.OpenSubKey(LoggingService.AreaName);

                            if (loggingServiceKey == null)
                            {
                                loggingServiceKey = eventLogKey.CreateSubKey(LoggingService.AreaName, RegistryKeyPermissionCheck.ReadWriteSubTree);
                                loggingServiceKey.SetValue("EventMessageFile", @"C:\Windows\Microsoft.NET\Framework\v2.0.50727\EventLogMessages.dll", RegistryValueKind.String);
                            }
                        }
                    }
                }
            }
        }

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            SPFarm farm = properties.Definition.Farm;

            if (farm != null)
            {
                LoggingService.Current.Delete();

                foreach (SPServer server in farm.Servers)
                {
                    RegistryKey baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, server.Address);

                    if (baseKey != null)
                    {
                        RegistryKey eventLogKey = baseKey.OpenSubKey(EventLogApplicationRegistryKeyPath, true);

                        if (eventLogKey != null)
                        {
                            RegistryKey loggingServiceKey = eventLogKey.OpenSubKey(LoggingService.AreaName);

                            if (loggingServiceKey != null)
                            {
                                eventLogKey.DeleteSubKey(LoggingService.AreaName);
                            }
                        }
                    }
                }
            }
        }
    }
}

This code will add your logging service into the diagnostic logging section of central admin so you can turn your logging up or down depending on what you need! (Note: Since the SPDiagnosticsServiceBase class has methods for logging to to the EventViewer as well as ULS, some of the code above is for manipulating those logging levels).

Happy Logging!