Blog Stats
  • Posts - 24
  • Articles - 0
  • Comments - 18
  • Trackbacks - 0

 

SharePoint logging to a list

I recently worked in an environment with several servers.

Locating the correct SharePoint log file for error messages, or development trace calls, is cumbersome.

And once the solution hit the cloud, it got even worse, as we had no access to the log files at all.

Obviously we are not the only ones with this problem, and the current trend seems to be to log to a list.

This had become an off-hour project, so rather than do the sensible thing and find a ready-made solution, I decided to do it the hard way.

So! Fire up Visual Studio, create yet another empty SharePoint solution, and start to think of some requirements.

  • Easy on/off
    I want to be able to turn list-logging on and off.
  • Easy logging
    For me, this means being able to use string.Format.
  • Easy filtering
    Let's have the possibility to add some filtering columns; category and severity, where severity can be "verbose", "warning" or "error".

Easy on/off

Well, that's easy. Create a new web feature. Add an event receiver, and create the list on activation of the feature. Tear the list down on de-activation.

I chose not to create a new content type; I did not feel that it would give me anything extra.

I based the list on the generic list - I think a better choice would have been the announcement type.

Approximately:

 public void CreateLog(SPWeb web)

        {

            var list = web.Lists.TryGetList(LogListName);

            if (list == null)

            {

                var listGuid = web.Lists.Add(LogListName, "Logging for the masses", SPListTemplateType.GenericList);

                list = web.Lists[listGuid];

                list.Title = LogListTitle;

                list.Update();

                list.Fields.Add(Category, SPFieldType.Text, false);

                var stringColl = new StringCollection();

                stringColl.AddRange(new[]{Error, Information, Verbose});

                list.Fields.Add(Severity, SPFieldType.Choice, true, false, stringColl);

                ModifyDefaultView(list);

            }

        }

Should be self explanatory, but: only create the list if it does not already exist (d'oh). Best practice: create it with a Url-friendly name, and, if necessary, give it a better title. ...because otherwise you'll have to look for a list with a name like "Simple_x0020_Log". I've added a couple of fields; a field for category, and a 'severity'. Both to make it easier to find relevant log messages.

Notice that I don't have to call list.Update() after adding the fields - this would cause a nasty error (something along the lines of "List locked by another user").

The function for deleting the log is exactly as onerous as you'd expect:

        public void DeleteLog(SPWeb web)

        {

            var list = web.Lists.TryGetList(LogListTitle);

            if (list != null)

            {

                list.Delete();

            }

        }

So! "All" that remains is to log. Also known as adding items to a list.

Lots of different methods with different signatures end up calling the same function.

For example, LogVerbose(web, message) calls LogVerbose(web, null, message) which again calls another method which calls:

private static void Log(SPWeb web, string category, string severity, string textformat, params object[] texts)

        {

            if (web != null)

            {

                var list = web.Lists.TryGetList(LogListTitle);

                if (list != null)

                {

                    var item = list.AddItem(); // NOTE! NOT list.Items.Add… just don't, mkay?

                    var text = string.Format(textformat, texts);

                    if (text.Length > 255) // because the title field only holds so many chars. Sigh.

                        text = text.Substring(0, 254);

                    item[SPBuiltInFieldId.Title] = text;

                    item[Degree] = severity;

                    item[Category] = category;

                    item.Update();

                }

            }

// omitted: Also log to SharePoint log.

        }

By adding a params parameter I can call it as if I was doing a Console.WriteLine: LogVerbose(web, "demo", "{0} {1}{2}", "hello", "world", '!'); Ok, that was a silly example, a better one might be: LogError(web, LogCategory, "Exception caught when updating {0}. exception: {1}", listItem.Title, ex);

For performance reasons I use list.AddItem rather than list.Items.Add.

For completeness' sake, let us include the "ModifyDefaultView" function that I deliberately skipped earlier.

        private void ModifyDefaultView(SPList list)

        {

            // Add fields to default view

            var defaultView = list.DefaultView;

            var exists = defaultView.ViewFields.Cast<string>().Any(field => String.CompareOrdinal(field, Severity) == 0);

 

            if (!exists)

            {

                var field = list.Fields.GetFieldByInternalName(Severity);

                if (field != null)

                    defaultView.ViewFields.Add(field);

                field = list.Fields.GetFieldByInternalName(Category);

                if (field != null)

                    defaultView.ViewFields.Add(field);

                defaultView.Update();

 

                var sortDoc = new XmlDocument();

                sortDoc.LoadXml(string.Format("<Query>{0}</Query>", defaultView.Query));

                var orderBy = (XmlElement) sortDoc.SelectSingleNode("//OrderBy");

                if (orderBy != null && sortDoc.DocumentElement != null)

                    sortDoc.DocumentElement.RemoveChild(orderBy);

                orderBy = sortDoc.CreateElement("OrderBy");

                sortDoc.DocumentElement.AppendChild(orderBy);

                field = list.Fields[SPBuiltInFieldId.Modified];

                var fieldRef = sortDoc.CreateElement("FieldRef");

                fieldRef.SetAttribute("Name", field.InternalName);

                fieldRef.SetAttribute("Ascending", "FALSE");

                orderBy.AppendChild(fieldRef);

 

                fieldRef = sortDoc.CreateElement("FieldRef");

                field = list.Fields[SPBuiltInFieldId.ID];

                fieldRef.SetAttribute("Name", field.InternalName);

                fieldRef.SetAttribute("Ascending", "FALSE");

                orderBy.AppendChild(fieldRef);

                defaultView.Query = sortDoc.DocumentElement.InnerXml;

                //defaultView.Query = "<OrderBy><FieldRef Name='Modified' Ascending='FALSE' /><FieldRef Name='ID' Ascending='FALSE' /></OrderBy>";

                defaultView.Update();

            }

        }

First two lines are easy - see if the default view includes the "Severity" column. If it does - quit; our job here is done.

Adding "severity" and "Category" to the view is not exactly rocket science. But then? Then we build the sort order query. Through XML. The lines are numerous, but boring. All to achieve the CAML query which is commented out. The major benefit of using the dom to build XML, is that you may get compile time errors for spelling mistakes. I say 'may', because although the compiler will not let you forget to close a tag, it will cheerfully let you spell "Name" as "Naem".

Whichever you prefer, at the end of the day the view will sort by modified date and ID, both descending. I added the ID as there may be several items with the same time stamp.

So! Simple logging to a list, with sensible a view, and with normal functionality for creating your own filterings.

I should probably have added some more views in code, ready filtered for "only errors", "errors and warnings" etc.

And it would be nice to block verbose logging completely, but I'm not happy with the alternatives. (yetanotherfeature or an admin page seem like overkill - perhaps just removing it as one of the choices, and not log if it isn't there?)

Before you comment - yes, try-catches have been removed for clarity. There is nothing worse than having a logging function that breaks your site!


Feedback

No comments posted yet.


Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 

 

 

Copyright © Norgean