Skip to main content

Using Spring AOP to create a quick/simple auditing mechanism

These days I have been working in .NET. The project I am on uses Spring.NET and NHibernate for data access. One of the requirements of the project was to capture user activity and log them. There are certainly many different approaches to achieve this goal. There are solutions that you could deploy to your database (triggers etc.) or create application level services that would capture and log user activity. Since the project was already utilizing Spring, I wanted to utilize Spring's AOP features.

The first thing I did was to create two database tables that would hold audit data. The first table I called history_log and the second table I called history_log_param.

The history_log table has the following columns.
CREATE TABLE [dbo].[history_log](
[id] [int] IDENTITY(1,1) NOT NULL,
[table_name] [varchar](50) NOT NULL,
[method_name] [varchar](50) NOT NULL,
[argument_count] [int] NOT NULL,
[message] [varchar](500) NOT NULL,
[user_name] [varchar](50) NOT NULL,
[date] [datetime] NOT NULL CONSTRAINT [DF_history_log_date] DEFAULT (getdate()))
The history_log_param table had the following columns.

CREATE TABLE [dbo].[history_log_param](
[id] [int] IDENTITY(1,1) NOT NULL,
[log_id] [int] NOT NULL,
[param_name] [varchar](50) NOT NULL,
[param_type] [varchar](50) NOT NULL,
[param_value] [text] NOT NULL,
[date] [datetime] NOT NULL CONSTRAINT [DF_history_log_param_date] DEFAULT (getdate()))
Needless to say log_id in history_log_param table is a foreign key to history_log table.

You can see that each log entry in history log can have multiple log parameter enteries.

Once the tables are set I went ahead and created an AOP advice using Spring.NET which I called HistoryLogAdvice. This advice implements IAfterReturningAdvice. The responsibility of this advice is to create a HistoryLog and HistoryLogParam objects populate the values in each object and persist them using a Dao object.

Here is the Spring.NET configuration I used to configure this advice.
<object id="historyLogAdvice" type="App.Utils.Aop.HistoryLogAdvice">
<property name="AppDao" ref="appDao">
</property></object>

<object id="appDao" type="Spring.Aop.Framework.ProxyFactoryObject">
<property name="target" ref="appDaoReal">
<property name="interceptorNames">
<list>
<value>historyLogAdvice</value>
</list>
</property>
</property></object>


This configuration enables this advice for any method that is invoked on appDao object.

The transaction configuration was set at the Service layer. Therefore, when this advice is executing and using the appDao to save the HistroyLog and HistoryLogParam objects it would participate in the existing transaction. If for some reason an exception is thrown the changes would be rolled back.

One thing to note is that method calls on the Dao to save the audit objects would create a recursive call stack. To eliminate this and to capture some specific metadata about the method call, I created an attribute called TableAttribute. Here is the definition for TableAttribute.


[System.AttributeUsage(System.AttributeTargets.Method)]

public class TableAttribute : System.Attribute
{
private string tableName;
private string customMessage;

public TableAttribute(string tableName)
{
this.tableName = tableName;
}

public TableAttribute(string tableName, string customMessage)
{
this.tableName = tableName;
this.customMessage = customMessage;
}
///
/// Table name used in the method in the DAO
///
public string TableName
{
get { return tableName; }
}
///
/// Specific message that is logged with each log
///
public string CustomMessage
{
get { return customMessage; }
}
}

I used this attribute to decorate function calls that basically persisted data. Here is an example:

[TableAttribute("users", "USER HAS BEEN MODIFIED")]
public void PersistUser(User user)
{ //function body... }
I then modified my HistoryLogAdvice and made sure that method that was invoked had this attribute attached to it. This broke the recursive nature of the set up since I made sure not to decorate the method that was persisting the HistoryLog and HistoryLogParam objects. Here is how it was done:

public void AfterReturning(object returnVal, MethodInfo method, object[] args, object target)
{
HistoryLog log = new HistoryLog();
object[] attributes = method.GetCustomAttributes(false);
foreach (Attribute attribute in attributes)
{
if (attribute is TableAttribute)
{
TableAttribute tableAttribute = (TableAttribute)attribute;
log.TableName = tableAttribute.TableName;;
log.Message = tableAttribute.CustomMessage;;
break;
}
}
}


The next step in the process was to assign values to HistoryLog and HistoryLogParam objects in the advice.


log.MethodName = method.Name;
if (args != null)
{
log.ArgumentCount = args.Length;
}

//access to user in the current thread
string user = Thread.CurrentPrincipal.Identity.Name.ToString();

User userObj = new User();
userObj.Username = user;

log.User = userObj;

if (args != null)
{
foreach (object arg in args)
{
HistoryLogParam param = new HistoryLogParam();
StringBuilder sb = new StringBuilder();

if (arg is BaseModel)
{
//should create a utiliy class to handle this operation back and fourth
XmlSerializer xs = new XmlSerializer(arg.GetType());
MemoryStream memoryStream = new MemoryStream();
XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);
xs.Serialize(xmlTextWriter, arg);
memoryStream = (MemoryStream)xmlTextWriter.BaseStream;
string XmlizedString = UTFUtil.UTF8ByteArrayToString(memoryStream.ToArray());
XmlizedString = XmlizedString.Remove(0, 1);
sb.Append(XmlizedString);
}
else
{
sb.Append(Convert.ToString(arg));
}
param.ParamType = arg.GetType().FullName;
param.ParentLog = log;
param.SerializedValue = sb.ToString();
log.LogParams.Add(param);
}
}
//insert the log into database
appDao.HistoryLog(log);



Since we have access to the invoked methods arguments, I looped through each argument and looked for a specific type. If one of the arguments is a domain object such as user, I converted the object to its XML using XmlSerializer. I then wrote the output of the xml serialization process as part of the audit log to the database. This at least allowed me to view the state of the data when the actual action was taken on the data. If the argument is not a domain object (primitive types etc.), it gets converted to string and gets logged as it is.

You can easily construct the objects back reading the arguments and the xml.

There are certainly restrictions on this approach such as not knowing the state of the object before the action was taken. You can certainly change the advice type to IAroundAdvice, log the state of the arguments before the actual method invocation (by loading them from the database) and add this method after the proceed call. That would give you the state before and after each user activity.

This solution was pretty quick to implement and I think it is a neat solution. By adding a simple attribute to your method calls you would be providing auditing capability on the method invocation.

Needless to say with any auditing there are performance hits to consider. This approach is no different. There is extra write at the database and xml serialization also takes time. Use it at your own risk. It worked great for my project!

Comments