read

I’ve used log4net in several projects to help with logging and debugging, but a unique requirement came up where I wanted to log not just human readable strings but to use the logging capability to store information about the actions users are performing on a MVC4 application.


I wanted to track the User, the Controller and the Action the user invoked, how long it took and if there was any error that occurred. So I created a custom class to represent this called ActionLoggerInfo and an custom ActionFilterAttribute which I applied as a global filter which called log4net to log the object.


public class ActionLoggerInfo
{
public string Controller { get; set; }
public string Action { get; set; }
public string User { get; set; }
public long ActionDurationInMillis { get; set; }
public string ExceptionDetails { get; set; }

public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}


I logged actions from the filter using the standard Info method.


ILog _actionLogger = LogManager.GetLogger("ActionLogger");

. . .
var actionInfo = new ActionLoggerInfo()
{
// set values of action info object

};

_actionLogger.Info(actionInfo);


Quite importantly the log4net log methods (Debug, Info, Warn, ) accept message as object and not as a string, which allows for neat decoupling of serialization to happen via configurable layout patterns and converters.


Overriding the ToString method of my custom ActionLoggerInfo class got me some of the way because when the default converters were called, they would invoke the ToString override and I could serialize the object to Json, but what I really wanted was for the individual fields of the custom info object to be saved in seperate columns of a table so that I could cobble together some very simple usage reporting for the site. Since I was targetting a SQL environment having a message string that would have to be parsed for each logged row simply wouldn’t cut it.


I needed a way to map individual fields in the log to columns in an AdoNetAppender.


Out the box, log4net provides a bunch of conversion pattern names that can be used in the log pattern string to shape your log, written in printf style (for example %message). To solve the problem I needed to introduce a new named pattern to represent my custom object and then be able to address individual fields within it.


Adding a named Conversion Pattern


The route I took to do this was to create a custom PatternLayout class in order to introduce the new conversion pattern name %actionInfo for the ActionLoggerInfo class.


public class ActionLayoutPattern : PatternLayout
{
public ActionLayoutPattern()
{
AddConverter(new ConverterInfo
{
Name = "actionInfo",
Type = typeof(ActionConverter)
}
);
}
}


You’ll notice that there is no link to ActionLoggerInfo directly in this pattern, it only serves to establish the name "actionInfo" and the converter ActionConverter to be used to perform the conversion.


The Converter


The converter is responsible for handling conversion requests of a LoggingEvent when log4net calls it internally to render a given pattern name to the writer. The converter can then inspect whichever aspect of the LoggingEvent it needs to retrieve the value to write, including the MessageObject that was originally logged.


Since we’re passing the custom object in as the MessageObject we have everything that we need to inspect the object and pull out the fields that we want to log wh.


Patterns in log4net follow a %conversionPatternName{option} syntax. Where the conversionPatternName is used to identify which converter to use from the PatternLayout‘s list of converters, and the option in squiggly brackets allows you to pass additional information.


In the case of a custom object this gives us the mechanism we need to specify which field of an object we are wanting to render. eg. %actionInfo{controller}


The option in the %<conversionPatternName>{<option>} pattern syntax is extracted and mapped to ActionConvert.Option when the converter is invoked, so it is trivial to inspect it and return the field you want.


public class ActionConverter : PatternConverter
{
protected override void Convert(System.IO.TextWriter writer, object state)
{
if (state == null)
{
writer.Write(SystemInfo.NullText);
return;
}

var loggingEvent = state as LoggingEvent;
var actionInfo = loggingEvent.MessageObject as ActionLoggerInfo;

if (actionInfo == null)
{
writer.Write(SystemInfo.NullText);
}
else
{
switch(this.Option.ToLower())
{
case "controller" :
writer.Write(actionInfo.Controller);
break;
case "action" :
writer.Write(actionInfo.Action);
break;
case "duration" :
writer.Write(actionInfo.ActionDurationInMillis);
break;
case "user" :
writer.Write(actionInfo.User);
break;
case "exceptiondetails" :
writer.Write(actionInfo.ExceptionDetails);
break;
default :
writer.Write(SystemInfo.NullText);
break;
}
}
}
}


Using a bit of Reflection it would be easy to make a generic object converter that could reflect to extract the values properties of an object based on the option parameter, but for my purposes the switch statement was adequate


Creating the Log Table in the database


CREATE SCHEMA [Logs]

GO

CREATE TABLE [Logs].[Actions]
(
[Id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Date] [datetime] NOT NULL,
[Thread] [varchar] (255) NOT NULL,
[Level] [varchar] (50) NOT NULL,
[Logger] [varchar] (255) NOT NULL,
[Message] [varchar] (4000) NOT NULL,
[Exception] [varchar] (2000) NULL,
[Controller] [varchar] (2000) NULL,
[Action] [varchar] (2000) NULL,
[User] [varchar] (2000) NULL,
[Duration] [int] NULL
)


Easily done.


Configuring the AdoNetAppender


We have to modify the commandText of the appender to contain the new database fields and parameters for the custom object’s fields.


Each parameter is then specified explicitly with our custom PatternLayout (in this example MyApp.ActionLayoutPattern) and with a conversionPattern that uses the custom conversion pattern name (in this example %actionInfo) and the field {controller}.


      <parameter>
<parameterName value="@controller" />
<dbType value="String" />
<size value="2000" />
<layout type="MyApp.ActionLayoutPattern">
<conversionPattern value="%actionInfo{controller}" />
</layout>
</parameter>


Full appender config example :


  <log4net>
<appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
<bufferSize value="100" />
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<connectionString value="Data Source=(localdb)\Projects;Initial Catalog=LoggingDB;Integrated Security=true;" />
<commandText value="INSERT INTO [Logs].[Actions] ([Date],[Thread],[Level],[Logger],[Message],[Exception],[Controller],[Action],[User],[Duration]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception, @controller, @action, @user, @duration)" />
<parameter>
<parameterName value="@log_date" />
<dbType value="DateTime" />
<layout type="log4net.Layout.RawTimeStampLayout" />
</parameter>
<parameter>
<parameterName value="@thread" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%thread" />
</layout>
</parameter>
<parameter>
<parameterName value="@log_level" />
<dbType value="String" />
<size value="50" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%level" />
</layout>
</parameter>
<parameter>
<parameterName value="@logger" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%logger" />
</layout>
</parameter>
<parameter>
<parameterName value="@message" />
<dbType value="String" />
<size value="4000" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%message" />
</layout>
</parameter>
<parameter>
<parameterName value="@exception" />
<dbType value="String" />
<size value="2000" />
<layout type="log4net.Layout.ExceptionLayout" />
</parameter>
<parameter>
<parameterName value="@controller" />
<dbType value="String" />
<size value="2000" />
<layout type="Beam.Ui.Filters.ActionLayoutPattern">
<conversionPattern value="%actionInfo{controller}" />
</layout>
</parameter>
<parameter>
<parameterName value="@action" />
<dbType value="String" />
<size value="2000" />
<layout type="Beam.Ui.Filters.ActionLayoutPattern">
<conversionPattern value="%actionInfo{action}" />
</layout>
</parameter>
<parameter>
<parameterName value="@user" />
<dbType value="String" />
<size value="2000" />
<layout type="Beam.Ui.Filters.ActionLayoutPattern">
<conversionPattern value="%actionInfo{user}" />
</layout>
</parameter>
<parameter>
<parameterName value="@duration" />
<dbType value="Int32" />
<layout type="Beam.Ui.Filters.ActionLayoutPattern">
<conversionPattern value="%actionInfo{duration}" />
</layout>
</parameter>
</appender>



</log4net>


Great I hope that helps someone else in a similar position.


Related Articles


Here are the articles that helped me along the way…


Log4Net custom LayoutPattern - Dejan Fajfar (April 2011)


Creating your own Custom PatternLayout, PatternParser, and PatternConvertor with log4net - Scott Hanselman (Jul 2004)


log4net PatternString - House of Metnetsky ()

Blog Logo

Stephen James

JavaScript and Front-End Developer


Published

Proudly published with Hexo
Image

Stephen James' Code Blog

Back to Overview