Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Clog: Client Logging, Silverlight Edition

0.00/5 (No votes)
16 Apr 2009 2  
A customizable log provider system that allows you to harness your existing logging system to log client side messages to your server. Includes a Silverlight interface and Log Viewer.

Please visit the CodePlex project site for the latest releases and source code.

Clog - All your log are belong to Clog.

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!

Clog Overview

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.

Consuming Clog

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.

Clog component diagram.

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:

<!-- InternalLogLevel is used to monitor log messages originating from Clog, 
    and which are written to the console. Valid values are (from less to most restrictive): 
    All, Debug, Info, Warn, Error, Fatal, None. 
    Xmlns is specified in order to gain intellisense within the Visual Studio config editor. 
    Place the Clog schema located in the project\Schemas directory 
    into C:\Program Files\Microsoft Visual Studio 9.0\Xml\Schemas directory. 
    SkipFrameCount is used to specify the number of frames to skip when resolving 
    the calling method of a write log call. Defaults to 4 if absent. -->
    <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"/>
            <!-- Uncomment to prevent access to those users that 
                 do now have membership of the specified roles. -->
            <!--
            <Filter Name="RoleMembership" 
                type="DanielVaughan.Logging.Filters.RoleMembershipFilter, 
                      DanielVaughan.Logging"
                Roles="Developer, Administrator" />
            -->
        </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.

Writing to the log flowchart

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.", 
                  /* TODO: Make localizable resource. */
                  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.

Browser and Log4Net Viewer

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.

Log Viewer display process flowchart

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.

Transparent code cannot call Critical code directly.

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.

StrackTrace class disassembled in Reflector

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.

Log Strategy provider class diagram

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">
  <!-- Add filters here. -->
</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);
    }

    /* Create a Log4Net event data instance, 
     * and populate it with our log entry information. */
    LoggingEventData data = new LoggingEventData();
    if (logEntry.ExceptionMemento != null)
    {    /* Use the exception memento to write 
         * the message and stack trace etc. */
        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;

    /* Copy custom properties into log4net Properties. */
    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.

Filters class diagram

Figure: Filter class diagram.

The current ILogProvider evaluates each IFilter by calling the IsValid method, as the following excerpt from the IPAddressFilter class demonstrates.

/// <summary>
/// Restricts logging based on an IP address range.
/// </summary>
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");

        /* Reset state. */
        begin = end = 0;

        var beginAttribute = e.ConfigurationElement.Attributes["Begin"];
        if (beginAttribute == null)
        {   /* TODO: Make localizable resource. */
            throw new ClientLoggingException("Begin ip address " + 
                      "attribute does not exists."); 
        }

        try
        {
            begin = ToUInt(IPAddress.Parse(beginAttribute.Value));
        }
        catch (Exception ex)
        {   /* TODO: Make localizable resource. */
            throw new ClientLoggingException("Begin ip address " + 
                      "is not a valid IP address.", ex); 
        }

        var endAttribute = e.ConfigurationElement.Attributes["End"];
        if (endAttribute == null)
        {   /* TODO: Make localizable resource. */
            throw new ClientLoggingException("End ip address attribute does not exists."); 
        }

        try
        {
            end = ToUInt(IPAddress.Parse(endAttribute.Value));
        }
        catch (Exception ex)
        {   /* TODO: Make localizable resource. */
            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);
    }

    /// <summary>
    /// Determines whether the specified addressValue IP address 
    /// falls within the range specified by beginAddress and endAddress.
    /// </summary>
    /// <param name="beginAddress">The begin address.</param>
    /// <param name="addressValue">The address value to test.</param>
    /// <param name="endAddress">The end address.</param>
    /// <returns>
    ///   <c>true</c> if the addressValue is within
    ///   the specified range; otherwise, <c>false</c>.
    /// </returns>
    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;
    }

    /// <summary>
    /// Converts and <see cref="IPAddress"/> to an unsigned int.
    /// </summary>
    /// <param name="ipAddress">The ip address to convert.</param>
    /// <returns>A <code>uint</code> representing 
    /// the specified ipAddress.</returns>
    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.

Log Entry class diagram

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
    • First release.
  • 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here