EPiServer, Episerver 11, log4net, logging

Logging might have negative impact to site performance

So you have implemented a web site (with Episerver most likely if you are reading this blog ;P). Initial performance tests are done in the production environment and the site has gotten green to go live. The development didn’t stop to the ‘project’ but continues and new features are introduced. Depending on the case performance tests might be run for each release candidate or then not.

This post is not about how to do continuous development and all the best practices related to that. This post is a simple heads up about how logging might have a huge negative impact to your site performance.

I’ll use the Episerver MVC Alloy demo template to demonstrate how logging can have undesired effect to the web site performance. Alloy uses log4net for logging so if you Google for performance tuning then all those hits will apply (as long as the tip is actually correct).

Test machine (development machine)

  • Intel Xeon E3-1231 v3 @ 3.40 GHz (turbo 3.80 GHz)
    • (4 cores, 8 threads)
  • 16 GB

Tools

Episerver CMS

  • Episerver.CMS 11.3.3 (NuGet version)
    • update 202
    • all nuget.org NuGet updates applied (except json.net 11 as Epi is not yet compatible with it)
  • log4net 2.0.8
  • Alloy MVC site was created with the latest VS extension and without search
  • running the site in release mode without debugging in the tests

Log4net default appender configuration

<appender name="infoLogAppender" type="log4net.Appender.RollingFileAppender" >
 <file value="App_Data\logs\InfoEpiLog4NetApp.log" />
 <encoding value="utf-8" />
 <staticLogFileName value="true"/>
 <datePattern value=".yyyyMMdd.'log'" />
 <rollingStyle value="Date" />
 <threshold value="Info" />
 <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
 <appendToFile value="true" />
 <layout type="log4net.Layout.PatternLayout">
  <conversionPattern value="%date [%thread] %level %logger: %message%n" />
 </layout>
</appender>

<logger name="EpiLog4Net.Controllers" additivity="false">
 <level value="Info" />
 <appender-ref ref="infoLogAppender" />
</logger>

Log info and above messages from controllers to a separate file (StartPageController is the only one having logging).

Reference performance

Initially I didn’t plan to use JMeter so I don’t have reference performance for it without any added log messages. Initially I just used bombardier to test the start page of Alloy.

Bombarding http://localhost:10506 with 10000 requests using 25 connections
 10000 / 10000 [=========] 100.00% 24s
Done!
Statistics Avg     Stdev  Max
 Reqs/sec  491.69  790.98 15984.02
 Latency   61.20ms 9.95ms 252.24ms
 HTTP codes:
 1xx - 0, 2xx - 10000, 3xx - 0, 4xx - 0, 5xx - 0
 others - 0
 Throughput: 3.18MB/s

Adding log messages

To test how logging can affect the site performance I added two info level messages to StartPageController.cs index action. One to the start and another to inside the setting of edithints (it really doesn’t matter where in the code these log messages are as long as they are in the index action of the controller and no conditions when those are logged).

// log messages:
logger.Information("Start page index action called.");
logger.Information("Start page is the site real site start page, setting edit hints.");

So as you can see there is no expensive work done to create the actual log messages.

Performance with default log4net settings

Bombarding http://localhost:10506 with 10000 requests using 25 connections
 10000 / 10000 [==============] 100.00% 3m15s
Done!
Statistics Avg      Stdev    Max
 Reqs/sec  51.08    13.82    210.47
 Latency   488.87ms 120.16ms 1.42s
 HTTP codes:
 1xx - 0, 2xx - 10000, 3xx - 0, 4xx - 0, 5xx - 0
 others - 0
 Throughput: 407.39KB/s

Erm, what did just happen to the stats?

  • average request/s dropped from 491.69 to 51.08
  • throughput dropped from 3.18MB/s to 407.39KB/s
  • average latency dropped from 61.20ms to 488.87ms

Quick explanation: logging is synchronous, a write lock on the file is acquired per log message, log entry writen and lock released (See the remarks on FileAppender.MinimalLock). So under pressure the logging slows down the site performance because each message needs to wait for the write lock.

Use log4net ExclusiveLock

I modified the EPiServerLog.config (used to configure log4net) to use the ExclusiveLock (see the remarks).

// remove this line from infoLogAppender appender:
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />

When that line is removed the default ExclusiveLock is used.

Performance with ExclusiveLock

Bombarding http://localhost:10506 with 10000 requests using 25 connections
 10000 / 10000 [==================] 100.00% 24s
Done!
Statistics Avg     Stdev   Max
 Reqs/sec  538.13  721.72  9989.01
 Latency   60.04ms 11.03ms 315.30ms
 HTTP codes:
 1xx - 0, 2xx - 10000, 3xx - 0, 4xx - 0, 5xx - 0
 others - 0
 Throughput: 3.24MB/s

Yipee, back to the same ballpark in performance that we had before adding logging (yes, the performance looks even better than before but it is not something you should look at, single run was run and at the same time anything could have been done in Windows in the backgroud that was consuming resources).

So the question is that if there seems to be no performance penalty using ExclusiveLock then why isn’t it the default in Episerver log4net logging configuration? Well, because – there is no such thing as free lunch.

ExclusiveLock allows only one process to log to the file. The file is locked as long as the process exists that acquired the lock. So if you need to do something to the file from another process you can’t. I would ask why would you want to log from multiple processes to the same file – use centralized logging to collect the log files for example.

Another thing that doesn’t work with ExclusiveLock is IIS Web Garden feature by default unless you use unique log4net properties in constructing the file name so that is unique per IIS worker process. This same applies to Azure Web Apps – you need to make the log file names uniquer per instance (in case you are logging to your shared drive).

Use log4net BufferingForwardingAppender

What about using the combination of MinimalLocking and the log4net BufferingForwardingAppender?

Configuration

<appender name="infoLockingLogAppender" type="log4net.Appender.RollingFileAppender" >
 <file value="App_Data\logs\BufferedInfoEpiLog4NetApp.log" />
 <encoding value="utf-8" />
 <staticLogFileName value="true"/>
 <datePattern value=".yyyyMMdd.'log'" />
 <rollingStyle value="Date" />
 <threshold value="Info" />
 <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
 <appendToFile value="true" />
 <layout type="log4net.Layout.PatternLayout">
 <conversionPattern value="%date [%thread] %level %logger: %message%n" />
 </layout>
</appender>

<appender name="infoBufferingForwarder" type="log4net.Appender.BufferingForwardingAppender">
 <bufferSize value="512" />
 <lossy value="false" />
 <Fix value="268" /> <!-- Flags: Message, ThreadName, Exception -->
 <appender-ref ref="infoLockingLogAppender" />
</appender>

<logger name="EpiLog4Net.Controllers" additivity="false">
 <level value="Info" />
 <appender-ref ref="infoBufferingForwarder" />
</logger>

Performance with buffering

Bombarding http://localhost:10506 with 10000 requests using 25 connections
 10000 / 10000 [===============] 100.00% 24s
Done!
Statistics Avg     Stdev  Max
 Reqs/sec  449.35  563.75 13988.81
 Latency   61.93ms 8.77ms 240.23ms
 HTTP codes:
 1xx - 0, 2xx - 10000, 3xx - 0, 4xx - 0, 5xx - 0
 others - 0
 Throughput: 3.14MB/s

So we are also in the same ballpark with performance with the BufferingForwardingAppender BUT…

If you look at the statistics you might notice there is a difference in the average requests per second: 538 vs 449, compared to the ExclusiveLock. My guess is that it relates to the fact that when the buffer hits its configured limit the log entries are writen to the underlying file and the log messages coming in needs to wait for this task to complete (its still asynchronous) but just a guess. The real but here is that you can easily loose log messages if the application is shutdown. In most cases this is not acceptable, so for this reason I wouldn’t use this approach.

Side note: in theory you should be able to react to the application shutdown (weather caused by recycle, stopped from IIS or app recycle because of crash) and call the log4net flush and shutdown methods, like this:

log4net.LogManager.Flush(10000); // wait 10 seconds
log4net.LogManager.Shutdown();

Unfortunately I couldn’t get this to work. I kept loosing log entries. But then again as we should be using the Episerver logging abstraction this should be done by the logging implementation ;-P (it’s responsibility).

Conclusion

I would choose the ExclusiveLock option when there is need to log informative messages during normal execution, otherwise your site will be hit heavily with performance issues even with modest loads (see below JMeter results).

Its too easy to affect site performance with log4net by just using the default MinimalLock and then someone adds some informative logging to each page request, so now you know at least one way to workaround this.

Excersice for you is to find the proper actually working asynchronous file appender for log4net 😉 …and then let us all know about it (test it before you paste the first NuGet package hit).

And the JMeter results

So the bombardier tool is to hammer the site so I created a quick test plan for JMeter that loaded the start page with:

  • 100 users
  • ramp-up: 10 seconds
  • loop count: 10
    • 1000 request in total
  • no cache or cookie manager, loading only the page HTML and no other resources
  • used constant timer that added 1.5 seconds delay to each request (not hammering the site)

Default log4net configuration

  • Average: 1673 ms
  • Min: 53 ms
  • Max: 2641 ms
  • Throughput: 24.6 reqs/s
  • Elapsed: 42 s

ExclusiveLock log4net configuration

  • Average: 57 ms
  • Min: 37 ms
  • Max: 181 ms
  • Throughput: 41.7 reqs/s
  • Elapsed: 25 s

Buffering log4net configuration

  • Average: 58 ms
  • Min: 37 ms
  • Max: 175 ms
  • Throughput: 41.7 reqs/s
  • Elapsed: 25 s
EPiServer, Episerver 11, log4net, logging

No logs after upgrading to EPiServer.Logging.Log4Net v.2.2.1

Updated one of our customer solution with Episerver update 201, quick check on local dev, everything OK. Commit and push. Isn’t this the way we work? 😉

Noticed this thread “Logging stopped working after latest epi upgrade” on the Episerver forums and just assumed it was about another issue that I had previously experienced. Gave an answer to the post and just assumed that everything is going to be just fine.

Well as you can see from the forum thread – everything wasn’t OK. Checked our internal test and noticed that no logs since the update, ooops.

Episerver has changed their own implementation to use an ASP.NET PreApplicationStartMethodAttribute which target implementation calls log4net.LogManager.GetLogger(typeof(PreApplicationStartModule)); This doesn’t have a side effect in debug mode but in release mode seems that some assembly load order changes and perhaps something again calls log4net logging before it is configured and leads to situation that there will be no logs. Normally the configuration is done by the assembly attribute in EPiServer.Logging.Log4Net assembly.

Quick workaround is to use appsettings to configure log4net:

<appSettings>
<!-- add these entries to configure log4net -->
<add key="log4net.Config" value="EPiServerLog.config"/>
<add key="log4net.Config.Watch" value="True"/>
</appSettings>
Episerver 11

How to change Episerver logging framework?

So lets say you want for some reason to change the default logging framework (log4net, https://logging.apache.org/log4net/). Episerver has made this really easy and it is documented at the bottom of this page: https://world.episerver.com/documentation/developer-guides/CMS/logging/

Basically you need to implement two interfaces: ILogger and ILoggerFactory

After implementing the interfaces you need to decorate your assembly with LoggerFactoryAttribute and set the LoggerFactoryType to your ILoggerFactory implementation type.

Before using your implementation remember to remove the Episerver.Logging.Log4Net NuGet package (and log4net package too). “The Logging API currently only supports one logger type and the API uses the first factory type it can find when it scans assemblies.”

I wanted to try out this feature and checked that there was no existing NLog (http://nlog-project.org/) package in the Episerver NuGet feed (yes there is EPi.Libraries.Logging.NLog 3.0.0 , but it doesn’t replace the logging framework, you need to configure log4net to not log anything). So I gave it this a shot and implemented logging with NLog. Sources are in GitHub: Swapcode.Episerver.Logging.NLog.

I’ll also upload the NuGet for this to Episerver NuGet feed and add the link if my package is accepted.

*update 2018-02-06* I’ve updated the documentation on GitHub to include all information needed for setup and configuration.

Azure Websites, EPiServer 7.5, log4net

EPiServer, Azure Websites and simple logging with log4net

So you are an EPiServer developer and have build many fancy sites using EPiServer CMS. You are used to use the default log4net logging on your sites and have configured the logging to log to a certain location on disk. Now you are going to host your EPiServer site on Azure Websites but what about logging? You already know that with Azure Websites you basically don’t have access to the underlying virtual machines disks. You might know there exists the log4net appender for Azure Table and Blob storage. Unfortunately that can’t be used currently as we are stuck with log4net version 1.2.10 because of (current) EPiServer dependencies.

So the solution is to write your own Azure table or blob storage appender but wait does the Azure Websites infrastructure actually provide us a way to log to a file(s)? Yes it does. Short version you always have d:\home\ folder (environment variable %HOME% refers to this) which is mapped to your Azure Websites hosting virtual machine. Read the Operating System Functionality Available to Applications on Azure Websites or see the Q&A on social forum or the diagnostics logging documentation.

On the Azure management portal if you choose your Azure Websites site and navigate to the dashboard you can see on the right hand side (not using the preview dashboard) you can see your site FTP/FTPS login information. Use WinSCP or some other FTP/FTPS client to connect and browse the structure. If you navigate to the root you should see /data, /LogFiles and /site folders. LogFiles folder sounds a good place to place our web site logs too.

I will use log4net RollingFileAppender so my configuration looks like this for the appender:

<appender name="errorFileLogAppender" type="log4net.Appender.RollingFileAppender" >
	<!-- log to azure websites LogFiles folder -->
	<file value="..\..\LogFiles\EPiErrors.log" />
	<encoding value="utf-8" />
	<staticLogFileName value="true"/>
	<!-- roll files based on size -->
	<rollingStyle value="Size" />
	<!-- roll the file when the size reaches 50MB -->
	<maximumFileSize value="50MB" />
	<!-- keep at maximum 7 log files before deleting them -->
	<maxSizeRollBackups value="7" />
	<threshold value="Error" />
	<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
	<appendToFile value="true" />
	<layout type="log4net.Layout.PatternLayout">
		<conversionPattern value="%date [%thread] %level %logger: %message%n" />
	</layout>
</appender>

So I use relative path for the log file based on knowing how the structure is in Azure Websites.

All great case closed but wait.. what about if I scale my site to use more than one instances? I don’t want the situation where both instances of the web site try to log to the same file (you do remember that / you did read that all instances have the folders mapped from an UNC). So we need a dynamic way of defining the log file name to have separate log files per site instance. This is easily accomplished with log4net. So I changed my configuration a bit:

<file type="log4net.Util.PatternString" value="..\..\LogFiles\EPiErrors-%property{log4net:HostName}.log" />

Adding the type to the file element we can use log4net properties (%property{XXXX}). By default the log4net:HostName property is set to the name of machine on which the event was originally logged. So this solves the need to be able to dynamically define the log file name per machine and now I get separate files per machine when scaling out the site.

So is there a catch somewhere? Yes. If using the Free or Shared plan you have 1GB of disk which is shared with all your web sites on that plan. If using basic plan you have 10GB and with standard plan you have 50GB disk so with these plans you have more room to spend disk for logging. So keep in mind what plan you are using and how you should configure the rolling of log files.