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/offI want to be able to turn list-logging on and off.Easy loggingFor me, this means being able to use string.Format.Easy filteringLet'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!