Using SharePoint 2010’s Logging Infrastructure, Part 2

Update 2011-03-23: It seems that creating the registry keys yourself causes the Office 2010 Cumulative Update for February 2011 to fail. Instead add the service to the farm’s services as Mike shows in the comments section. I’ll update my code when I have access tio a SharePoint installation. Thanks to Matthias for the heads up!

Update 2011-02-03: Fixed links to part 1.

Part one of this post illustrated the basics of SharePoint 2010’s logging infrastructure. Let’s start to use it.

Writing to SharePoint’s log

Code like this will produce the expected entries–but the Product field will have a value of “Unknown” as you can see in the screenshot below:

void WriteTraceButton_Click(object sender, EventArgs e) {
 foreach (TraceSeverity ts in Enum.GetValues(typeof(TraceSeverity))) {
  if (ts != TraceSeverity.None) {
   // ts==TraceSeverity.None would cause an ArgumentException: 
   // "Specified value is not supported for the severity parameter."
   SPDiagnosticsCategory dc = new SPDiagnosticsCategory(
    "MyDiagnosticsCategory", ts, EventSeverity.None);
   SPDiagnosticsService.Local.WriteTrace(123, dc, ts, "Just a test.", null);
  }
 }
} // WriteTraceButton_Click()
ULS log with Product field set to "Unknown"

The problem is that the SPDiagnosticsCategory’s Area field of type SPDiagnosticsArea is null and internal. And this type’s Name property defines the Product field’s value. If this confuses you as it confused me have a look at part 1 of this post. In any case you have to derive your own class from SPDiagnosticsServiceBase to set this value.

Here’s a sample implementation for this class using a value of “MyDiagnosticsArea” for the Product field along with some comments on why you need specific things. The ProvideAreas() method also tells SharePoint that this class is going to use an area named “MyDiagnosticsArea” with categories named “MyDiagnosticsCategory0” to “MyDiagnosticsCategory2” as well as which event and trace level should be the default critical level for each category.

When choosing names for your areas and categories keep in mind that these fields will be truncated at 30 characters.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace LogTest.WebPart1 {
 // Don't forget the System.Runtime.InteropServices.Guid attribute
 [Guid("171C481C-EDF2-4383-A0D6-7242020DF395")]
 class MyDiagnosticsService : SPDiagnosticsServiceBase {
  public static string AreaName = "MyDiagnosticsArea";
  public static ushort EventID = 123;

  public enum MyDiagnosticsCategory {
   MyDiagnosticsCategory0,
   MyDiagnosticsCategory1,
   MyDiagnosticsCategory2
  } // enum MyDiagnosticsCategory

  public MyDiagnosticsService() : base("My Diagnostics Service", SPFarm.Local) {
  } // ctor()

  public MyDiagnosticsService(string name, SPFarm parent) : base(name, parent) {
   // SPDiagnosticsServiceBase.GetLocal() wants the default ctor and this one
  } // ctor()

  protected override bool HasAdditionalUpdateAccess() {
   // Without this SPDiagnosticsServiceBase.GetLocal<MyDiagnosticsService>()
   // throws a SecurityException, see
   // http://share2010.wordpress.com/tag/sppersistedobject/
   return true;
  } // HasAdditionalUpdateAccess()

  public static MyDiagnosticsService Local {
   get {
    // SPUtility.ValidateFormDigest(); doesn't work here
    if (SPContext.Current != null) {
     SPContext.Current.Web.AllowUnsafeUpdates = true;
    }
    // (Else assume this is called from a FeatureReceiver)
    return SPDiagnosticsServiceBase.GetLocal<MyDiagnosticsService>();
   }
  } // Local

  public void LogMessage(ushort id, MyDiagnosticsCategory myDiagnosticsCategory,
   TraceSeverity traceSeverity, string message, params object[] data) {
   if (traceSeverity != TraceSeverity.None) {
    // traceSeverity==TraceSeverity.None would cause an ArgumentException:
    // "Specified value is not supported for the severity parameter."
    SPDiagnosticsCategory category 
     = Local.Areas[AreaName].Categories[myDiagnosticsCategory.ToString()];
    Local.WriteTrace(id, category, traceSeverity, message, data);
   }
  } // LogMessage()

  protected override IEnumerable<SPDiagnosticsArea> ProvideAreas() {
   List<SPDiagnosticsCategory> categories = new List<SPDiagnosticsCategory>();
   categories.Add(new SPDiagnosticsCategory(
    MyDiagnosticsCategory.MyDiagnosticsCategory0.ToString(),
    TraceSeverity.None, EventSeverity.None));
   categories.Add(new SPDiagnosticsCategory(
    MyDiagnosticsCategory.MyDiagnosticsCategory1.ToString(),
    TraceSeverity.Verbose, EventSeverity.Verbose));
   categories.Add(new SPDiagnosticsCategory(
    MyDiagnosticsCategory.MyDiagnosticsCategory2.ToString(),
    TraceSeverity.Monitorable, EventSeverity.Error));
   SPDiagnosticsArea area = new SPDiagnosticsArea(
    AreaName, 0, 0, false, categories);
   List<SPDiagnosticsArea> areas = new List<SPDiagnosticsArea>();
   areas.Add(area);
   return areas;
  } // ProvideAreas()
 } // class MyDiagnosticsService
} // namespace LogTest.WebPart1

This code tests the custom class producing the log entries shown below–with the Product field’s value set to “MyDiagnosticsArea”. Of course there are no entries for MyDiagnosticsCategory0 as it’s TraceLevel had been set to TraceLevel.None. And there’s only one entry for MyDiagnosticsCategory2 because it’s critical TraceSeverity level is very high.

void LogMessageButton_Click(object sender, EventArgs e) {
 foreach (MyDiagnosticsService.MyDiagnosticsCategory dc
  in Enum.GetValues(typeof(MyDiagnosticsService.MyDiagnosticsCategory))) {
  MyDiagnosticsService.Local.LogMessage(MyDiagnosticsService.EventID, dc,
   TraceSeverity.None, "Just a test.", null);
  MyDiagnosticsService.Local.LogMessage(MyDiagnosticsService.EventID, dc,
   TraceSeverity.Verbose, "Just a test.", null);
  MyDiagnosticsService.Local.LogMessage(MyDiagnosticsService.EventID, dc,
   TraceSeverity.High, "Just a test.", null);
  MyDiagnosticsService.Local.LogMessage(MyDiagnosticsService.EventID, dc,
   TraceSeverity.Unexpected, "Just a test.", null);
 }
} // LogMessageButton_Click()

ULS log with Product field set to "MyDiagnosticArea"

But we’re not there yet: an entry in each of the Central Administration server’s registry is needed for the new area and it’s categories to show up in the Diagnostic Logging configuration window. The best way to create it is a feature receiver as shown below. It actually creates the registry entry even if the server doesn’t run Central Administration because the server.Role doesn’t reflect this reliably.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using LogTest.WebPart1;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.Win32;

namespace LogTest.Features.Feature1 {
 [Guid("3f799d60-01e7-478b-a63b-07c212c00b0f")]
 public class Feature1EventReceiver : SPFeatureReceiver {
  private const string _wssServicesRegistryKeyPath =
   @"SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\14.0\WSS\Services";

  public override void FeatureActivated(SPFeatureReceiverProperties properties) {
   //Debugger.Break();
   foreach (SPServer server in properties.Definition.Farm.Servers) {
    if (server.Role != SPServerRole.Invalid) {
     RegistryKey hklmRegistryKey = RegistryKey.OpenRemoteBaseKey(
     RegistryHive.LocalMachine, server.Address);
     // Create Registry key for integrating with SharePoint's Central Administration 
     // - Key name = namespace.classname
     // - Value of AssemblyQualifiedName entry = assembly's strong name
     RegistryKey wssServicesRegistryKey = hklmRegistryKey.OpenSubKey(
      _wssServicesRegistryKeyPath, true);
     RegistryKey myDiagnosticsServiceRegistryKey = wssServicesRegistryKey.OpenSubKey(
      typeof(MyDiagnosticsService).ToString());
     if (myDiagnosticsServiceRegistryKey == null) {
      myDiagnosticsServiceRegistryKey = wssServicesRegistryKey.CreateSubKey(
       typeof(MyDiagnosticsService).ToString());
      myDiagnosticsServiceRegistryKey.SetValue(
       "AssemblyQualifiedName", properties.Definition.ReceiverAssembly);
      }
     }
    }
  } // FeatureActivated()

  public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
   //Debugger.Break();
   foreach (SPServer server in properties.Definition.Farm.Servers) {
    if (server.Role != SPServerRole.Invalid) {
     RegistryKey hklmRegistryKey = RegistryKey.OpenRemoteBaseKey(
     RegistryHive.LocalMachine, server.Address);
     // - Remove Registry key for integrating with SharePoint's Central Administration 
     RegistryKey wssServicesRegistryKey = hklmRegistryKey.OpenSubKey(
      _wssServicesRegistryKeyPath, true);
     RegistryKey myDiagnosticsServiceRegistryKey
      = wssServicesRegistryKey.OpenSubKey(typeof(MyDiagnosticsService).ToString());
     if (myDiagnosticsServiceRegistryKey != null) {
      wssServicesRegistryKey.DeleteSubKey(typeof(MyDiagnosticsService).ToString());
     }
    }
   }
  } // FeatureDeactivating()
 } // class Feature1EventReceiver
} // namespace LogTest.Features.Feature1

After deploying the solution go to the Diagnostic Logging configuration window and press OK. That’s the trick to make new areas and categories show up here. As this screenshot shows SharePoint is now using the new area, it’s categories, and their critical level default values.

Administrators can now adjust these levels to their needs in a central place. Part 3 of this post will explain how to use Sharepoint 2010’s logging infrastructure to write to the Windows event log.

Advertisements
This entry was posted in Uncategorized and tagged . Bookmark the permalink.

11 Responses to Using SharePoint 2010’s Logging Infrastructure, Part 2

  1. Remco Bosman says:

    Isn’t adding registry key a problem on Farm installations? As the key is only added local in the server you are activating the feature in? Wonder why MS didn’t put something in to handle this natively.

    • dbremes says:

      Yes it is, thanks for pointing this out. I’ll finish testing a solution to this in the next days and update the code accordingly.
      Done 2010-09-19

      • Mike says:

        I think editing the registry is not the prefered solution. You can add the service to the farm services and sharepoint will do it for you. Add this in a farm feature receiver:


        SPWebService parentService = properties.Feature.Parent as SPWebService;

        if (parentService != null)
        {
        SPFarm farm = parentService.Farm;

        string serviceId = LoggingService.ServiceName;

        // remove service if it allready exists
        foreach (SPService service in farm.Services)
        {
        if (service is LoggingService)
        {
        if (service.Name == serviceId)
        {
        service.Delete();
        }
        }
        }
        }

      • Mike says:

        and of cources:

        // install the service
        farm.Services.Add(serviceToInstall);

  2. David Wrightsman says:

    How can we keep track of logging level changes to these newly added logging categories, so that a deactivea and activate feature will keep the previous logging chages, can you please provide example for OnDeserialization() and Update override methods of SPDiagnosticsServiceBase

    • dbremes says:

      For my customer it was OK to start over with the default values so there was no need to store changes.

      As SPDiagnosticsServiceBase and SPDiagnosticsService don’t override OnDeserialization() I’d guess we don’t have to either. So I would just try to get Update() working by peeking into SPDiagnosticsService’s Update() and SPDiagnosticsServiceBase’s Update() and UpdateShadowCategories() with Reflector.

      It seems the key is in SPDiagnosticsService’s InitSPDiagnosticsServiceFromRegistry() and PushToRegistry() which means keeping your values in the registry and updating your fellow service instances in the farm accordingly.

  3. David Wrightsman says:

    I am trying to store the Categories of Custom Area into an SPPersistedObject, but not sure how to store multiple values in it. I think this can help in keeping track of the custom logging changes made by client, and restore them back, when the solution is retracted and re deployed back. An example to store multiple values in SPPersistedObject or SPPersistedChildCollection would really help, and think would be a better way when compared to storing the values in registry.

    • dbremes says:

      Sorry for taking so long to respond. Somehow couldn’t post from home …

      As there are dozens of samples about using SPPersistedObject can you detail what part of a sample is missing? There are even posts showing how to completely hose one’s hierarchical object store within minutes :-).

  4. Pingback: Using SharePoint 2010′s Logging Infrastructure, Part 3 | Dieter Bremes's Blog

  5. westerdaled says:

    Hi

    I have built my ULSLogger but I am struggling trying to follow Mikes suggestion adding an instance of my ULSLogger to the farm services

    I have declared my
    public class ULSLogger : SPDiagnosticsServiceBase
    {
    // ensured a single instance with a public static property
    // usual codee used for ULSLogging
    ……

    In my FeatureReceiver
    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
    ……

    // old feature has been deleted so now want to add my current logging service
    farm.Services.Add(new (ULSLogger()) ); <—– not sure about the exact syntax here
    farm.Update();

    can anybody help?

  6. westerdaled says:

    Doh solved it… yesterday.

    made my constructor public (from private).. This caused a few retraction issues.

    // finally
    farm.Services.Add(ULSLogger.Current );
    arm.Update();

Leave a Reply

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