Contents
This article is a remake of Simple XML based Error Log. However in this release, I've made lot of changes so I'd rather come up with a new article instead of an update. The basic idea is the same, to log exceptions which have occurred in your application into an XML file. The major differences are:
- All Exceptions are logged as derivate from base exception
DNHCommonException
from namespace DNH.Common
. I'll write about it in detail later in this article. ErrorLog
provides more options in logging:
- single file
- single file per hour
- single file per day
- single file per exception
- Actual code for logging is completely rewritten in order to improve performance and consume less memory. Again, I'll cover optimizations in this article.
ErrorLog
comes with pre-created XSLT transformations. ErrorLog
is now thread safe. - Logging format is described via XML Schema.
- Exception itself now provides actual formatting into XML.
DNH.Common.Errors
provides means of error notification (will discuss about notification event and notification providers later). ErrorLog
has Error
event.
As you can see, the main focus is on scalability and reasonable performance, so I believe this article can be useful.
This article provides some examples on how to:
- effectively append into large XML files.
- design plugin architecture (OK, I'd love some feedback here).
- send mails using classes from
System.Web.Mail
. - transform XML file using XSLT support in
System.Xml
transformations. - effectively query XML file using classes from
System.Xml.XPath
namespace. - raise and handle events.
ErrorLog
is part of my library DNH.Common
. This library is everything but finished project, but don't worry. Most likely (90% chance) I won't change its interface with regard to logging solution presented here. The basic piece of library is DNH.Common.Dll. In this assembly, there is one very important class and interface. The class is called DNHCommonException
and that interface is IOutputAsXml
.
As name of this interface says, classes implementing IOutputAsXml
provide some kind of XML output. Point of this system is that you don't care about XML representation of these classes. You just cast them to IOutputAsXml
. This is how it looks in C#:
public interface IOutputAsXml
{
XmlDocument ToXml();
System.Xml.Schema.XmlSchema Schema{get;set;}
}
And this is a typical usage:
class MyObject:IOutputAsXml
{
private string _value;
public XmlDocument ToXml()
{
XmlDocument doc = new XmlDocument();
XmlDeclaration declaration =
doc.CreateXmlDeclaration("1.0","UTF-8","yes");
XmlElement root =
doc.CreateElement(null,"root",this.Schema.TargetNamespace);
root.InnerText = this._value;
doc.AppendChild(declaration);
doc.AppendChild(root);
return doc;
}
}
...
XmlDocument doc = myObject.ToXml();
doc.Save("myObject.xml");
...
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>myValue</root>
At this point, you can see that it's possible to call ToXml()
for members, so you can easily represent compound objects in XML.
In .NET the word Serialization means: "Serialization can be defined as the process of storing the state of an object instance to a storage medium. During this process, the public and private fields of the object and the name of the class, including the assembly containing the class, is converted to a stream of bytes, which is then written to a data stream. When the object is subsequently deserialized, an exact clone of the original object is created." On the other way, IOutputAsXml
obligate implementers to provide *some* XML representation - it's not required nor intended to be an exact clone. Think about it as the XML alternative to generic object.ToString()
method.
DNHCommonException
derives from System.Exception
and implements IOutpuAsXml
interface. DNHCommonException
provides readonly property DNHCommonExceptionType ExceptionType
which returns member of DNHCommonExceptionType
. Possible values and their meanings are:
public enum DNHCommonExceptionType
{
Error = 0,
FatalError = 1,
SecurityError = 2,
UnhandledError = 3
}
You can specify the type of error in exception's constructor overload. Obviously, this information is included in actual log file.
You can find XML Schema for basic DNHCommon.DNHCommonException
here.
This class implements actual writing to XML file. There are several "modes" of logging, and technique of writing XML sometimes differ. ErrorLog
exposes two events: Error
and Notification
. The first event fires before ErrorLog
logs exception, and second event fires before notification is sent.
Note: In the current implementation, both events fire before an exception is logged, but this may change in the future. The rule is that Error event fires before exception is logged and Notification event fires before notification is send (as I already wrote), but there is no "correct" order for Error and Notification events (and for actual logging and sending notification, too).
What happens when it comes to saving error information into file depends on logging mode. When in oneExceptionFile
mode, saving is done this way:
XmlDocument doc = new XmlDocument();
doc.LoadXml(this.ToXml().OuterXml);
XmlDocumentFragment frag = doc.CreateDocumentFragment();
frag.InnerXml = e.ToXml2().OuterXml;
doc.DocumentElement.AppendChild(frag);
doc.Save(tempLogName);
It can't be more straightforward. One entry is a relatively small piece of XML, so there is no big deal with using XmlDocument
.
A different situation is when we use one of the "appending" modes. In this scenario, XML files grows and you could easily get files about few MB or larger. So it's time to use the different approach. In my code you can find this method:
private void Append(string path, DNHCommonException e)
{
StreamWriter sw = File.AppendText(path);
XmlTextWriter xtw = new XmlTextWriter(sw);
xtw.WriteRaw(e.ToXml2().OuterXml);
xtw.Close();
}
Basically, it appends XML into plain text file... that XML doesn't even have root element, so it can't be used directly. Instead, I use the technique described in Efficient Techniques for Modifying Large XML Files. Point is in using IO API, which is much faster than using XmlReader
/XmlWriter
or (even worse) XmlDocument
. Now there is question: OK, so we have XML-like plain text file. How to make "regular" XML file from it, with all advantages of XSLT, etc.? Answer is - entity. Together with text file, there is the XML file like this:
="1.0"="UTF-8"="no"
<!doctype errorlog[<!ENTITY log SYSTEM "log.txt">]>
<errorlog xmlns="http://dnhsoftware.org/schemas/dnhcommon/ErrorLog.xsd">
&log;
</errorlog>
As you can see in document type definition, &log;
is reference to the external entity, in this case it's the text file log.txt. When XML reader which is able to expand entities (for example XmlValidatingReader
) reads our XML file, it "includes" log.txt into our XML file. So, in result, when you call XSTL transformation on it (as shown later in this article), result contains ALL markup - even from log.txt.
NotificationProviderBase
is base class (surprisingly) for notification providers. It has one function:
public abstract void Send(DNHCommonException ex);
You call this function when you want to send notification of error (e.g. you want to send email to administrator and tell him that access to DB failed or something like that). Rules when, how and who to send notification are described in configuration file. DNH.Common.Errors
comes with one implementation of NotificationProviderBase
, EmailNotificationProvider
is in assembly DNH.Common.Errors.Providers.EmailNotificationProvider.dll. You can look how it's written (source is included with library). In fact override for Send()
is very simple:
public override void Send(DNHCommonException ex)
{
if(this._smtpServer != null)
{
SmtpMail.SmtpServer = this._smtpServer;
}
SmtpMail.Send(message);
}
Class ErrorLogConfig
, as it's name says, provides API to library's configuration file. There are classes from System.Configuration
namespace, but they don't meet my needs (or I don't know/understand them enough?), so I wrote ErrorLogConfig
class in order to provide API I needed. It internally uses System.Xml.XPath.XPathDocument
for reading configuration file. Speaking about XPath
document, there is another rule for efficient use of XML in the .NET framework: Don't use XmlDocument
when you don't want to modify your XML document. XmlDocument
is relatively a heavy thing and it is memory/performance overkill to use it for XPath-quering (selecting nodes) in XML file. Use XPathDocument
instead.
="1.0"="utf-8"
<configuration>
<exceptions>
<exception type="DNH.Common.DNHCommonException">
<target name="boss" />
<target name="admin" />
</exception>
</exceptions>
<notifications>
<notification target="admin@example.org" nick="admin" >
<provider name="Email" />
</notification>
<notification target="boss@contozo.com" nick ="boss" >
<provider name="Email" />
</notification>
</notifications>
<providers>
<provider name="DNH.Common.Errors.Providers.EmailNotificationProvider"
nick="Email" />
</providers>
</configuration>
I hope this structure is intuitive enough. Typically, there is a long list of exceptions and much shorter list of targets and providers.
This is a helper class, it has one method:
public static void FormatErrorLog(string inFile, string outFile,
String StyleSheetURI)
This method applies XSLT transformation (StyleSheetURL
parameter is URL to .xslt file) to file inFile
and saves result as outFile
. There are two predefined .xslt files in demo, log2html.xslt and log2txt.xslt. First one formats log's XML into nice HTML, second one into old-school plain text format. Of course, if you know XSLT, you can provide your own formatting. Implementation looks like this:
public static void FormatErrorLog(string inFile, string outFile, String StyleSheetURI)
{
XslTransform transform = new XslTransform();
transform.Load(StyleSheetURI);
XmlWriter outWriter = new XmlTextWriter(outFile,System.Text.Encoding.UTF8);
XmlDocument myDoc = new XmlDocument();
XmlValidatingReader vr = new XmlValidatingReader(new XmlTextReader(inFile));
vr.EntityHandling = EntityHandling.ExpandEntities;
vr.ValidationType = ValidationType.None;
myDoc.Load(vr);
XmlUrlResolver myResolver = new XmlUrlResolver();
myDoc.XmlResolver = myResolver;
transform.Transform(myDoc,null,outWriter,myResolver);
outWriter.Close();
}
Now last thing: how to actually use ErrorLog
? You can declare/create new instance of ErrorLog
like:
public ErrorLog log;
log = new ErrorLog("logs/log.xml");
Than you can call ErrorLog.SaveExceptionToLog(Exception ex)
method in try-catch
block.
try
{
InfinityRulez();
}
catch (Exception ex)
{
log.SaveExceptionToLog(ex);
}
If you want to do some custom action (e.g display error page) when ErrorLog.SaveExceptionToLog()
method is called, you can register event Error
like this:
private void InitializeComponent()
{
this.log.Error += new ErrorLogErrorEventHandler(log_Error);
...
Then you only need to write body of log_Error
handler... You can register/handle Notification
event same way.
For guidelines about throwing and handling exceptions, please search CodeProject, or MSDN, or search with Google or other favourite search engines by your choice. There is a lot to say about this topic, it would be material for a whole new article. So I just skip this part and pray that you (reader) will investigate it by yourself!
I've included demo application, so you can quickly see my solution in action. User interface and whole application is very simple and it has only one purpose - to show how logs looks, that it is thread safe etc... However there are some hard-coded paths so please believe me that folders logs, XSLT and their content should be where it is now... Of course, only in this sample! In real use you can change all paths as you wish. However configuration file DNH.Common.Errors.Config must be in same folder as assembly DNH.Common.Errors.Dll. Also if you want to use notification providers (plugins) their location must be same as you write in .config file. That's all, have fun. I am looking forward to your bug reports, ideas and improvements!
There is of course a lot to do... :
[TODOs from Simple XML based Error Log]
case when more exceptions occurs in same time should be handled. Done. add more informations (like infos about currently logged users in your app). Done. manipulating with XML could be done using XmlDocument
or XmlDocumentFragment
. It's more clear. NO! generated XML is described by DTD. Expect use of XML Schema in next release. Done. - this one is difficult: automatic analysis of occured errors. but it would be great :)
[Current TODOs]
- Implement some kind of friendly error messages for user (yes, like in User Friendly Exception Handling).
- Improve project documentation.
- Further improve performance.
References
History