6

I'm looking for some advice on logging. I've written a wrapper called Logger which internally uses Microsoft Enterprise Library 5.0. Currently it enables us to log in this way:

Logger.Error(LogCategory.Server, "Some message with some state {0}", state);

The problem I face is that every log in the EventViewer seems to be unrelated even though some of them are somehow related, for example they all come from processing a request from a particular client.

Let me elaborate on the problem. Suppose I'm working on a service which needs to handle requests from multiple clients at the same time, each passing different set of parameters to the service methods. Few of the parameters can be used to identify the requests, such as which client is making what kind of request with what uniquely identifiable parameters, etc. Say these parameters are (call the contextual information):

  • ServerProfileId
  • WebProfileId
  • RequestId
  • SessionInfo

Now the service started processing the requests, doing one thing after another (like a workflow). In the process I'm logging local1 messages such as "file not found" or "entry not found in DB", but I don't want to manually log the above information (the contextual information) with every log, rather I want the logger to log them automatically every time I log local message:

Logger.Error(LogCategory.Server, "requested file not found");

And I want the above call to log the contextual information along with the message "requested file not found" so I can relate the message with its context.

The question is, how should I design such a logger wrapper which logs contexts automatically? I want it to be flexible enough, so that I may add more specific contextual information along the way the service processes the request, as all the important information may not be available in the beginning of the request!

I also want to make it configurable, so I can log the local messages without logging the context information, as they're not needed when everything works great. :-)


1. By local message I mean the messages which is more specific, local in nature. By contrast, I would say, the contextual informations are the global messages, as they make sense for the entire flow of processing a request.

Nawaz
  • 353,942
  • 115
  • 666
  • 851
  • Check out http://www.sharpcrafters.com/blog/post/Configuring-PostSharp-Diagnostics-Toolkits.aspx – Petko Petkov Jan 16 '13 at 13:14
  • 1
    @PetkoPetkov: I'm not using PostSharp. I'm using Microsoft Enterprise Library, as mentioned in the question itself. – Nawaz Jan 16 '13 at 14:09

2 Answers2

4

Here is one approach that uses Enterprise Library that is fairly easy to setup. You can use Activity Tracing for storing global context and Extended Properties for storing local context.

For the sake of an example I'll use a service locator without any wrapper class to demonstrate the approach.

var traceManager = EnterpriseLibraryContainer.Current.GetInstance<TraceManager>();

using (var tracer1 = traceManager.StartTrace("MyRequestId=" + GetRequestId().ToString()))
using (var tracer2 = traceManager.StartTrace("ClientID=" + clientId))
{
    DoSomething();
}

static void DoSomething()
{
    var logWriter = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
    logWriter.Write("doing something", "General");

    DoSomethingElse("ABC.txt");
}

static void DoSomethingElse(string fileName)
{
    var logWriter = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();

    // Oops need to log
    LogEntry logEntry = new LogEntry()
    {
        Categories = new string[] { "General" },
        Message = "requested file not found",
        ExtendedProperties = new Dictionary<string, object>() { { "filename", fileName } }
    };

    logWriter.Write(logEntry);
}

The configuration would look like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="loggingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.LoggingSettings, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" />
    </configSections>
    <loggingConfiguration name="" tracingEnabled="true" defaultCategory="General"
        logWarningsWhenNoCategoriesMatch="false">
        <listeners>
            <add name="Flat File Trace Listener" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.FlatFileTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.FlatFileTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                fileName="trace.log" formatter="Text Formatter" traceOutputOptions="LogicalOperationStack" />
        </listeners>
        <formatters>
            <add type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                template="Timestamp: {timestamp}&#xA;Message: {message}&#xA;ActivityID: {activity}&#xA;Context: {category}&#xA;Priority: {priority}&#xA;EventId: {eventid}&#xA;Severity: {severity}&#xA;Title:{title}&#xA;Machine: {localMachine}&#xA;App Domain: {localAppDomain}&#xA;ProcessId: {localProcessId}&#xA;Process Name: {localProcessName}&#xA;Thread Name: {threadName}&#xA;Win32 ThreadId:{win32ThreadId}&#xA;Local Context: {dictionary({key} - {value}{newline})}"
                name="Text Formatter" />
        </formatters>
        <categorySources>
            <add switchValue="All" name="General">
                <listeners>
                    <add name="Flat File Trace Listener" />
                </listeners>
            </add>
        </categorySources>
        <specialSources>
            <allEvents switchValue="All" name="All Events" />
            <notProcessed switchValue="All" name="Unprocessed Category" />
            <errors switchValue="All" name="Logging Errors &amp; Warnings" />
        </specialSources>
    </loggingConfiguration>
</configuration>

This will result in an output like this:

----------------------------------------
Timestamp: 1/16/2013 3:50:11 PM
Message: doing something
ActivityID: 5b765d8c-935a-445c-b9fb-bde4db73124f
Context: General, ClientID=123456, MyRequestId=8f2828be-44bf-436c-9e24-9641963db09a
Priority: -1
EventId: 1
Severity: Information
Title:
Machine: MACHINE
App Domain: LoggingTracerNDC.exe
ProcessId: 5320
Process Name: LoggingTracerNDC.exe
Thread Name: 
Win32 ThreadId:8472
Local Context: 
----------------------------------------
----------------------------------------
Timestamp: 1/16/2013 3:50:11 PM
Message: requested file not found
ActivityID: 5b765d8c-935a-445c-b9fb-bde4db73124f
Context: General, ClientID=123456, MyRequestId=8f2828be-44bf-436c-9e24-9641963db09a
Priority: -1
EventId: 0
Severity: Information
Title:
Machine: MACHINE
App Domain: LoggingTracerNDC.exe
ProcessId: 5320
Process Name: LoggingTracerNDC.exe
Thread Name: 
Win32 ThreadId:8472
Local Context: filename - ABC.txt

----------------------------------------

Things to note:

  • Since we are using tracing we get the .NET activity ID for free which can be used to correlate activities. Of course we can use our own context information as well (custom request ID, client ID, etc.).
  • Enterprise Library uses the tracing "operation name" as a category so we need to set logWarningsWhenNoCategoriesMatch="false" otherwise we'll get a flurry of warning messages.
  • A downside of this approach may be performance (but I haven't measured it).

If you want to disable global context (which is, in this implementation, tracing) then all you need to do is edit the configuration file and set tracingEnabled="false".

This seems to be a fairly straight forward way to achieve your objectives using built-in Enterprise Library functionality.

Other approaches to consider would be to potentially use some sort of interception (custom LogCallHandler) which can be quite elegant (but that might depend on the existing design).

If you are going to go with a custom implementation to collect and manage context then you could consider looking at using Trace.CorrelationManager for per thread context. You could also look at creating an IExtraInformationProvider to populate the extended properties dictionary (see Enterprise Library 3.1 Logging Formatter Template - Include URL Request for an example).

Community
  • 1
  • 1
Randy Levy
  • 22,566
  • 4
  • 68
  • 94
  • +1. Great. That is somewhat close to what I'm looking for. It gave me lots of ideas. Thanks a lot. – Nawaz Jan 16 '13 at 16:09
  • One quesiton: how does `EnterpriseLibraryContainer.Current` know which instance of `LogWriter` it is supposed to return? How does it identify or figure out the context in which it is running? Is it using thread-id? What if I run `DoSomethingElse()` in a different thread? – Nawaz Jan 16 '13 at 16:13
  • 2
    The actual LogWriter implementation class is registered with the container as a Singleton so you will always get back the same LogWriter instance; concurrency is handled internally. Tracer actually uses CorrelationManager under the covers so all context information is stored in a thread-bound context. Each thread will have it's own context. – Randy Levy Jan 16 '13 at 16:23
  • So how would I make it work if the request-processing uses multiple-threads which is often the case (the most common being event handlers which run in different threads)? – Nawaz Jan 16 '13 at 16:28
  • 2
    Context information will (should) be populated in threads created within a logical operation. e.g. if I changed `DoSomethingElse("ABC.txt");` to `ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomethingElse), "ABC.txt");` the output will be the same. See [CorrelationManager Class](http://msdn.microsoft.com/en-us/library/system.diagnostics.correlationmanager.aspx) for a threading example. – Randy Levy Jan 16 '13 at 16:42
  • That is great. How about if I do this : `abc.OnCompleted += Handler;` then will `Handler` get the correct `LogWriter` instance (assuming it executes in different thread)? – Nawaz Jan 16 '13 at 16:58
  • There is one instance of LogWriter returned for the appdomain so every thread will have the same instance. – Randy Levy Jan 16 '13 at 17:31
  • Ohh.. in that case, I had to ask this question: will `Handler` log the correct context information? – Nawaz Jan 16 '13 at 17:34
1

Disclaimer: I do not know the specifics of Microsoft technical stack.

What I would do in this case is something along the lines of:

Implement a Singleton (at the server instance level, it does not need to be a global singleton in case your app is deployed to a cluster) which is basically a sort of Hashmap where "Key" is a uniquely meaningful identifier for the request you are processing. In Java I'd use the thread-id (assuming you serve the request by dispatching a thread from a thread pool). If this is impossible/does not make sense in your case, you have to use the Request ID (but in this case you have to make it percolate down to the log manager, while my idea would be to use something that is intrinsically available in the thread itself).

The "value" for the Hashmap will be a set of data - in your case ServerProfileId, WebProfileId, RequestId (unless it's already the key), SessionInfo - so that the logmanager can conditionally retrieve these and pick up the parts that are meaningful for a specific log event.

So: create a globally available reference to request state at the start of the request management, and make sure that the logging manager can retrieve request state when needed.

Of course you have to make sure that the hashmap is correctly managed (by removing state after a request has been disposed with).

p.marino
  • 6,244
  • 3
  • 25
  • 36
  • I'm thinking of writing a logger, say `ContextualLogger` whose instance will be passed down to the methods, and along the way updating the instance with contextual information (so that they can be logged with messages). In this way, the logger doesn't need anything from outside. Additionally I could inject an instance of some `LogContextProvider` (that implements `ILogContextProvider`) to the constructor of `ContexualLogger`. The context-provider will provide the important context whenever the logger needs it, and the GC will collect it when I'm done with it. Sounds good? – Nawaz Jan 16 '13 at 15:42
  • If the cost of injecting the ContextualLogger across the various call layers is not too high then your solution is probably better (if nothing else, you don't need to worry about cleaning up the Hashmap when a Request is concluded). My solution is better if passing the extra info as part of the method is too expensive/cumbersome. – p.marino Jan 16 '13 at 15:46
  • What do you mean by "cost" here? It is basically same as yours, except that I will pass down the logger instance itself instead of some ID. So I don't have to worry about other threads processing the other requests. Your solution is inherently multi-threaded, mine depends on whether I use threads to process the request or not. – Nawaz Jan 16 '13 at 15:48
  • 1
    I agree that passing the Request-id or an object instance is the same (you have to basically add an extra parameter to all the method calls where you expect to have to log something), so yes - if you can't use an "implicit" ID like the thread identifier the cost is the same, and your solution is better (more explicit, less "clutter" like my singleton hashmap). – p.marino Jan 16 '13 at 15:58
  • Thanks, that gave me a bit more confidence. But I'm also looking for possible improvements. I'm sure if I'm not the first facing this problem, looking for better way to log all relevant messages. – Nawaz Jan 16 '13 at 16:04