Using Log4Net in ArcMap COM Extensions/Commands etc
Posted by Dave Bouwman | Posted in .NET, ArcGIS Devt, Utilities | Posted on 15-10-2009
5
I was building an edit task for ArcMap today, and got to a point where I had the geometry operations solid (using Test-Driven-Development for them) but when I got to iterating over sets of real data, I though it would be really handy to use a logger to kick out more detailed debug information. We use log4net in our web applications, so I thought I’d try dropping it in.
Turns out it took all of 5 minutes to get it working, so here’s the how-to (there’s some sample code at the end)
Get log4net
Get log4net from http://logging.apache.org/log4net/download.html – simple.
Add the Reference
Now that you have the log4net zip archive, copy the log4net.dll from the \log4net-1.2.10\bin\net\2.0\release folder in the zip file into your \lib folder. I always have a \lib folder for external assemblies that are used in a project. This is preferable to storing these in a central location because the \lib folder get’s stored in source control along with everything else that your project required. So – if you don’t have a lib folder, make one!
Now that it’s in the create a reference to log4net from your project in Visual Studio.
Configure It
The easiest way to use log4net is with the standard log file appender which just writes to a text file. In order to configure this, we need to edit the ArcMap.exe.config file (located in \ArcGIS\bin). Of course you can store the log in other places (databases etc) – just Google for the details.
By default, this configuration file is almost empty, so you’ll need to add in the configSections as well as the log4net section.
<configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"
requirePermission="false" /> </configSections>
…lower down in the file…
<log4net> <appender name="LogFileAppender" type="log4net.Appender.FileAppender"> <file value=".\\Logs\_log.txt" /> <appendToFile value="true" /> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline%exception" /> </layout> </appender> <root> <level value="ALL" /> <appender-ref ref="LogFileAppender" /> </root> </log4net>
Now we have to wire log4net into our code. In order to be useful, we’ll want to have access to the “logger” throughout our class, so we’ll make it a private member. Ideally we’d pass in the logger from a dependency injection framework, but we’ll keep things simple for this example (sometimes DI *is* overkill). Thus, we’ll setup the logger in the constructor of our class.
private log4net.ILog _logger;
/// <summary>
///Constructor.
/// </summary>
publicLRSEditTask()
{
log4net.Config.XmlConfigurator.Configure();
_logger = log4net.LogManager.GetLogger(typeof(LRSEditTask));
}
Logging
Would not be very much help if I did not show how to actually write to the log! Now that we have _logger, we can just pump data to it. There are a number of levels you use to send out data, depending on the severity. During development, I’ll use the Debug(<info>) method to see what’s going on inside the code, but you’ll also want to use the Warn, Error and Fatal methods when your code experiences errors. Using the different methods allows you to control what’s output to the log file via the configuration section. This way you can leave all that debug stuff in the code, but not have log files piling up on production systems.
In the sample code, I’m just pumping out examples of each type of log.
public void Activate(IEditor Editor, IEditTask oldTask) { _logger.Info("Task Activated."); } public void Deactivate() { _logger.Warn("Task Deactivated."); } public string Name { get { return "Log4Net Example Task"; } } public void OnDeleteSketch() { _logger.Fatal("On Delete Sketch Called"); } public void OnFinishSketch() { _logger.Error("On Finish Sketch Called"); }
In the log file you’ll see something like this…
2009-10-15 15:32:25,631 [1] INFO Log4NetDemoTask.LogEditTask [(null)] – Task Activated.
2009-10-15 15:32:29,205 [1] ERROR Log4NetDemoTask.LogEditTask [(null)] – On Finish Sketch Called
2009-10-15 15:32:33,003 [1] FATAL Log4NetDemoTask.LogEditTask [(null)] – On Delete Sketch Called
2009-10-15 15:32:38,182 [1] WARN Log4NetDemoTask.LogEditTask [(null)] – Task Deactivated.
Post Build Tasks
This is a subtle thing, but as far as I can tell, if you want your code to be able to “see” the ArcMap.exe.config file, your dll must reside in the \bin folder right beside ArcMap.exe. This is easily accomplished by adding a post-build event in Visual Studio
copy $(TargetDir)*.* “c:\program files\ArcGIS\bin”
Now, every time you build the project, it will copy your dll and any dependencies (log4net in our case) into the \bin folder.
A final note – when you start using logging, you’ll want to be able to look at the log without re-opening the file all the time. Thanks to Vish I’ve been using BareTail for quite some time now – this app will let you watch the file as lines are added to it.
Here’s a simple sample editing task, along with an ArcMap.exe.config file. Have fun!


Great tips! I didn’t know about BareTail.
Another tip I learned from the .NET blogoshpere: When calling GetLogger(), instead of hard coding the class name you can use reflection to get the declaring type. This way when you copy and paste the _logger declaration from class to class you don’t have to remember to replace the hard coded class name. Many people also recommend making the logger static and readonly. For example:
private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
Thanks for this article. One thing to mention is the performance of logging. Imagine you want to log a string which is very costly to build, eg. many variables or complex toString()-methods of your data-objects. When a “debug” log statement gets called, the string will be constructed and then the logger detects that the logging level is set to “error”. So the constructed string will not be logged. Some time wasted for string construction.
The solution is an extra if-statement:
if (_logger.IsDebugEnabled) { LOG.Debug(”some complex string with many variables”); }
You can easily create a snippet for this
Best regards, Stefan
Instead of changing the class name in each class where the logger appears, you can use reflection:
_logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
Sometimes it seems that using the ArcMap.exe.config file is somewhat impractical. You can also use a own config file which exists next to the executing dll (e.g. your command).
In the following example a log4net config file named like the executing dll with an “.log4net” appendix is loaded.
System.IO.FileInfo fileInfo = new System.IO.FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location + “.log4net”);
log4net.Config.XmlConfigurator.ConfigureAndWatch( fileInfo );
You can execute this in the OnCreate method in your command….
Instead of copying your dll’s to the ArcGIS bin folder you can configure log4net with a memorystream created from a string.
string configString = “….”; // same as you would define in the app.config
MemoryStream configStream = new MemoryStream(Utf8Encoding.Default.GetBytes(configString));
log4net.Config.XmlConfigurator.Configure(configStream);
Regards,
Samuel