One of my colleagues came-up with a unique situation where it was required to create log files based on the input file which is uploaded. For example if A.xml is uploaded, the corresponding log file should be A_log.txt.
I am a strong believer that Logging / EH / caching are cross-cutting architecture aspects and should be least invasive to the business-logic written in enterprise application.
I have been using Enterprise Library for logging / EH (i use to work with Avanade, so i have affection towards the library!! :D ). I have been also using excellent library called PostSharp for cross cutting aspect. Here i present a solution with and without PostSharp all in a unit test. Please see full source code at end of the this blog post.
But first, we need to tweak the enterprise library so that the log files are created at runtime based on input given. Below is Custom trace listner which writes log into a given file extracted out of Logentry extendedProperties property.
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Logging.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using System.IO;
using System.Text;
using System;
using System.Diagnostics;
namespace Subodh.Framework.Logging
{
[ConfigurationElementType(typeof(CustomTraceListenerData))]
public class LogToFileTraceListener : CustomTraceListener
{
private static object syncRoot = new object();
public override void TraceData(TraceEventCache eventCache, string source,
TraceEventType eventType, int id, object data)
{
if ((data is LogEntry) & this.Formatter != null)
{
WriteOutToLog(this.Formatter.Format((LogEntry)data), (LogEntry)data);
}
else
{
WriteOutToLog(data.ToString(), (LogEntry)data);
}
}
public override void Write(string message)
{
Debug.Print(message.ToString());
}
public override void WriteLine(string message)
{
Debug.Print(message.ToString());
}
private void WriteOutToLog(string BodyText, LogEntry logentry)
{
try
{
//Get the filelocation from the extended properties
if (logentry.ExtendedProperties.ContainsKey("filelocation"))
{
string fullPath =
Path.GetFullPath(logentry.ExtendedProperties["filelocation"].ToString());
//Create the directory where the log file is written to if it does not exist.
DirectoryInfo directoryInfo
= new DirectoryInfo(Path.GetDirectoryName(fullPath));
if (directoryInfo.Exists == false)
{
directoryInfo.Create();
}
//Lock the file to prevent another process from using this file
//as data is being written to it.
lock (syncRoot)
{
using (FileStream fs
= new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.Write, 4096, true))
{
using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8))
{
Log(BodyText, sw);
sw.Close();
}
fs.Close();
}
}
}
}
catch (Exception ex)
{
throw new LoggingException(ex.Message, ex);
}
}
/// <summary>
/// Write message to named file
/// </summary>
public static void Log(string logMessage, TextWriter w)
{
w.WriteLine("{0}", logMessage);
}
}
}
The above can be “plugged into” the code using below configuration
<loggingConfiguration name="Logging Application Block" tracingEnabled="true"
defaultCategory="Trace" logWarningsWhenNoCategoriesMatch="true">
<listeners>
<add listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.CustomTraceListenerData,
Microsoft.Practices.EnterpriseLibrary.Logging, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
traceOutputOptions="None" filter="All" type="Subodh.Framework.Logging.LogToFileTraceListener,
Subodh.Framework.Logging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
name="Subodh Custom Trace Listener" initializeData="" formatter="Text Formatter" />
</listeners>
Similarly we can use PostSharp to expose the above as cross cutting aspects as below
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using PostSharp.Laos;
using System.Diagnostics;
using GC.FrameworkServices.ExceptionHandler;
using Subodh.Framework.Logging;
namespace Subodh.Framework.ExceptionHandling
{
[Serializable]
public sealed class LogExceptionAttribute : OnExceptionAspect
{
private string prefix;
private MethodFormatStrings formatStrings;
// This field is not serialized. It is used only at compile time.
[NonSerialized] private readonly Type exceptionType;
private string fileName;
/// <summary>
/// Declares a <see cref="XTraceExceptionAttribute"/> custom attribute
/// that logs every exception flowing out of the methods to which
/// the custom attribute is applied.
/// </summary>
public LogExceptionAttribute()
{
}
/// <summary>
/// Declares a <see cref="XTraceExceptionAttribute"/> custom attribute
/// that logs every exception derived from a given <see cref="Type"/>
/// flowing out of the methods to which
/// the custom attribute is applied.
/// </summary>
/// <param name="exceptionType"></param>
public LogExceptionAttribute( Type exceptionType )
{
this.exceptionType = exceptionType;
}
public LogExceptionAttribute(Type exceptionType, string fileName)
{
this.exceptionType = exceptionType;
this.fileName = fileName;
}
/// <summary>
/// Gets or sets the prefix string, printed before every trace message.
/// </summary>
/// <value>
/// For instance <c>[Exception]</c>.
/// </value>
public string Prefix { get { return this.prefix; } set { this.prefix = value; } }
/// <summary>
/// Initializes the current object. Called at compile time by PostSharp.
/// </summary>
/// <param name="method">Method to which the current instance is
/// associated.</param>
public override void CompileTimeInitialize( MethodBase method )
{
// We just initialize our fields. They will be serialized at compile-time
// and deserialized at runtime.
this.formatStrings = Formatter.GetMethodFormatStrings( method );
this.prefix = Formatter.NormalizePrefix( this.prefix );
}
public override Type GetExceptionType( MethodBase method )
{
return this.exceptionType;
}
/// <summary>
/// Method executed when an exception occurs in the methods to which the current
/// custom attribute has been applied. We just write a record to the tracing
/// subsystem.
/// </summary>
/// <param name="context">Event arguments specifying which method
/// is being called and with which parameters.</param>
public override void OnException( MethodExecutionEventArgs context )
{
string message = String.Format("{0}Exception {1} {{{2}}} in {{{3}}}. \r\n\r\nStack Trace {4}",
this.prefix,
context.Exception.GetType().Name,
context.Exception.Message,
this.formatStrings.Format(context.Instance,
context.Method, context.GetReadOnlyArgumentArray()),
context.Exception.StackTrace);
if(!string.IsNullOrEmpty(fileName))
{
ApplicationLogger.LogException(message, fileName);
}
else
{
ApplicationLogger.LogException(message, Source.UtilityService);
}
}
}
}
To use the above below is the unit test
[TestMethod]
[ExpectedException(typeof(NotImplementedException))]
public void TestMethod1()
{
MethodThrowingExceptionForLog();
try
{
MethodThrowingExceptionForLogWithPostSharp();
}
catch (NotImplementedException ex)
{
throw ex;
}
}
private void MethodThrowingExceptionForLog()
{
try
{
throw new NotImplementedException();
}
catch (NotImplementedException ex)
{
// create file and then write log
ApplicationLogger.TraceMessage("this is a trace message
which will be logged in Test1MyFile", @"D:\EL\Test1Myfile.txt");
ApplicationLogger.TraceMessage("this is a trace message
which will be logged in YetAnotherTest1Myfile", @"D:\EL\YetAnotherTest1Myfile.txt");
}
}
// Automatically log details using attributes
// Log exception using attributes .... A La WCF [FaultContract(typeof(FaultMessage))] style]
[Log(@"D:\EL\Test1MyfileLogPostsharp.txt")]
[LogException(typeof(NotImplementedException), @"D:\EL\Test1MyfileExceptionPostsharp.txt")]
private void MethodThrowingExceptionForLogWithPostSharp()
{
throw new NotImplementedException();
}
The good thing about the approach is that all the logging and EH is done at centralized location controlled by PostSharp. Of Course, if some other library has to be used instead of EL, it can easily be plugged in. Also, the coder ARE ONLY involved in writing business code in methods, which makes code cleaner.
Here is the full source code. The third party assemblies provided are from EL and PostSharp and i presume you will find these useful.
Do let me know your thoughts / ideas on the same.