Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / SQL

Information Logging API for .NET

4.88/5 (10 votes)
10 Jul 2006CPOL10 min read 5   885  
An API for logging information in response to application events such as exceptions.

Sample Image - InfoLogger1.png

Introduction

I started this project about three years ago, and in that time, this API has evolved some, as needs came about. The basic idea was to have a library that did simple information logging, from debug traces, to exception logging; from plain text files, to database, to event logs, to XML. The result of many small design changes to accommodate the needs of various applications, is an API that allows for a wide variety of logging scenarios and configurations, all configured through XML config files.

There are several stand-alone logger implementations for different backing stores, and several wrapping loggers that provide for more complex configurations. You could, for instance, create a logger that logs only warnings and errors to database, and logs of all types to a text file, and in case of failure, connects to the database log to email and the system log.

AbstractLogger : ILogger

AbstractLogger, as you might guess from the name, is an abstract implementation of the ILogger interface. In many places, it is referenced in place of the interface for reasons related to the XmlSerializer's inability to deserialize interfaces. However, the interface is generally used when using an instance of a logger in your code. This abstract implementation provides some basic services that all loggers require, simplifying other implementations. It only has three abstract methods that an inheriting logger must implement, which will be covered below. My database implementation assumes a specific data structure, but you could easily create your own implementation to conform to your own specifications. Beyond that, I have ceased to find a need for other implementations. To get a better understanding of this abstract class, let's look as its public members.

XML Serialization

Before looking at members though, it's important to note the XML serialization attributes attached to this class. This class has several XmlInclude attributes to aid in XML serialization, which is necessary for the ability to load loggers from config files.

C#
[XmlInclude(typeof(MailLogger))]
[XmlInclude(typeof(FileLogger))]
[XmlInclude(typeof(XmlLogger))]
[XmlInclude(typeof(EventLogger))]
[XmlInclude(typeof(EmptyLogger))]
[XmlInclude(typeof(DataLogger))]
[XmlInclude(typeof(CompositeLogger))]
[XmlInclude(typeof(ChainLogger))]
[XmlInclude(typeof(SafeLogger))]
public abstract class AbstractLogger : ILogger {
    //...
}

The XmlInclude attributes tell the XmlSerializer what types to expect when serializing or deserializing. Because the XmlSerializer must know about the possible concrete types for this class, the API itself would need modification for you to be able to use the config file features of this API with your own implementations. For more information on XML serialization, see my article: Using the XmlSerializer Attributes.

Logging Level and Strict Level

Each logger implementation uses these two properties to determine if a log they've been asked to process should be logged by them. The LoggingLevel property, of type LogType, determines the level at which the logger will consider logging. The LogType enum has four values: Information, Debug, Warning, and Error. Each Log object has a LogType. If a logger has a LoggingLevel of Warning, it will ignore logs with levels below Warning. The StringLevel property is a boolean property that determines if the LoggingLevel is the only level that it will log, or if it will log higher levels as well. A StrictLevel of true will only log the level specified in LoggingLevel. Clear as mud? Playing with the sample application should help you understand.

IsLoggable(LogType)

IsLoggable is an internal method that concrete implementations can use to decide if a log should be processed based on the LoggingLevel and StrictLevel properties. If you create your own implementation, you can use this base method to make that determination for you before logging a log.

Log(ILog)

The Log method is where most of the functionality happens. Implementations of this class don't normally supply their own Log method; they instead override the abstract method DoLog discussed below. This method does some checking, including a call to IsLoggable before delegating the actual logging to the DoLog implementation of the current instance.

Abstract Methods: DoLog(ILog), GetLogs(), Dispose()

All implementations of AbstractLogger (that are not abstract themselves) must implement these three methods. The ILogger interface inherits from IDisposable, so all implementations must provide a Dispose method to release any external resource, such as database connections or file handles. The interface also requires a GetLogs method for retrieving logs, though not all implementations actually do this due to their inability to retrieve logs from their particular media. Database, event logs, and XML are some loggers that can retrieve logs. And finally, the abstract class requires that you override the DoLog method, which is where the different implementations actually do their entry of logs.

OnLog Event

The abstract class will fire the OnLog event when a log has been processed, so any code that is interested in knowing about logging can subscribe to this event and be notified of a logging occurrence. To date, I've not found a good use for this feature, but it seemed reasonable to include it. There may be some situations where this becomes helpful.

Logger Concrete Implementations

First, we will discuss the three wrapper implementations of AbstractLogger that aid in creating complex hierarchies of loggers; then, we will look at the simple logger implementations.

Logger Wrappers

The wrapper loggers wrap one or more loggers to provide custom processing.

CompositeLogger

The CompositeLogger wraps multiple instances of loggers for scenarios where you want to log to multiple loggers. As an example, you might want to log warnings and errors to a database, and only errors to email. So, you would create a CompositeLogger, with a logging level of Warning or the default of Information, that wraps a DataLogger with a logging level of Warning and an EmailLogger with a logging level of Error.

ChainLogger

The ChainLogger follows the "Chain of Responsibility" design pattern. It wraps multiple loggers, and when asked to process a log, it will attempt each logger in its internal collection until it finds an instance that can process the log. A logger might not process a log for several reasons, including an exception that results in a failure to log, or a negative response from its IsLoggable method due to logging level restrictions. Exceptions can occur with database or file loggers, for instance. If none of the wrapped loggers handle the log, and any of those attempted failed due to exception, an exception is thrown from this wrapper. As an example, you might want to log to a database, and log to email if the database log failed. So, you would create a ChainLogger that wraps a DataLogger and an EmailLogger.

SafeLogger

This is a simple implementation that wraps a single logger. All it does is try to log to its wrapped logger, and suppress exceptions if its logger has a failure. It is sometimes useful to use this as a root logger to ensure that the application doesn't fail due to a logging failure.

Simple Loggers

ConsoleLogger

The ConsoleLogger, as you might suspect, logs to the console. This logger can be useful during debugging; then you might want to switch to a more useful logger once the project moves from development. Loggers such as this, that log to a string, use the ILog's ToString method to generate its output. The FileLogger uses a similar approach. See the Log section below for more details on a log's text output.

DataLogger

My implementation of a database logger currently only supports Microsoft SQL Server, and uses the following data definition. It also uses my proprietary Data Access API, which is detailed in my article: Simplified .NET Data Access API.

SQL
CREATE TABLE [dbo].[Logging] (
    [Log_id] [int] IDENTITY (1, 1) NOT NULL ,
    [Date] [datetime] NOT NULL ,
    [Application] [varchar] (15) NOT NULL ,
    [Status] [varchar] (15) NULL ,
    [Type] [varchar] (15) NULL ,
    [Identifier] [varchar] (15) NULL ,
    [Message] [varchar] (255) NOT NULL ,
    [Details] [text] NULL 
)
GO

CREATE TABLE [dbo].[LoggingStatus] (
    [Status] [varchar] (15) NOT NULL 
)
GO

CREATE TABLE [dbo].[LoggingTypes] (
    [Type] [varchar] (15) NOT NULL 
)
GO

ALTER TABLE [dbo].[Logging] ADD 
    CONSTRAINT [df_Logging_Date] DEFAULT (getdate()) FOR [Date],
    CONSTRAINT [pk_Logging_Log_id] PRIMARY KEY  NONCLUSTERED 
    (
        [Log_id]
    )  
GO

ALTER TABLE [dbo].[LoggingStatus] ADD 
    CONSTRAINT [pk_LoggingStatus_Status] PRIMARY KEY  NONCLUSTERED 
    (
        [Status]
    )  
GO

ALTER TABLE [dbo].[LoggingTypes] ADD 
    CONSTRAINT [pk_LogingTypes_Type] PRIMARY KEY  NONCLUSTERED 
    (
        [Type]
    )  
GO

ALTER TABLE [dbo].[Logging] ADD 
    CONSTRAINT [fk_Logging_Status] FOREIGN KEY 
    (
        [Status]
    ) REFERENCES [dbo].[LoggingStatus] (
        [Status]
    ),
    CONSTRAINT [fk_Logging_Type] FOREIGN KEY 
    (
        [Type]
    ) REFERENCES [dbo].[LoggingTypes] (
        [Type]
    )
GO

create procedure spLoggingAdd
@Application varchar(15), 
@Date datetime, 
@Status varchar(15), 
@Type varchar(15), 
@Identifier varchar(15), 
@Message varchar(255), 
@Details text
as
    insert into Logging (Application, Date, Status, Type, Identifier, Message, Details)
     values (@Application, @Date, @Status, @Type, @Identifier, @Message, @Details)
return @@IDENTITY
GO


create procedure spLoggingUpd
@Log_id int, 
@Application varchar(15), 
@Date datetime, 
@Status varchar(15), 
@Type varchar(15), 
@Identifier varchar(15), 
@Message varchar(255), 
@Details text
as
    update Logging set
        Application = @Application,
        Date = @Date, 
        Status = @Status, 
        Type = @Type, 
        Identifier = @Identifier, 
        Message = @Message, 
        Details = @Details
        where Log_id = @Log_id
return @@ROWCOUNT
GO

insert into LoggingStatus (Status) values ('Failure')
insert into LoggingStatus (Status) values ('Other')
insert into LoggingStatus (Status) values ('Success')

insert into LoggingTypes (Type) values ('Debug')
insert into LoggingTypes (Type) values ('Error')
insert into LoggingTypes (Type) values ('Information')
insert into LoggingTypes (Type) values ('Warning')

The DataLogger has one property: ConnectionInfo, which is of type ConnectionInfo, which is a data config class used in my data access API. This property defines the connection information for your database. This implementation also provides a handfull of other methods for more specific requests for existing logs and for truncating the logs.

EmptyLogger

The EmptyLogger does nothing with a log. It is sometimes used as a place holder logger.

EventLogger

The EventLogger logs to the system event log. It has a Name property that is required to log to the event log. This is the name that will be used for the application name in the event log.

FileLogger

The FileLogger logs to a text file, as you might suspect. The file name, location, and extension do not matter. This logger, like the ConsoleLogger, MailLogger, and StreamLogger, uses the Log.ToString method to generate its text output. It has one property FileName, which specifies the path for the log file.

MailLogger

The MailLogger logs to an email. This logger has three properties, for the email address that the email will be from, the server to use, and a StringCollection for the recipient addresses.

StreamLogger

This logger uses the Log.ToString method to log to a text stream. Because this logger holds an instance of a stream, it cannot participate in the config file loading. This is a more general implementation than the ConsoleLogger, that can be used in the same fashion for similar purposes.

XmlLogger

The XmlLogger logs to an XML file through XML serialization. It uses the XmlLogFile and XmlLogFileSerializer classes to facilitate reading and writing logs. Like the FileLogger, it has a FileName property to specify the file path to use.

Logs

Now, to talk about the logs themselves... The diagram below shows the types related to logs:

Log API

The Log class implements the ILog interface, both of which use the LogType and LogStatus enumerations to classify their data. Log also uses the LogFormatter class to determine its text output when using its ToString method. When you create a Log instance, you can alter its format pattern if you wish. The formatter patterns used by LogFormatter are discussed below. This class has several constructors for initializing its information. The properties of Log are described below:

  • Application is the name of the application creating the log.
  • LogTime is automatically initialized to the time of the log entry.
  • Status is of type LogStatus, showing the log's status of Success, Failure, or Other.
  • LogType is of type LogType, specifying if the log is of type Information, Debug, Warning, or Error.
  • Identifier is a string to identify the log with (not required).
  • Message is the log message, meant to be a summary statement like an exception message.
  • Details holds any information you'd like to attach, meant for information like an exception's stack trace.
  • Object can hold any object you wish to attach. When outputting text, the object's ToString method will be used.
  • Formatter is an instance of LogFormatter used for formatting the text output of logs, which will be discussed shortly.

LogFormatter

This class is responsible for generating a string representation of an ILog instance with the pattern specified. Patterns may include any of the following values, which will be replaced with the corresponding property values:

  • {app}
  • {time}
  • {status}
  • {type}
  • {id}
  • {message}
  • {details}
  • {object}

The formatter uses a simple String.Replace to replace the pattern identifiers with the log values. Other than that, your formatter string can include any text you wish.

Config and Serialization

This API is designed with XML serialization in mind. The sample application is specifically for creating and editing logger config files. The classes related to config serialization are shown in the class diagram below:

Config API

The LoggerConfig class, basically, just holds an instance of an AbstractLogger, and provides a root node for config file serialization. The LoggerConfigSerializer class reads and writes logger config XML, and is the main mechanism for constructing a logger from config information.

LoggerFactory

This class is provided as a convenience for users of this API. You can create instances of this class with its single constructor that requires a config file path and an application name, or you can use its static GetInstance method that takes the same parameters. It encapsulates the process of reading the config file and constructing a logger instance, and provides simple pass-through methods to log different types of information. So, an ASPX application could use this API as easily as the following example:

C#
try {
    // BOOM!
} catch (Exception ex) {
    LoggerFactory.GetInstance(Server.MapPath("logger.config"), 
                              "my web app").Log(ex);
}

Config Builder: The Sample Application

Sample Application

The sample application allows you to build and save, or read and edit a logger config. Also, with a logger loaded, you can use this tool to send test logs to your structure to ensure its behavior.

Conclusion

This component has been very useful to me throughout the development of many applications, from personal utilities to web services in production environments. I hope you find it useful, or at the very least, interesting for its design aspects or inspiration for your own API. It also stands as a testament to XML serialization, which has proven to be an immensely useful tool for many different applications, not least of all, storing configuration information.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)