views:

60

answers:

1

I'm trying to implement a custom log4net logger/logmanager so that I can add a new level. I also want to have the schema of the target table (I'm using an AdoNetAppender and SQL Server 2008) to have some optional fields; for instance, the level I'm adding is used to track usage, and one of the fields needs to be a Duration (to tell us how long something took). However, other levels (INFO, DEBUG, FATAL, etc.) will not have any value for Duration. So I need to be able to insert into the table with not all fields having values--preferably without having to reimplement all of the standard levels logging to pass a null value for the irrelevant fields. Ideally, log4net would automatically pass NULL for property values that it cannot find for a particular logging event.

The code in my custom logger is as follows:

public void Usage(TimeSpan? duration, DateTime? startTime, DateTime? endTime, string message)
    {
        if (IsUsageEnabled)
        {
            LoggingEvent loggingEvent = new LoggingEvent(GetType(), Logger.Repository, Logger.Name, _currentUsageLevel,
                message, null);
            if (startTime.HasValue)
            {
                loggingEvent.Properties["StartTime"] = startTime.Value;
            }
            else
            {
                loggingEvent.Properties["StartTime"] = DBNull.Value;
            }

            if (endTime.HasValue)
            {
                loggingEvent.Properties["EndTime"] = endTime.Value;
            }
            else
            {
                loggingEvent.Properties["EndTime"] = DBNull.Value;
            }

            if (duration.HasValue)
            {
                loggingEvent.Properties["Duration"] = duration.Value.TotalMilliseconds;
            }
            else
            {
                loggingEvent.Properties["Duration"] = DBNull.Value;
            }

            Logger.Log(loggingEvent);
        }
    }

(the assignments to DBNull.Value were added in hopes that that would help log4net pass nulls when the value was not passed, but I hope they can be removed).

The appender config is as follows:

<appender name="SQLAppender" type="log4net.Appender.AdoNetAppender">
  <bufferSize value="1" />
  <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  <connectionString value="my connection string (which actually gets replaced at runtime with a connection string retrieved from a config database)" />
  <commandText value="INSERT INTO MyTable ([Date],[Thread],[Level],[Logger],[Message],[Exception], [Login], [StartTime], [EndTime], [Duration]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception, @login, @start_time, @end_time, @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="4000" />
    <layout type="log4net.Layout.ExceptionLayout" />
  </parameter>
  <parameter>
    <parameterName value="@login" />
    <dbType value="String" />
    <size value="255" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%property{CurrentLogin}" />
    </layout>
  </parameter>
  <parameter>
    <parameterName value="@start_time" />
    <dbType value="DateTime" />
    <layout type="log4net.Layout.PatternLayout"
      value="%property{StartTime}" />
  </parameter>
  <parameter>
    <parameterName value="@end_time" />
    <dbType value="DateTime" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%property{EndTime}" />
    </layout>
  </parameter><parameter>
    <parameterName value="@duration" />
    <dbType value="Double" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%property{Duration}" />
    </layout>
  </parameter>
</appender>

When I try to log something with my custom level, it works ONLY if I pass in duration, starttime and endtime. For anything else--including default levels--it seems to fail silently, with no clues in the output window (although logging with default levels throws exceptions saying it can't convert String to DateTime or Double).

Any reason that anyone can see why this would fail silently like this?

A: 

I finally figured it out, after tracing through some log4net source. The problem was that I was using a PatternLayout for rendering the custom parameters. This layout coerces null to the string "(null)", which causes problems if you're trying to insert it into, say, a nullable datetime field in the table. The solution is simple: instead of using the PatternLayout layout, use log4net.Layout.RawPropertyLayout. This will pass a null property as null, which is what you want.