Skip to content

Logging Practices for .NET

Shawn South edited this page Feb 19, 2014 · 2 revisions

This article is an extension of Logging Practices. Please ensure that you are familiar with the parent article as well.

All .NET applications should be using both

  • ELMAH to capture, record and notify staff of any unhandled Exceptions, and
  • NLog (via Common.Logging - a wrapper framework that allows us to seamlessly switch between logging engines without needing to modify code) for all other log output.

From the coding perspective

Unhandled Exceptions are recorded automatically by ELMAH. All that is required on the development end is to provide an appropriate customError configuration so that the user is presented with a friendly error message.

For all other logging output, the developer should call the appropriate method of the ILog object:

log.Debug("This is my debug message");
log.Warn("This is a {0} message, using String.Format-like syntax.", "warning");

TIP:

One benefit of using the Common.Logging framework is that it includes a syntax for eliminating unnecessary calls to the logging engine. The following code will not invoke the Debug() method (and thus not incur the memory overhead of doing so) unless the log level is configured to provide Debug output:

log.Debug(m => m("This is my debug message"));

ELMAH

ELMAH is a standard Exception management and logging handler in wide use around the industry. The project site can be found at https://code.google.com/p/elmah/ (documentation). In its most basic, default configuration, ELMAH records all unhandled Exceptions and provides an interface in which to view them.

Installation note:

Install the package called ELMAH, not the ELMAH Core Library (no config); the former will add all the necessary settings to the Web.config file. If you're using ASP.NET MVC there is also an ELMAH package specifically for that framework.

Configuring ELMAH

The default configuration stores the ELMAH log in memory. Be aware that this means log entries:

  • will not persist accross application restarts.
  • can only be viewed for the server on which they were logged. (Not appropriate for clusters.)

If you decide to continue using the memory log, increasing the default log size is reccommended:

  <elmah>
    ...
    <errorLog type="Elmah.MemoryErrorLog, Elmah" size="300" />
    ...
  </elmah>

Applications deployed to QA or Production should be configured to both write output to the common Elmah database and send e-mail notifications, since we are only using it to report unhandled Exceptions:

  ...
  <elmah>
    ...
    <errorLog	type="Elmah.SqlErrorLog, Elmah" 
				connectionStringName="Elmah.Sql" 
				applicationName="(see notes)"
	/>
	<!-- NOTE: The from address is required, and MUST match the account configured in Smtp.config -->
	<errorMail  to="sample.recipient@bellevuecollege.edu"
    	        from="from.address@bellevuecollege.edu"
        	    subject="Application threw an unhandled Exception"
            	async="false"
	            useSsl="true"
	/>
    ...
  </elmah>
  ...

ApplicationName and ApplicationNameQA are recommended for the applicationName attribute.

Notice that the SqlErrorLog references a connectionStringName:

  ...
  <connectionStrings>
    ...
    <add name="Elmah.Sql" connectionString="data source=SERVER_NAME;initial catalog=Elmah;integrated security=True;"/>
    ...
  </connectionStrings>
  ...

If this is a new deployment, make sure the application pool identity has read/write access to the Elmah database.

We also want to filter out errors we have little-to-no control over, so that e-mail recipients don't get flooded with messages they can't do anything about. The following filter rules tell ELMAH not to send e-mail for any HTTP 404 errors or Exceptions caused by bots crawling the site:

  ...
  <elmah>
    ...
    <errorFilter>
        <test>
          <and>
            <or>
              <equal binding="HttpStatusCode" value="404" type="Int32" />
              <regex binding="Context.Request.UserAgent" pattern="googlebot|exabot|msnbot|nutch|bingbot|blekkobot|yandex|robot" />
            </or>
            <regex binding="FilterSourceType.Name" pattern="mail" />
           </and>
        </test>
    </errorFilter>
    ...
  </elmah>
  ...

For more information about, and examples of, filtering ELMAH output see

Security

For security reasons, ELMAH is configured to initially not allow access to the log viewer from remote machines. Once the application is deployed, we want to allow access to support personnel:

  ...
  <elmah>
    ...
	<!--
		See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for 
		more information on remote access and securing ELMAH.
	-->
	<security allowRemoteAccess="true" />
    ...
  </elmah>
  ...
  <location path="elmah.axd" inheritInChildApplications="false">
	...
	<system.web>
		<!-- 
			See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for 
			more information on using ASP.NET authorization securing ELMAH.
		-->
		<authorization>
			<allow roles="AD_GROUP_NAME" />
			<deny users="*" />  
		</authorization>
	</system.web>
	...
  </location>
  ...

For more details, see http://code.google.com/p/elmah/wiki/SecuringErrorLogPages

Common.Logging

If you used NuGet to add Common.Logging and NLog to your project, everything should already be configured automatically. For convenience, here are some commonly-referenced documents with regards to Common.Logging configuration:

Configure for NLog

The following configuration will use an external NLog configuration file from the _configSource folder in the project. NOTE that the factoryAdapter node specifies NLog20 - this may need to be manually edited as the NuGet installer sometimes configures the project for NLog10.

If you want to specify the nlog configuration in the Web.config itself (not recommended), use a configType of INLINE (see the documentation linked above for more detail).

  ...
  <common>
    <logging>
      <factoryAdapter type="Common.Logging.NLog.NLogLoggerFactoryAdapter, Common.Logging.NLog20">
        <arg key="configType" value="FILE" />
        <arg key="configFile" value="~/_configSource/NLog.config" />
      </factoryAdapter>
    </logging>
  </common>
  ...

Configure for ELMAH (obsolete)

Applications should be configured to use NLog for logging (see above). This setting is only provided for historical reference.

  ...
  <common>
    <logging>
      <factoryAdapter type="Common.Logging.Elmah.ElmahLoggerFactoryAdapter, Common.Logging.Elmah">
        <arg key="MinLevel" value="All|Trace|Debug|Info|Warn|Error|Fatal|Off" />
      </factoryAdapter>
    </logging>
  </common>
  ...

NLog

NLog is a standard logging framework based loosely on Log4Net (which, in turn, is based on Log4j) but with a simpler syntax.

The recommended means of using NLog is through the Common.Logging abstraction framework and configuring with an external file. The common practice is to name this file NLog.config and place it in the _configSource folder along with other external config files that hold college-specific settings.

An NLog.config file has the following basic structure (more detail can be found in the configuration documentation):

<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <targets>
    <target name="target1" ... />
    <target name="target2">
      <!-- target parameters, etc. -->
    </target>
  </targets>

  <rules>
    <logger name="*" minlevel="Info" writeTo="target1" />
  </rules>
</nlog>

(TIP: You can omit the xmlns, etc. attriubutes of the nlog node, but including these provides Intellisense when editing in Visual Studio.)

targets

Each target defines a destination that NLog can write to. A number of targets are supported by NLog (including Database, Windows EventLog, File, etc.) or you can even write your own.

rules

Each log entry invoked from within the code will be written out to the target specified in each rule that matches.

Formatting log output

NLog uses an inline templating system of Layout Renderers - which are assigned to a Target's layout - to define what actually gets written to the log. A number of Layout Renderers are supported, but some of the most common include:

${message}

The message specified in the code which made the call to write out to the log.

${level}

The level of the log entry (e.g. Fatal, Error, Warn, Info, Debug, Trace)

${logger}

The name of the logger used to to log the message. Unless otherwise specified (in the code) this defaults to the class wherein the log event was invoked.

${date}

The current date and time.

Many Layout Renderers also have formatting, etc. options. For example, a UTC datetime can be specified thusly:

${date:universalTime=true}

Variables and custom layouts

NLog also supports variables, which can be used to create custom layouts that can then be used in the same way standard Layout Renderers are. See the samples below for examples of how variables can be used.

Sample NLog.config targets

The following configuration snippets demonstrate common target configurations used in our applications.

Sample NLog database target

This example demonstrates writing to the standard ELMAH database, which is our current standard so that all application logs are stored in the same place.

  ...
  <variable name="applicationName" value="SampleName"/>

  <!-- Database settings -->
  <variable name="dbServer" value="SAMPLE_SERVER_HOST"/>
  <variable name="dbName" value="Elmah"/>
  <variable name="dbConnection" value="data source=${dbServer};initial catalog=${dbName};integrated security=SSPI;persist security info=False"/>

  <targets>
    <!-- NOTE: We need to force generation of a GUID in the command because SQL doesn't seem to be able to convert from a string -->
    <target name="elmahDatabase" xsi:type="Database"
            dbProvider="mssql"
            connectionString="${dbConnection}"
            commandText="DECLARE @guid UNIQUEIDENTIFIER = NEWID(); EXEC ELMAH_LogError @guid, @Application, @Host, @Type, @Source, @Message, @User, @AllXml, @StatusCode, @TimeUtc"
    >
      <parameter name="@Application" layout="${applicationName}"/>
      <parameter name="@Host" layout="${machinename}"/>
      <parameter name="@Type" layout="${level}"/>
      <parameter name="@Source" layout="${logger}"/>
      <parameter name="@Message" layout="${message}"/>
      <parameter name="@User" layout="${windows-identity}"/>
      <parameter name="@AllXml">
        <layout><![CDATA[<error type="${level}" message="${message}" time="${date:format=o:universalTime=true}"/>]]></layout>
      </parameter>
      <parameter name="@StatusCode" layout="0"/> <!-- only used by ELMAH -->
      <!-- ${longdate} produces a 4-char millisecond, but SQL Server only accepts 3 for DATETIME, so we have to truncate -->
      <parameter name="@TimeUtc" layout="${longdate:universalTime=true:padding=23:fixedlength=true}"/>
    </target>
  </targets>
  ...

Sample NLog e-mail target

E-mail targets are generally used for notification alerts to support personnel - to alert them that something is wrong and needs attention. For that reason, they are typically configured with a rule that specifies only certain level entries are sent.

  ...
  <variable name="applicationName" value="SampleApplicationName"/>

  <!-- Email notification settings -->
  <variable name="smtpUsername" value="SampleUsername"/>
  <variable name="smtpPassword" value="SamplePassword"/>
  <variable name="fromAddress" value="${smtpUsername}@bellevuecollege.edu"/>
  <variable name="emailRecipients" value="sample.recipient@bellevuecollege.edu"/>

  <targets>
    <!-- NOTE: The from address is required, and MUST match the account configured in Smtp.config -->
    <target name="emailAlert" xsi:type="Mail"
            enableSsl="true"
            from="${fromAddress}"
            to="${emailRecipients}"
            subject="${applicationName} Error: ${logger}"
            body="${message}"
            useSystemNetMailSettings="true"
      />
  </targets>
  ...

The useSystemNetMailSettings attribute tells NLog to use the SMTP settings specified in the standard .NET mailSettings/smtp configuration node. This allows us to configure SMTP authentication for the current security context (i.e. under the IIS application pool identity) rather than explicitly supplying credentials, which would be insecure.

See Smtp.config.