Login or Sign Up to become a member!
LessThanDot Sit Logo

LessThanDot

Web Developer

Less Than Dot is a community of passionate IT professionals and enthusiasts dedicated to sharing technical knowledge, experience, and assistance. Inside you will find reference materials, interesting technical discussions, and expert tips and commentary. Once you register for an account you will have immediate access to the forums and all past articles and commentaries.

LTD Social Sitings

Lessthandot twitter Lessthandot Linkedin Lessthandot friendfeed Lessthandot facebook Lessthandot rss

Note: Watch for social icons on posts by your favorite authors to follow their postings on these and other social sites.

Your profile

    Search

    XML Feeds

    Google Ads

    « Export HTML table to Excel with grid linesVulnerability in IIS Could Allow Elevation of Privilege »
    comments

    A friend of mine pointed me in the direction of a cool library for Aspect Oriented Programming called PostSharp the other day. I'd read about AOP in the past, but always had so much going on that it slipped my mind to look further into it. But, his excitement rubbed off on me and it wasn't long before I thought of a way I could use it. I'd been wanting to set up logging on a site I've been working on so that I could collect data for a while and report on it to see which controller methods are running the slowest. Not a permanent solution, just a way to gather data for a while to analyze it and identify any pain points I may have missed. Using PostSharp I can do it without littering too much logging code throughout the controllers themselves (all they would need is an attribute).

    First thing we need to do is create a table to log to. Something like this:

    1. CREATE TABLE  `my_site`.`LOG` (
    2.   `ID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    3.   `DATE` DATETIME NOT NULL,
    4.   `Thread` VARCHAR(32) NOT NULL,
    5.   `Context` VARCHAR(10) NOT NULL,
    6.   `Level` VARCHAR(10) NOT NULL,
    7.   `Logger` VARCHAR(512) NOT NULL,
    8.   `Method` VARCHAR(200) DEFAULT NULL,
    9.   `Parameters` VARCHAR(8000) DEFAULT NULL,
    10.   `Message` VARCHAR(1000) NOT NULL,
    11.   `Exception` VARCHAR(4000) DEFAULT NULL,
    12.   `ExecutionTime` DECIMAL(14,4) DEFAULT NULL,
    13.   PRIMARY KEY  (`ID`),
    14.   INDEX `ix_log_level` (`Level`),
    15.   INDEX `ix_log_executiontime` (`ExecutionTime`)
    16. ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    The only interesting thing here is the "ExecutionTime" column. I added this because in this case I am logging to MySQL, and MySQL doesn't store the millisecond portion of Date/Times. Seems it would be easier anyway to just log the time rather than try to connect start and finish entries (you could also do it in a single entry, as shown here). The reason I didn't do this is because I wanted to be able to split the table into 3 (start, finish, and exception entries) to get as good an idea as I could what is happening at any given time.

    Another thing to note here is that I added a column for parameter name/value combinations and the method name. log4net has a built-in conversion pattern for determining the method name, but it will not work for me because I plan to wrap log4net in a separate static helper class, in case I ever want to change the logging solution behind the scenes. I also read on log4net's Pattern Layout docs that getting any kind of information about the caller from log4net is very slow, because it generates a call stack to read the information from. That is one hell of a warning, they might as well just put police tape around those methods. So I will take my chances getting this info from PostSharp!

    Next is to configure log4net. Added an xml file called log4net.config to the top-level directory in the project. Something like this ought to do:

    1. <?xml version="1.0" encoding="utf-8" ?>
    2. <log4net>
    3.   <!-- How to set up secondary appender (bufferless) for Exceptions only? -->
    4.   <!-- Log4Net Appender Settings-->
    5.   <root>
    6.     <level value="All" />
    7.     <appender-ref ref="ADONetAppender" />
    8.   </root>
    9.   <appender name="ADONetAppender" type="log4net.Appender.ADONetAppender">
    10.     <bufferSize value="10"/>
    11.     <lossy value="false"/>
    12.     <connectionType value="MySql.Data.MySqlClient.MySqlConnection, MySql.Data"/>
    13.     <connectionString value="Server=999.99.443.206;Database=my_site; Uid=someuser;Pwd=welcome1;"/>
    14.     <commandText value="INSERT INTO Log (Date,Thread,Level,Logger,Message,Method,Parameters,Exception,Context,ExecutionTime) VALUES (?log_date, ?thread, ?log_level, ?logger, ?message, ?method_name, ?parameters, ?exception, ?context, ?execution_time)"/>
    15.     <parameter>
    16.       <parameterName value="log_date"/>
    17.       <dbType value="DateTime"/>
    18.       <layout type="log4net.Layout.RawTimeStampLayout"/>
    19.     </parameter>
    20.     <parameter>
    21.       <parameterName value="thread"/>
    22.       <dbType value="String"/>
    23.       <size value="32"/>
    24.       <layout type="log4net.Layout.PatternLayout">
    25.         <conversionPattern value="%t"/>
    26.       </layout>
    27.     </parameter>
    28.     <parameter>
    29.       <parameterName value="log_level"/>
    30.       <dbType value="String"/>
    31.       <size value="512"/>
    32.       <layout type="log4net.Layout.PatternLayout">
    33.         <conversionPattern value="%p"/>
    34.       </layout>
    35.     </parameter>
    36.     <parameter>
    37.       <parameterName value="context"/>
    38.       <dbType value="String"/>
    39.       <size value="512"/>
    40.       <layout type="log4net.Layout.PatternLayout">
    41.         <conversionPattern value="%x"/>
    42.       </layout>
    43.     </parameter>
    44.     <parameter>
    45.       <parameterName value="logger"/>
    46.       <dbType value="String"/>
    47.       <size value="512"/>
    48.       <layout type="log4net.Layout.PatternLayout">
    49.         <conversionPattern value="%c"/>
    50.       </layout>
    51.     </parameter>
    52.     <parameter>
    53.       <parameterName value="message"/>
    54.       <dbType value="String"/>
    55.       <size value="1000"/>
    56.       <layout type="log4net.Layout.PatternLayout">
    57.         <conversionPattern value="%m"/>
    58.       </layout>
    59.     </parameter>
    60.     <parameter>
    61.       <parameterName value="exception"/>
    62.       <dbType value="String"/>
    63.       <size value="4000"/>
    64.       <layout type="log4net.Layout.ExceptionLayout"/>
    65.     </parameter>
    66.     <parameter>
    67.       <parameterName value="method_name"/>
    68.       <dbType value="String"/>
    69.       <size value="200"/>
    70.       <layout type="log4net.Layout.PatternLayout">
    71.         <conversionPattern value="%property{method_name}"/>
    72.       </layout>
    73.     </parameter>
    74.     <parameter>
    75.       <parameterName value="execution_time"/>
    76.       <dbType value="Decimal"/>
    77.       <layout type="log4net.Layout.PatternLayout">
    78.         <conversionPattern value="%property{execution_time}"/>
    79.       </layout>
    80.     </parameter>
    81.     <parameter>
    82.       <parameterName value="parameters"/>
    83.       <dbType value="String"/>
    84.       <layout type="log4net.Layout.PatternLayout">
    85.         <conversionPattern value="%property{parameters}"/>
    86.       </layout>
    87.     </parameter>
    88.   </appender>
    89. </log4net>

    Nothing really special there, except for the additional parameters we added that I didn't see on most of the vanilla demos. One thing to note is the conversionPattern we used for the custom properties, "%property{PROPERTY_NAME}" as it can be very handy if you want to set custom parameters. There's also a special "ErrorLog" that writes to a flat file without using a buffer, for errors only. This is so that if there is a fatal error in the application, the exceptions leading up to it are not lost. Onward. Next thing we need to do is ensure that log4net is configured when we start up the application. There are two ways to do this:

    I first used AssemblyInfo.cs like so:

    1. [assembly: log4net.Config.XmlConfigurator(ConfigFile = "log4net.config", Watch = true)]

    But found I could also use the Application_start method like this:

    1. protected void Application_Start()
    2. {
    3.     RegisterRoutes(RouteTable.Routes);
    4.     log4net.Config.XmlConfigurator.Configure(new System.IO.FileInfo(HttpContext.Current.Server.MapPath("log4net.config")));
    5. }

    I have not yet decided which I like better, but I lean towards the second method because the first just feels a bit dirty. So feel free to pick the one you like best.

    Now that this is done, the fun can begin. First thing I did was set up a little helper class, so that I don't have calls to log4net all over the place. Besides returning a log4net.ILog to be used in writing entries, this class will have a few methods to use to Add/Remove parameters from the ThreadContext's Properties and NDC Stack. This class will look like this:

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Web;
    5. using log4net;
    6.  
    7. [System.Diagnostics.DebuggerStepThrough]
    8. public static class Logging
    9. {
    10.     /// <summary>Log an Informational Message</summary>
    11.     public static void Info(String message)
    12.     {
    13.         LogManager.GetLogger("root").Info(message);
    14.     }
    15.  
    16.     /// <summary>Log an Error Message</summary>
    17.     public static void Error(String message, Exception ex)
    18.     {
    19.         //for errors, use the ErrorLog that will write to a flat file (bufferless) rather than database (buffered)
    20.         LogManager.GetLogger("ErrorLogger").Error(message, ex);
    21.     }
    22.  
    23.     /// <summary>Insert a parameter to MDC</summary>
    24.     public static void SetParameter(String name, String value)
    25.     {
    26.         ThreadContext.Properties[name] = value;
    27.     }
    28.  
    29.     /// <summary>Remove parameter from MDC</summary>
    30.     public static void RemoveParameter(String name)
    31.     {
    32.         ThreadContext.Properties.Remove(name);
    33.     }
    34.  
    35.     /// <summary>Add another context to NDC</summary>
    36.     public static void PushContext(Object obj)
    37.     {
    38.         ThreadContext.Stacks["NDC"].Push(obj.ToString());
    39.     }
    40.  
    41.     /// <summary>Clear all contexts from NDC</summary>
    42.     public static void ClearContext()
    43.     {
    44.         ThreadContext.Stacks["NDC"].Clear();
    45.     }
    46.  
    47.     /// <summary>Remove top context from NDC</summary>
    48.     public static void PopContext()
    49.     {
    50.         ThreadContext.Stacks["NDC"].Pop();
    51.     }
    52.  
    53.     /// <summary>Set Execution time parameter (null for zero)</summary>
    54.     public static void SetExecutionTime(DateTime? current)
    55.     {
    56.         Double execution_time_milliseconds = 0;
    57.         if (current != null)
    58.         {
    59.             DateTime start;
    60.             if (DateTime.TryParse(ThreadContext.Properties["start_time"].ToString(), out start))
    61.             {
    62.                 Int64 execution_time_ticks = (current.Value - start).Ticks;
    63.                 execution_time_milliseconds = (execution_time_ticks * 1.00 / TimeSpan.TicksPerMillisecond);
    64.             }
    65.         }
    66.         ThreadContext.Properties["execution_time"] = execution_time_milliseconds;
    67.     }
    68.  
    69.     /// <summary>Store context-specific start time for sharing across methods (within a thread)</summary>
    70.     public static void SetStartTime()
    71.     {
    72.         ThreadContext.Properties["start_time"] = DateTime.Now.ToString();
    73.     }
    74. }

    Ok so now we know how we are going to do the logging. Now, time to go through and add calls to this logging code throughout our application right? Not exactly. Lets take a look how post sharp comes in. We'll want to extend the class "OnMethodBoundaryAspect" found in PostSharp.Laos. Use of this class will allow us to weave code into our application at compile time that will execute at various points during method execution (if the class has been tagged with the attribute we are about to create). For this exercise we are concerned with overriding the OnEntry, OnExit, and OnException methods. Their purposes ought to be pretty straight forward. I also added a method to take the method's parameter values and build it into a string like "[param1 = A][param2 = B]". The code for this looks like so:

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using PostSharp.Laos;
    5. using System.Collections;
    6.  
    7. [Serializable]
    8. [System.Diagnostics.DebuggerStepThrough]
    9. public class LoggableAttribute : OnMethodBoundaryAspect
    10. {
    11.     public override void OnEntry(MethodExecutionEventArgs event_args)
    12.     {
    13.         Logging.SetStartTime();
    14.         Logging.SetExecutionTime(null);
    15.         Logging.SetParameter("method_name", event_args.Method.Name);
    16.         Logging.SetParameter("class_name", event_args.Instance.GetType().ToString());
    17.  
    18.         //for analysis, we want to be able to identify individual executions
    19.         event_args.MethodExecutionTag = Guid.NewGuid();
    20.  
    21.         Logging.PushContext(event_args.MethodExecutionTag);
    22.         Logging.SetParameter("parameters", ParametersToString(event_args));
    23.  
    24.         Logging.Info("Called " + event_args.Method);
    25.     }
    26.  
    27.     public override void OnExit(MethodExecutionEventArgs event_args)
    28.     {
    29.         Logging.SetExecutionTime(DateTime.Now);
    30.         Logging.SetParameter("parameters", ParametersToString(event_args));
    31.         Logging.SetParameter("method_name", event_args.Method.Name);
    32.         Logging.SetParameter("class_name", event_args.Instance.GetType().ToString());
    33.        
    34.         Logging.Info("Finished " + event_args.Method);
    35.  
    36.         Logging.PopContext();
    37.     }
    38.  
    39.     public override void OnException(MethodExecutionEventArgs event_args)
    40.     {
    41.         Logging.SetExecutionTime(DateTime.Now);
    42.         Logging.SetParameter("parameters", ParametersToString(event_args));
    43.         Logging.SetParameter("method_name", event_args.Method.Name);
    44.         Logging.SetParameter("class_name", event_args.Instance.GetType().ToString());
    45.        
    46.         Logging.Error("Error Encountered in " + event_args.Method, event_args.Exception);
    47.     }
    48.  
    49.     //helpers
    50.     private static String ParametersToString(MethodExecutionEventArgs event_args)
    51.     {
    52.         String output = "";
    53.         if (event_args.GetReadOnlyArgumentArray() != null)
    54.         {
    55.             for (int i = 0; i < event_args.GetReadOnlyArgumentArray().Length; i++)
    56.             {
    57.                 output += String.Format("[{0} = {1}]", event_args.Method.GetParameters()[i].Name, event_args.GetReadOnlyArgumentArray()[i]);
    58.             }
    59.         }
    60.         return output;
    61.     }
    62. }

    Pretty simple, considering what it does. Now that we've gone through all this effort to set things up, we can see where the magic happens. It seems like a lot of work, but this is where we get the payoff. Find a controller in your project, such as the default home controller. And just add your attribute to it:

    1. [HandleError]
    2. [Loggable]
    3. public class HomeController : Controller
    4. {
    5.     public ActionResult Index()
    6.     {
    7.         ViewData["Title"] = "Page Title";
    8.         ViewData["Message"] = "Hey LessThanDotters!";
    9.  
    10.         return View();
    11.     }
    12. }

    Now, the "[Loggable]" attribute is all that you need to add to any class that you want logging to take place on the three method boundaries that we wrote code for (there may be some limitations, but I'm not aware of them yet). You can add it on a method-by-method basis as well. If you want to stop logging a certain class/method, just remove the attribute. Its' really that easy.

    I'm sure I will find some problems with this approach eventually (first try and all), and I will update with those as I find them. Or if anything is immediately apparent, please point it out in the comments!

    Sorry this post was so heavy on the code, I just thought it was really cool and wanted to share. Hopefully it gives you your own ideas for ways to use the PostSharp library.

    Have fun!

    About the Author

    User bio imageAlex is a .net and SQL Server developer from southeastern PA, where he lives with a lovely wife and a veritable smorgasbord of pets. He recently completed a masters degree in Software Engineering from Penn State. He loves mountain biking, open source software, home brewing, Syracuse basketball, and the mono runtime.
    Social SitingsTwitterLinkedInHomePageLTD RSS Feed
    10901 views
    InstapaperVote on HN

    12 comments

    Comment from: SQLDenis [Member] Email
    *****
    SQLDenis I think the IP address 999.99.443.206 is wrong :-)

    Just kidding, great post, I will have to try it out...why did you not use PostgreSQL it has the timestamp dataype which goes down all the way to 1 microsecond
    06/24/09 @ 06:52
    Comment from: Alex Ullrich [Member] Email
    Alex Ullrich Thanks Denis. I have a free MySQL database with my web host, figured I'd use it. I've actually gotten quite fond of it (especially ON DUPLICATE KEY UPDATE), but it'll never take SQL Server's place in my heart :)

    I've never used PostgreSQL but I suppose I should give it a look one of these days.
    06/24/09 @ 06:58
    Comment from: David [Visitor] · http://www.loggershop.co.uk
    ****-
    David When I made games using PHP and mysql I would store milliseconds as a separate column, then string that together with DATETIME to give the user his score. I would order these separately on the page using a few IF statements so it looked right.
    06/25/09 @ 00:48
    Comment from: Alex Ullrich [Member] Email
    Alex Ullrich I saw this on the twitters today: http://code.google.com/p/postsharp-user-plugins/wiki/Log4PostSharp

    Probably a better alternative in most cases (not sure for this one, since I needed to manually calculate execution time within aspect code)
    07/20/09 @ 11:27
    Comment from: Jon R [Visitor]
    *****
    Jon R Brill :-)

    One point though. If I put the Loggable attribute on my MVC controller base class, I get:

    Called Void OnActionExecuting(System.Web.Mvc.ActionExecutingContext)

    rather than the Action method name...

    Any ideas why this might be?

    Cheers,
    Jon
    09/29/09 @ 15:26
    Andrei Ignat do you use log4postsharp ?
    05/02/10 @ 07:27
    Comment from: Alex Ullrich [Member] Email
    Alex Ullrich Andrei, I haven't used log4postsharp, but it does look very promising. If I need this again, I plan to use it (assuming I can get it running with PostSharp 1.5 / mono)

    Jon, sorry I didn't see this comment but I'm not sure what could cause your problem with the base controller. Take it you weren't overriding OnActionExecuting in your class ;) If I get a chance I'll look into this.
    05/13/10 @ 15:02
    Comment from: Anil [Visitor] Email
    Anil Hello Alex,
    Thanks for such a grate example. I have implemented your code in my application and it works well, one thing though, it is taking much time to insert record in db as around 2-3 thousands records enter during login process and more for other events. Will you please tell me how can i make it faster. Actually I m implementing it in MVC. My problem is i want to create a log of what actions are slowing us down for the whole site and let those events build up for 2-3 days. Then group by action, and get an average time to process, and see what the major issues are in the application. Please help
    me on this
    06/21/10 @ 04:04
    Comment from: Alex Ullrich [Member] Email
    Alex Ullrich Hmm, I would start by trying to increase the buffer size log4net uses (this will minimize round trips to the database)
    10/05/10 @ 08:52
    Comment from: Saeed Neamati [Visitor] · http://www.thoughtresults.com
    Saeed Neamati What I see here, is tons of logging code lines. This I don't think is a good practice.
    12/29/11 @ 01:51
    Comment from: Alex Ullrich [Member] Email
    Alex Ullrich I think you may be missing the point Saeed. It seems like a lot of code (though not a lot more than it'd take to get something you can explicitly call) but it saves you from writing even a single method call in places you need logging. It also makes it easy to implement wholesale changes in your logging behavior without needing to change method signatures all over the place. In large systems, this benefit is not trivial.
    12/29/11 @ 03:36
    Comment from: James [Visitor] Email
    James Great tutorial. Curious if you have an updated code base for the latest release? Code is throwing errors for latest build of PostSharp.
    11/21/12 @ 08:57

    Leave a comment


    Your email address will not be revealed on this site.

    To mislead the spambots.

    Your URL will be displayed.
    (Line breaks become <br />)
    (Name, email & website)
    (Allow users to contact you through a message form (your email will not be revealed.)