Please visit the CodePlex project site for the latest releases and source code.
Silverlight Edition
Contents
Introduction
So you've deployed your brand new Silverlight application to test. But wait, there's a problem! Your testers tell you that it breaks when they perform a particular action. They send you a multitude of screenshots, yet to no avail. Without more information, the issue appears irresolvable. With deadlines looming and no hope in sight, desperation sets in. You raise your fists in the air and exclaim, "If only I knew what was happening client side!".
Enter Clog.
OK, the previous hypothetical scenario is melodramatic, but it highlights the need for an integrated client side logging solution. Thus, I decided to create Clog. Clog is a log provider system that allows you to harness your existing logging system to log client side messages to your server. It is fully customizable, can serialize and log all Exception
types, and allows filtering of messages both on the client and server sides using custom or inbuilt filters, which so far consists of an IP Address Range Filter and a Role Membership filter. In this release, Clog includes an extendible log provider system, a log4net log strategy, a Microsoft Enterprise Library log strategy, and a Silverlight log viewer. Clog now supports multiple simultaneous log strategies!
Figure: Consuming Clog from a web client application.
This article will discuss how Clog works, how to set up Clog on both the client and the server, configuration of the Silverlight Log Viewer control, and also some more advanced topics such as the Silverlight security model.
Although this article's client side focus is primarily on a Silverlight implementation, Clog is capable of providing logging services to any .NET or web service consumer.
Background
A solid server side logging system is the mainstay of most web based applications. With the advent of WPF, Silverlight, and a client side CLR, it is my view that we will see a shift in focus away from the traditional and primarily server based logging scenario, to cater to a more client centric environment.
While Visual Studio allows us to readily debug Silverlight, Windows Forms, and WPF applications, without the debugger or some means of tracing client side events, we can find ourselves left in the dark. We have no built-in mechanism for logging to, e.g., an event log on a client machine, and we lack an immediate feedback mechanism that could allow us to know if our client side .NET applications are behaving correctly. Clog bridges this client-server divide. We are now able to selectively capture logging events that originate from both client and server side applications.
I recall, some years ago, my first experiences while hand coding form validation JavaScript, and trying to provide myself with client side feedback. It was, and still is, common practice to use message boxes for displaying information while scripting. It's a fairly slipshod and haphazard approach, and also potentially embarrassing if you should forget to comment out the code when you're through! Today, there are one or two AJAX JavaScript client to server logging projects out there. And, while they may work well with loosely typed JavaScript, they address a different need.
There are numerous logging libraries for .NET, and many of us have come to know and rely on a particular system over time; sometimes customizing it to suit our own requirements. Clog allows us to keep our existing system, by wrapping it; allowing us to perform both client and server logging in the same manner.
Figure: Local and remote clients consuming Clog.
Clog System Overview
Clog's core component is DanielVaughan.Logging.dll. It provides for most of the server side functionality. Silverlight logging functionality is located in DanielVaughan.Logging.Silverlight.dll, and auxiliary to this is the optional component DanielVaughan.Logging.Silverlight.UI.dll, which contains the LogViewer
Silverlight control. The following diagram provides a conceptual overview of some of the principle types and their interdependencies.
Figure: Clog component diagram.
Using Clog
To enable Clog for client side logging, we complete a two stage process. First, we configure the server based project to use Clog. Then, we configure our client side project to use Clog.
Server Side Configuration
To enable Clog on the server side, add a reference to the DanielVaughan.Logging assembly.
<section name="Clog"
type="DanielVaughan.Logging.Configuration.ClientLoggingConfigurationSectionHandler,
DanielVaughan.Logging"/>
Next, create the Clog
config section, as in the following excerpt:
<!---->
<Clog xmlns="http://danielvaughan.orpius.com/Clog/2/0/"
InternalLogLevel="All" SkipFrameCount="4">
<LogStrategy Name="Simple"
Type="ExampleWebsite.SimpleLogStrategy, ExampleWebsite">
<Filter Name="IPAddressRange"
Type="DanielVaughan.Logging.Filters.IPAddressRangeFilter,
DanielVaughan.Logging"
Begin="127.0.0.0" End="127.0.0.10"/>
</LogStrategy>
<LogStrategy Name="Log4Net"
Type="DanielVaughan.Logging.LogStrategies.Log4NetStrategy,
DanielVaughan.Logging.Log4NetLogStrategy">
<Filter Name="IPAddressRange"
Type="DanielVaughan.Logging.Filters.IPAddressRangeFilter,
DanielVaughan.Logging"
Begin="127.0.0.0" End="127.0.0.10"/>
<!---->
<!---->
</LogStrategy>
</Clog>
Create a new file in your web project called ClogService.svc, open it, and paste the following content:
<%@ ServiceHost Language="C#" Debug="true" Service="DanielVaughan.Logging.ClogService" %>
The service code is actually located in the DanielVaughan.Logging.dll assembly. It is up to you to define your preferred logging method. For this demonstration, we are using log4net. While configuring log4net is outside the scope of this article, you can view the example website download to see how it's done. Briefly, it requires adding an assembly reference to log4net.dll, adding a config section in the web.config, and then initialising log4net within your website. I do this by performing an arbitrary logging request when the application starts.
N.B.: If you don't initialise log4net from your website before it is used in another assembly, it won't be configured properly.
You may notice the new SkipFrameCount
attribute in the configuration. This is a new feature, as of version 1.8, which allows you to wrap Clog in your own adapter, while still retaining the ability to correctly resolve the origin of a log write call.
Clog for Silverlight Configuration
To use Clog in your Silverlight project, add a reference to the DanielVaughan.Logging.Silverlight assembly. If you also wish to use the Silverlight Log Viewer, then add a reference to the DanielVaughan.Logging.Silverlight.UI assembly as well.
Writing to the Log from Silverlight
To use Clog on the client side, within a Silverlight application, we add a reference to the DanielVaughan.Logging.Silverlight assembly, and we use the LogManager
to provide us with an ILog
by calling the GetLog
method like so:
static readonly ILog log = LogManager.GetLog(typeof(Page));
The typeof(Page)
argument is used to determine the name of the Log in conjunction with the URL of the page itself. This gives us a fine grained control over filtering of log requests. That is, we can control logging requests not only based on a particular Silverlight custom control or page, but also on where it has been deployed. To facilitate debugging from localhost where there is a dynamic port specified in the URL, we don't include the port number in the log name.
Figure: Client log writing process.
The client-side Silverlight Log dispatches log entries to the server asynchronously, as the following excerpt shows:
void WriteLogEntryAux(LogLevel logLevel, string message,
Exception exception, IDictionary<string, object> properties)
{
ExceptionMemento memento = null;
if (exception != null)
{
memento = CreateMemento(exception);
}
var logEntryData = new LogEntryData
{
LogLevel = logLevel,
Message = message,
ExceptionMemento = memento,
CodeLocation = GetLocation(),
LogName = Name,
ThreadName = Thread.CurrentThread.Name,
ManagedThreadId = Thread.CurrentThread.ManagedThreadId,
Url = pageUri.ToString(),
OccuredAt = DateTime.Now,
Properties = properties != null ?
new Dictionary<string, object>(properties) : null
};
OnLogEntrySendAttempt(new LogEventArgs(logEntryData));
var clientInfo = new ClientInfo
{
LogName = logEntryData.LogName,
MachineName = logEntryData.MachineName,
Url = logEntryData.Url,
UserName = logEntryData.UserName
};
if (clientConfigurationData == null ||
clientConfigurationData.RetrievedOn.AddSeconds(
clientConfigurationData.ExpiresInSeconds) < DateTime.Now)
{
lock (loggingConfigLock)
{
if (clientConfigurationData == null ||
clientConfigurationData.RetrievedOn.AddSeconds(
clientConfigurationData.ExpiresInSeconds) < DateTime.Now)
{
var clogService = GetClogService();
clogService.BeginGetConfiguration(clientInfo,
asyncResult =>
{
ClientConfigurationData result;
try
{
result = clogService.EndGetConfiguration(asyncResult);
}
catch (Exception ex)
{
OnInternalMessage(
new InternalMessageEventArgs(
"Unable to retrieve configuration from server.", ex));
return;
}
try
{
clientConfigurationData = result;
clientConfigurationData.RetrievedOn = DateTime.Now;
WriteLogEntryAux(logEntryData, clientConfigurationData);
}
catch (Exception ex)
{
OnInternalMessage(
new InternalMessageEventArgs(
"Problem writing log entry after successfully retrieving configuration.",
ex));
throw;
}
}, null);
return;
}
}
}
WriteLogEntryAux(logEntryData, clientConfigurationData);
}
You may notice that we are using a class called ChannelManagerSingleton
in the previous excerpt. Some information about it can be found in another of my articles. It is essentially used to cache service channels.
Silverlight Log Viewer
Overview
The Log Viewer is a Silverlight control that can be placed on a Canvas
to automatically monitor the LogManager
. When an ILog
instance receives a request to write a log message, the Log Viewer is able to display the log message. The following screen capture shows the Log Viewer receiving an outgoing log entry, while the Log4Net Viewer shows the result of the log entry after it has been relayed to log4net.
Figure: Clog Silverlight Log Viewer with Log4Net Viewer.
Using the Log Viewer
To include the Log Viewer in your Silverlight application, add a reference to the DanielVaughan.Logging.Silverlight.UI assembly, and add the following namespace definition to the root canvas on a page:
xmlns:UI="clr-namespace:DanielVaughan.Logging.Silverlight.UI;
assembly=DanielVaughan.Logging.Silverlight.UI"
Then, place the LogViewer
XAML element somewhere on the page.
<UI:LogViewer x:Name="LogViewer" />
The Log Viewer has been rebuilt in the version for Silverlight 2 RTW, and is rather basic to say the least.
Inside the Log Viewer
When a Silverlight application requests the writing of a log entry, two events may be raised by the active ILog
instance. The first event, WriteRequested
, happens unconditionally, and if the "Log Viewer" is in OfflineMode
, a log entry will be displayed immediately. The second event, LogEntrySent
, is raised if the log entry is sent to the server. In this case, if the "Log Viewer" is not in OfflineMode
, then the log entry is displayed. Sending of the log entry depends on the ILog
's ClientConfigurationData
LogLevel
and Enabled
properties, and the requested log level.
Figure: Log Viewer processing a log entry flowchart.
Silverlight Security Model
Silverlight does not use Code Access Security (CAS). Silverlight uses the transparency model introduced in .NET 2.0. In this model there are three levels: Transparent, Safe Critical, and Critical. In the Silverlight CLR, all code is "Transparent" by default, and, therefore, so is the user code. This is the opposite of the desktop CLR, which is "Critical" by default (.NET Security Blog). Any method decorated with a SecurityCritical
attribute can't be called directly by user code because transparent code is not allowed to call "Critical" code. If "Transparent" code attempts to call "Critical" code, a MethodAccessException
ensues.
Figure: Transparent code cannot call Critical code directly.
The Silverlight mscorlib contains "many" methods decorated with the SecurityCritical
attribute. One such method is the System.Diagnostics.StackTrace
constructor, and, unfortunately for us, this means we can't get a stack trace; preventing us from obtaining and passing on the origin of calls to the Log. It makes sense to hide stack trace information from user code, as it may reveal sensitive information. It would, however, be nice to have a "Safe Critical" method to retrieve a stack trace consisting of just user code. But, I'm not holding out for that one.
To take a look at what methods are available to our user code, fire up Reflector, and replace the Framework mscorlib assembly in Reflector with the Silverlight version. As we can see, much of this assembly is a no-go-zone.
Figure: StackTrace class disassembled in Reflector.
Extending Clog
Clog Provider Model
"All your log are belong to Clog."
-D. Vaughan. (See derivation)
Clog uses ILogStrategy
instances to send log entries to third-party logging systems. Included with Clog are two ILogStrategy
implementations. The first is a really simple tracing strategy that serves as a basic example; the second, a log4net strategy. I hope to include more in a later release. If you happen to write one for a particular third party logging system, I'd love to include it in the next release (with credit, of course).
An ILogStrategyProvider
is tasked with instantiating the ILogStrategy
and exposing it through its LogStrategy
property. Internal to the DanielVaughan.Logging
module, there is a default implementation of an ILogStrategy
that should be sufficient in most cases.
Figure: ILogStrategy and ILogStrategyProvider class diagram.
Integrating Clog with your Existing Third Party Logging System
To integrate Clog with an existing logging system, implement the ILogStrategy
interface, and specify the type in the provider configuration using the LogStrategy
attribute, as in the following example:
<LogStrategy Name="CustomStrategy" Type="YourAssembly.Strategy, YourAssembly">
-->
</LogStrategy>
The ILogStrategy
interface has three members. For client side logging functionality, implement the void Write(IClientLogEntry logEntry);
and LogLevel GetLogLevel(string logName);
methods. If you plan to only use Clog for client side logging, then you can forget about the void Write(IServerLogEntry logEntry);
overload (of course, you will still need a default empty implementation). If, however, you wish to use Clog as a wrapper for your existing logging system, then provide an appropriate implementation for the IServerLogEntry
overload as well.
The Log Strategy determines how a log entry is written to a log. It is here that we connect our existing logging system, such as log4net, to Clog. When a log write is requested, the current Log Strategy must take the information present in the IServerLogEntry
or IClientLogEntry
and construct a call to the existing logging system. The LogLevel GetLogLevel(string logName);
method is used to determine the threshold at which log entries are written, and it also allows us to inhibit logging on the client side if the level is higher than the requested logging level.
This release of Clog comes with a simple tracing log strategy, a log4net strategy (Log4NetStrategy
), and a Microsoft Enterprise Library Logging Application Block strategy (EnterpriseLibraryStrategy
). The following excerpt shows how the class writes a log message to log4net using a specified IClientLogEntry
argument:
public void Write(IClientLogEntry logEntry)
{
ILog log = defaultLog;
if (logEntry.LogName != null)
{
log = LogManager.GetLogger(logEntry.LogName);
}
LoggingEventData data = new LoggingEventData();
if (logEntry.ExceptionMemento != null)
{
data.ExceptionString = logEntry.ExceptionMemento.ToString();
}
data.Level = GetLog4NetLevel(logEntry.LogLevel);
ICodeLocation location = logEntry.CodeLocation;
if (location != null)
{
data.LocationInfo = new LocationInfo(location.ClassName,
location.MethodName, location.FileName,
location.LineNumber.ToString());
}
data.LoggerName = logEntry.LogName;
data.Message = string.Format("{0}\nMachineName:{1}",
logEntry.Message,
logEntry.MachineName);
data.ThreadName = logEntry.ThreadName;
data.TimeStamp = logEntry.OccuredAt;
data.UserName = logEntry.UserName;
if (logEntry.Properties != null && logEntry.Properties.Count > 0)
{
var properties = new log4net.Util.PropertiesDictionary();
foreach (var prop in logEntry.Properties)
{
properties[prop.Key] = prop.Value;
}
data.Properties = properties;
}
LoggingEvent loggingEvent = new LoggingEvent(data);
log.Logger.Log(loggingEvent);
}
Filters
Clog uses server side filters to determine what log entries to discard before they are sent to the active Log Strategies. Filters are evaluated when retrieving ClientConfigurationData
, and on receipt of a log write request. I have included a number of filters with this release. One is an IP address range filter, which will restrict logging to an IP within a range that is defined in the provider configuration. Another is a Role Membership filter, which restricts logging to those authenticated users who have a role specified in the configuration. Update: since publishing, I have detailed various new filters in this article.
Figure: Filter class diagram.
The current ILogProvider
evaluates each IFilter
by calling the IsValid
method, as the following excerpt from the IPAddressFilter
class demonstrates.
class IPAddressRangeFilter : FilterBase
{
uint begin;
uint end;
public IPAddressRangeFilter()
{
Init += IPAddressRangeFilter_Init;
}
void IPAddressRangeFilter_Init(object sender, FilterInitEventArgs e)
{
ArgumentValidator.AssertNotNull(e, "e");
ArgumentValidator.AssertNotNull(e.ConfigurationElement,
"e.ConfigurationElement");
begin = end = 0;
var beginAttribute = e.ConfigurationElement.Attributes["Begin"];
if (beginAttribute == null)
{
throw new ClientLoggingException("Begin ip address " +
"attribute does not exists.");
}
try
{
begin = ToUInt(IPAddress.Parse(beginAttribute.Value));
}
catch (Exception ex)
{
throw new ClientLoggingException("Begin ip address " +
"is not a valid IP address.", ex);
}
var endAttribute = e.ConfigurationElement.Attributes["End"];
if (endAttribute == null)
{
throw new ClientLoggingException("End ip address attribute does not exists.");
}
try
{
end = ToUInt(IPAddress.Parse(endAttribute.Value));
}
catch (Exception ex)
{
throw new ClientLoggingException("End ip address " +
"is not a valid IP address.", ex);
}
if (Action == FilterAction.Default)
{
Action = FilterAction.Allow;
}
if (Action != FilterAction.Allow && Action != FilterAction.Deny)
{
throw new ConfigurationErrorsException(InvalidActionMessage);
}
}
public override bool IsValid(LogEntryOrigin origin, IClientInfo info)
{
string addressValue = info.IPAddress;
bool withinRange = string.IsNullOrEmpty(addressValue) ||
IsWithinRange(begin, addressValue, end);
switch (Action)
{
case FilterAction.Allow:
return withinRange;
case FilterAction.Deny:
return !withinRange;
}
throw new ConfigurationErrorsException(InvalidActionMessage);
}
static bool IsWithinRange(uint beginAddress, string addressValue, uint endAddress)
{
IPAddress address;
if (!IPAddress.TryParse(addressValue, out address))
{
return false;
}
uint ip = ToUInt(address);
return ip >= beginAddress && ip <= endAddress;
}
static uint ToUInt(IPAddress ipAddress)
{
byte[] bytes = ipAddress.GetAddressBytes();
uint result = (uint)bytes[0] << 24;
result += (uint)bytes[1] << 16;
result += (uint)bytes[2] << 8;
result += bytes[3];
return result;
}
}
Logging Exceptions the Clog Way
When a request to log an Exception
occurs client side, an ExceptionMemento
is used to encapsulate the exception information so that it can be correctly serialized and sent over the wire. You may be aware that an Exception
is not immediately serializable without using a binary formatter for serialization, and this is due to the IDictionary _data
field. To sidestep any serialization issues, and relieve the Clog consumer from having to worry about the serializability of his or her logged exceptions, we gather what information we can from the exception into an ExceptionMemento
instance. This is then sent over HTTP to our Clog web service, where it is relayed to the active Log Strategy.
Log Entries
ILogEntry
instances encapsulate the log entry information that passes through Clog, via a remote client application or local consumer, and on to the end point, the active ILogStrategy
. External consumers of Clog use the LogEntryData
data type, which forms the base type for all concrete ILogEntry
instances, and omits various internal properties. When a LogEntry
arrives at the Clog Web Service, a ClientLogEntry
is instantiated and decorated with the IP of the incoming request. This is then sent on to the static Log
class. The following class diagram shows the ILogEntry
interface and its inheritors. Concrete implementations of the various ILogEntry
interfaces are internal to the DanielVaughan.Logging project.
Figure: Log Entry class diagram.
IServerLogEntry
instances represent server side log entries, and are used when requests to write to the log originate within the same application, and most likely within the same appdomain, as the DanielVaughan.Logging.dll component.
IClientLogEntry
instances, on the other hand, represent client side log entries. These are used when requests to write to the log originate from a remote client, such as a Silverlight Clog Log.
Future Enhancements
- Use custom exceptions to expose protected data to the
ExceptionMemento
- Add filter by log etc., to Log Viewer
- Provide more Unit Tests for core logging functionality
Conclusion
This article discussed the implementation of Clog; a client server logging provider system. It showed how to set it up, including configuration of the Silverlight Log Viewer control. The article also touched on some more advanced topics such as the Silverlight security model. You can find the next article in the series here. Although Clog is still in its infancy, I believe it shows a lot of promise for becoming quite a useful tool.
I hope you find this project useful. If so, then you may like to rate it and/or leave feedback below.
References
History
- November 2007
- January 2008
- Added a
config
attribute to disable the use of ASP.NET Membership by Clog.
- Integrated the Silverlight and WPF Editions into the same download.
- Improved multithreaded logging capability to prevent exceptions due to logging calls being made from non-STA threads.
- April 2009
- Updated article to reflect changes in Clog version 1.8. Please see the Clog site for a list of changes.