Introduction
Very often we have a necessity for program logging. Thus should include the possibility of using different data storage (files, databases, event logs, etc.) and the ability to control the level of information logging.
The library log4net
is one of many implementations, which help to solve all these problems.
Background
log4net
log4net
is a tool to help the programmer output log statements to a variety of output targets. In case of problems with an application, it is helpful to enable logging so that the problem can be located. With log4net
it is possible to enable logging at runtime without modifying the application binary. The log4net package is designed so that log statements can remain in shipped code without incurring a high performance cost. It follows that the speed of logging (or rather not logging) is crucial.
At the same time, log output can be so voluminous that it quickly becomes overwhelming. One of the distinctive features of log4net
is the notion of hierarchical loggers. Using these loggers, it is possible to selectively control which log statements are output at arbitrary granularity.
Features
- Support for multiple frameworks
- Output to multiple logging targets
- Hierarchical logging architecture
- XML configuration
- Dynamic configuration
- Logging context
- Proven architecture
- Modular and extensible design
- High performance with flexibility
Configuration
log4net
is configured using an XML configuration file. The configuration information can be embedded within other XML configuration files (such as the application's .config file) or in a separate file. log4net
can monitor its configuration file for changes and dynamically apply changes made by the configurator. The logging levels, appenders, layouts, and just about everything else can be adjusted at runtime.
Alternatively log4net
can be configured programmatically.
See also: official log4net site.
NHibernate
NHibernate
is an Object-relational mapping (ORM) solution for the Microsoft .NET platform: it provides a framework for mapping an object-oriented domain model to a traditional relational database. Its purpose is to relieve the developer from a significant portion of relational data persistence-related programming tasks.
Features
- Natural programming model -
NHibernate
supports natural OO idiom; inheritance, polymorphism, composition and the .NET collections framework, including generic collections - Native .NET -
NHibernate
API uses .NET conventions and idioms - Support for fine-grained object models - a rich variety of mappings for collections and dependent objects
- No build-time bytecode enhancement - there's no extra code generation or bytecode processing steps in your build procedure
- The query options -
NHibernate
addresses both sides of the problem; not only how to get objects into the database, but also how to get them out again - Custom SQL - specify the exact SQL that
NHibernate
should use to persist your objects. Stored procedures are supported on Microsoft SQL Server - Support for "conversations" -
NHibernate
supports long-lived persistence contexts, detach/reattach of objects, and takes care of optimistic locking automatically.
See also: official NHibernate site.
Using the Code
The log4net.Appender.AdoNetAppender
class is used in that library for logging in the database. In the settings for this class, an element connectionType
is given, which value
attribute must refer to the class, inherited from IDbConnection
:
<appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
<bufferSize value="100" />
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data/>
...
</appender>
In one of my projects NHibernate
ORM-solution was used, and I decided to use its capabilities to insert log information into the database. But I did not find (maybe it exists, but I simply didn't find) any support for this possibility for NHibernate
. So I made a wrapper for NHibernate.ISession
, which could be used in log4net
library.
I needed to implement the following interfaces:
IDbConnection
- represents an open connection to a data source, and is implemented by .NET Framework data providers that access relational databases. In my implementation, I use NHibernate
instead of the ADO.NET
classes:
- In
public
constructor, I create NHibernate.ISessionFactory
object - In
Open
method, I open NHibernate
session - In
BeginTransaction()
method, I create my HDbTransaction
object with the help of Session.BeginTransaction
ConnectionState
property returns connection state from NHibernate.ISession object
CreateCommand()
creates my HDbCommand
object Dispose()
method closes open connection and calls HNibernate.ISession Dispose()
method
public class HDbConnection : System.Data.IDbConnection
{
private ISessionFactory _sessionFactory;
private ISession Session { get; set; }
public HDbConnection()
{
Configuration config = new Configuration();
config.Configure();
config.AddAssembly(System.Reflection.Assembly.GetExecutingAssembly());
_sessionFactory = config.BuildSessionFactory();
}
#region IDbConnection Members
public System.Data.IDbTransaction BeginTransaction
(System.Data.IsolationLevel il)
{
return new HDbTransaction(Session.BeginTransaction(), il);
}
public void Open()
{
Session = _sessionFactory.OpenSession();
}
public System.Data.ConnectionState State
{
get
{
return Session.Connection.State;
}
}
public void Close()
{
Session.Close();
}
public string ConnectionString { get; set; }
public System.Data.IDbCommand CreateCommand()
{
return new HDbCommand(Session);
}
...
#endregion
#region IDisposable Members
public void Dispose()
{
if(Session.IsOpen) Session.Close();
Session.Dispose();
}
#endregion
}
As it's seen from the source, there was no necessity in the implementation of all methods and properties of this interface.
IDbTransaction
- represents a transaction to be performed at a data source. Here I simply use the relevant methods of NHibernate.ITransaction
interface:
public class HDbTransaction : IDbTransaction
{
private ITransaction _transaction;
private IsolationLevel _isolationLevel;
public HDbTransaction(ITransaction transaction)
{
_transaction = transaction;
}
...
#region IDbTransaction Members
public void Commit()
{
_transaction.Commit();
}
public void Rollback()
{
_transaction.Rollback();
}
...
#endregion
#region IDisposable Members
public void Dispose()
{
_transaction.Dispose();
}
#endregion
}
IDbCommand
- represents a SQL statement that is executed while connected to a data source. In my implementation of the most important thing is the method ExecuteNonQuery()
, which does a database query:
public class HDbCommand: IDbCommand
{
private ISession _session;
public HDbCommand(ISession session)
{
_session = session;
}
...
public int ExecuteNonQuery()
{
IQuery query = _session.CreateSQLQuery(CommandText);
foreach (HDbDataParameter p in Parameters)
query = query.SetParameter(p.ParameterName, p.Value);
return query.List().Count;
}
}
IDbDataParameter
- represents a parameter to an IDbCommand
object. The implementation is trivial: I just created public
properties with get
/set
accessors.
public class HDbDataParameter : IDbDataParameter
{
#region IDbDataParameter Members
public byte Precision { get; set; }
public byte Scale { get; set; }
public int Size { get; set; }
public DbType DbType { get; set; }
public ParameterDirection Direction { get; set; }
public string ParameterName { get; set; }
public string SourceColumn { get; set; }
public DataRowVersion SourceVersion { get; set; }
public object Value { get; set; }
...
#endregion
}
IDataParameterCollection
- collects all parameters relevant to an IDbCommand
object. Here I create HDbDataParameter
collection and use Linq
with List<hdbdataparameter />
<hdbdataparameter> methods to realize the necessary functionality:
public class HDbParameterCollection : IDataParameterCollection
{
IList<HDbDataParameter> parameters = new List<HDbDataParameter>();
public object this[string parameterName]
{
get
{
return (from p in parameters
where p.ParameterName == parameterName
select p).SingleOrDefault();
}
set{throw new NotImplementedException(); }
}
...
#region IEnumerable Members
public System.Collections.IEnumerator GetEnumerator()
{
return parameters.GetEnumerator();
}
#endregion
}
In the GetEnumerator()
method, I am returning IList.Enumerator()
realization.
Generally, I do not need to write some complex code. Some of the methods I left empty (with NotImplementedException
).
Then you need to create a database with a table. log4net
will insert log information using NHibernate
. Here is a script for Microsoft SQL Server
.
CREATE TABLE [dbo].[Log] (
[Id] [int] IDENTITY (1, 1) NOT NULL,
[Date] [datetime] NOT NULL,
[Thread] [varchar] (255) NOT NULL,
[Level] [varchar] (50) NOT NULL,
[Logger] [varchar] (255) NOT NULL,
[Message] [varchar] (4000) NOT NULL
)
Columns Date
, Thread
, Level
, Logger
and Message
will be filled with data by log4net
.
Then the class HDbConnection
and parameterized insert query must be specified in the application configuration file. The description of parameters are the same as in AdoNetAppender
:
<appender name="HibernateAdoNetAppender" type="log4net.Appender.AdoNetAppender">
<connectionType value="HLogger.HDbConnection, HLoggerLibrary" />
<commandText value="INSERT INTO Log (Date,Thread,Level,Logger,Message)
VALUES (:log_date, :thread, :log_level, :logger, :message)" />
<parameter>
<parameterName value="log_date" />
<dbType value="DateTime" />
<layout type="log4net.Layout.PatternLayout"
value="%date{yyyy'-'MM'-'dd HH':'mm':'ss'.'fff}" />
</parameter>
<parameter>
<parameterName value="thread" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout" value="%thread" />
</parameter>
<parameter>
...
</appender>
Note: In NHibernate
, parameters in the query must begin with ':'
Usage
When log4net
is configured, we can use it to log some information to the database:
using log4net;
namespace HLoggerConsole
{
class Program
{
static void Main(string[] args)
{
ILog log = log4net.LogManager.GetLogger("NHibernateLogging");
log.Error("Error!", new Exception("Exception"));
log.Warn("Warning");
}
}
}
After this, the following entries will appear in the database:
Because log4net
can use output to multiple logging targets, we can add other Appender
to catch errors associated with the database. In this example, the records will be added in a file and console:
...
<appender name="ConsoleAppender"
type="log4net.Appender.ConsoleAppender" >
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern"
value="%d [%t] %-5p %c [%x] [%X{auth}] - %m%n" />
</layout>
</appender>
...
<appender name="LogFileAppender"
type="log4net.Appender.FileAppender" >
<param name="File" value="log-file.txt" />
<param name="AppendToFile" value="true" />
<layout type="log4net.Layout.PatternLayout">
<param name="Header" value="[Header]"><param name="Footer" value="[Footer]">
<param name="ConversionPattern" value="%d [%t] %-5p %c [%x] [%X{auth}] - %m%n">
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="DEBUG" />
</filter>
</appender>
Notes
- The column '
Level
' is redundant. - In this example, I use a direct query to database with
NHibernate
. I think instead of using the direct insert query, it is better to use mapped class. To do this, you need to add this class and change the implementation of the method HDbCommand.ExecuteNonQuery()
.
History
- 2nd February, 2010: First version of article
- 3rd February, 2010: Updated article
- 4th February, 2010: Updated article