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.
[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.
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:
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:
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:
try {
} catch (Exception ex) {
LoggerFactory.GetInstance(Server.MapPath("logger.config"),
"my web app").Log(ex);
}
Config Builder: The 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.