Extending The SharePoint 2010 Health & Usage – Part 4: Writing a Custom Usage Receiver

This is the 4th article and last article in a series where I have been discussing the extensibility offered with the SharePoint 2010 Usage and Health services. If you have read all the prior articles to this point then bravo for you! I really hope this last article was worth the wait. At this point I have provided an overview of the Health and Usage Service, discussed the development of a Custom Usage Provider and showed you how to create custom Health reports and host those within Central Administration. This last article covers my favorite Health and Usage extensibility; “Usage Receivers”. In fact, here is a little secret I will let you in on, to build its analytic reports the SharePoint Web Analytics Service application uses a Usage Receiver to grab all its data from the OOB Request Usage Provider. So take a look at the SharePoint 2010 Web Analytics with all its data and rich reporting and know that all came from a Usage Receiver. 

If you missed the prior articles the list below is for you, otherwise lets jump into the article…

  1. Feature and Capability Overview
  2. Writing a Custom Usage Provider
  3. Writing Custom Reports
  4. Writing a Custom Usage Receiver (this article)

Code Download: Microsoft.SP.Usage.zip (160 kb)

The Scenario

As a final reminder, I am walking you through the building a solution to track file downloads from SharePoint Document Libraries and report on the results. It’s key this solution be scalable so we will are creating a custom Usage Provider, adding a few custom usage reports in Central Administration, and we will now create a Usage Receiver to help us display download information within a Web Part for each SPSite within our environment.

Overview

A custom Usage Receiver is much like a List or List Item event receiver however you register a Usage Receiver with one or more Usage Providers. This allows your Usage Receiver to receive callbacks each time a Usage Provider completes an import of usage data. As you may recall from the first article in this series Usage Providers, are called to import data from *.usage files via a Import Timer Job which is by default set to run every 30 minutes. Once the Usage Provider has completed processing its Usage Entries any registered Usage Receivers will be called in series. If no data has been collected since the last time the Import Timer Job has fired then the Usage Provider will not be called and as a result any registered Usage Receivers will not receive a call either.

 

blog-usage-image14

 

Building the Usage Receiver

If you have been following this series of articles almost every time I start building a component I always seem to start with a class which derives from some SharePoint abstract class out of Microsoft.SharePoint.Administration namespace – well this component is no different. We will start with creating a class named DownloadUsageReceiver which derives from SPUsageReciver. We only have one method we need (or even can) override which is UsageImported. When called, this method is passed a SPUsageReceiverProperties object which among other things has an IEnumerator of Usage Entries which were imported by the owning Usage Provider. There is also a reference to the Usage Provider, which was unfortunately named UsageDefinition, but I bet you could have figured that one out.

So for our solution we want to take each SPUsageEntry from the enumerator and track the total document download count and total size of all documents downloaded per Site Collection and store this total within the site collection’s root web’s AllProperties property bag. Its from here that our Web Part will pull the download information for display within the Site Collection.

blog-usage-image15

This data is stored within the property bag as a string which represents a XML serialized class I call DownloadInformation. It’s a simple class which I mark as [serializable] and have two public getter/setter fields which store the data points we care about.

 [Serializable]
 public class DownloadInformation
 {
        
     public long TotalDownloads { get; set; }
     public long TotalBytesDownloaded { get; set; }

 }

 

Once we override the UsageImported method on our DownloadUsageReceiver class we create a dictionary of DownloadInformation objects which has a GUID key which represents the Site Collection Id. We then loop through each SPUsageEntry casting it to a DownloadUsageEntry where we then increment the total download size with the size read from the entry and update the entry count. Of course, we first have to ensure a DownloadInformation object exists in the dictionary and if that’s not the case its created.

public override void UsageImported(SPUsageReceiverProperties usageProperties)
{
    int entries = 0;
            
    try
    {

        if (usageProperties == null || usageProperties.UsageEntries == null)
        {
            return;
        }
                

        IEnumerator<SPUsageEntry> usageEntries = usageProperties.UsageEntries;


        Dictionary<Guid, DownloadInformation> siteCollectionTracking = new Dictionary<Guid, DownloadInformation>();

        while (usageEntries.MoveNext())
        {

                    
            DownloadUsageEntry entry = usageEntries.Current as DownloadUsageEntry;
            if (entry == null)
            {
                //these are not the droids we are after
                continue;
            }


            if (siteCollectionTracking.ContainsKey(entry.SiteId))
            {

                DownloadInformation downloadInfo = siteCollectionTracking[entry.SiteId];
                downloadInfo.TotalDownloads++;
                downloadInfo.TotalBytesDownloaded += entry.Size;

            }
            else
            {

                siteCollectionTracking.Add(entry.SiteId, new DownloadInformation() { TotalDownloads = 1, TotalBytesDownloaded = entry.Size });

            }

            entries++;

        }

        if (entries > 0)
        {

            PostProcess(siteCollectionTracking);
                    
        }

    }
    catch /*no throw - let the caller believe we have it under control*/
    {
        //TODO: Write this to the ULS Log, ref: http://todd-carter.com/post/2010/12/17/Yuletide-ULS
    }
            
}

 

Once processing has completed on each DownloadUsageEntry, and assuming we have entries, its time to call into the PostProcess method passing in the dictionary we have taken such care to create. Its within PostProcess where each Site Collection with a download is updated with new download information. Since this code is running within the OWSTimer process, which is running under the SharePoint Farm account, we should have enough permissions to each of the Site Collections – if we don’t you have bigger problems than updating download information.

So one small gotcha I want to call out; there is a potential race condition in the code below. Since each server within the SharePoint farm executes the Usage Import Timer Job independently to import data stored from within its own *.usage files its possible this code will be executed simultaneously on two different machines which both may have download information to update for the same Site Collection and its possible we might miss one of those updates by overwriting the update since we are not updating within a transaction and/or taking a farm wide lock. No worries however, its not the end of the world and I just wanted to call it out here to make you aware of the potential for this occurring.

private static void PostProcess(Dictionary<Guid, DownloadInformation> siteCollectionTracking) 
{
                
    foreach (Guid siteId in siteCollectionTracking.Keys)
    {

        using (SPSite site = new SPSite(siteId))
        {

            using (SPWeb web = site.RootWeb)
            {
                        
                bool isNew = !web.AllProperties.ContainsKey(Constants.SiteDownloadUsageKey);
                string serializedSiteDownloadInformation;
                DownloadInformation newDownloadInformation = siteCollectionTracking[siteId];
                DownloadInformation siteDownloadInformation = null;
                        

                if (!isNew)
                {
                    serializedSiteDownloadInformation = web.AllProperties[Constants.SiteDownloadUsageKey] as string;

                    if (!String.IsNullOrEmpty(serializedSiteDownloadInformation))
                    {
                        siteDownloadInformation = SerializationHelper.Deserialize<DownloadInformation>(serializedSiteDownloadInformation);

                        if (siteDownloadInformation != null)
                        {
                            siteDownloadInformation.TotalDownloads += newDownloadInformation.TotalDownloads;
                            siteDownloadInformation.TotalBytesDownloaded += newDownloadInformation.TotalBytesDownloaded;
                        }
                    }

                }
                                                
                if (siteDownloadInformation == null)
                {
                    siteDownloadInformation = newDownloadInformation;
                }

                serializedSiteDownloadInformation = SerializationHelper.Serialize<DownloadInformation>(siteDownloadInformation);

                if (isNew)
                {
                    web.AllProperties.Add(Constants.SiteDownloadUsageKey, serializedSiteDownloadInformation);
                }
                else
                {
                    web.AllProperties[Constants.SiteDownloadUsageKey] = serializedSiteDownloadInformation;
                }
                        
                web.Update();
            }

        }

    }
                                    
}

Registration

To register a Usage Receiver with a Usage Provider we need to first obtain a local reference to the Usage Provider of choice. Because each provider derives from the SPUsageProvider there is no need to cast the local reference to a specific provider type but you do want to pass the correct type into the generic GetLocal call. There is a Boolean named EnableReceivers which, when set to true, allows all registered Usage Receivers to be called. This means if you add your Usage Receiver to a Usage Provider which has this value set to false and has additional Usage Receivers configured, once you set the EnableReceivers to true so your Usage Receiver is called others may be called too. This is an unfortunate design but I don’t suspect too many folks to be impacted by this Usage Provider scoped setting.

Within a Farm scoped Feature we will call the public static methods below which are members of the DownloadUsageReceiver class.

public static void Register()
{
  var usage = SPUsageDefinition.GetLocal<DownloadUsageProvider>();
            
  if (usage != null)
  {
                
    //register the Receivers
    if (!usage.EnableReceivers)
    {
      usage.EnableReceivers = true;
    }
    usage.Receivers.Remove(typeof(DownloadUsageReceiver));
    usage.Receivers.Add(typeof(DownloadUsageReceiver));
                
    usage.Update();
  }

}

public static void Unregister()
{
  var usage = SPUsageDefinition.GetLocal<DownloadUsageProvider>();

  if (usage != null)
  {
    usage.Receivers.Remove(typeof(DownloadUsageReceiver));

    usage.Update();
  }
}

Conclusion

Think of Usage Receivers as Event Receivers for Usage Providers. As I stated at the very start of this article I consider Usage Receivers as my favorite extensibility feature of the Usage and Health service as I can think of a ton of potential and uses within my future SharePoint 2010 solutions.

I really hoped you found this series helpful but what I really hope is that I sparked some interest and you can use this in a real world SharePoint 2010 solution. If you develop such a solution I would love to hear about it – ping me via email, through my blog, or post a comment.

Happy SharePoint coding friends!

Post to Twitter Post to Facebook Post to LinkedIn Post to Delicious Post to Digg

3 thoughts on “Extending The SharePoint 2010 Health & Usage – Part 4: Writing a Custom Usage Receiver”

  1. I tried to get it in my site and i couldnt get DownloadUsage view. How can we get that view. I tried to run the stored procedure to see the content. How will we create downloadusage view?

  2. This article is perfect finish of the series of articles regarding Extending the SharePoint 2010 Health & Usage for the users and allows developer to follow more accurate development process.

Leave a Reply

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