次の方法で共有


Installing Event Sources For The Logging Application Block

One of the nice things about the Enterprise Library Logging Application Block (LAB) is that it's so darned configurable (like the other Enterprise Library Application Blocks). One of the benefits of this is that it makes it rather easy to unit test your code's logging logic. On the other hand, all this configurability (if that's a word) comes at a price, which I recently discovered when I began thinking about installing a Windows service that's using the LAB to log to the Windows event log.

Essentially, to enable an application to run with least privilege, you should install the event sources when you deploy the application. This can easily be done in a custom application Installer using the EventLogInstaller class. The only problem here is that this requires you to define the event source in code at design time, whereas the event log trace listeners for the LAB is defined in the application configuration file at deployment time.

I initiated a little discussion about this issue over at the Enterprise Library discussion forum, and one observation is that it's probably not very common that a system administrator would want to change the event sources after installing the application. It's a good point, but even so, it didn't sit too well with me to hard-code my event sources in an application Installer, and then doubling them in the configuration file.

For that reason, I began thinking about how hard it would be to simply load the application configuration while running the installer and iterating through all the relevant trace listeners. It turned out to be not quite as easy as I originally thought, but not that hard either. Here's how:

Basically, all you need to do is to read the configuration file and parse it, extracting the information you want. While it would be possible to simply load the entire file into an XPathDocument and find the information via XPath, that doesn't really feel right. After all, there's a strongly typed configuration section for the LAB, so why not reuse that?

My next thought was to use ConfigurationManager.OpenExeConfiguration to load the application configuration, but then I thought that this might bypass Enterprise Library's ability to redirect configuration sections. The best approach, obviously, would be to use Enterprise Library's ConfigurationSourceFactory.Create method, and that's what I ended up doing.

The LoggingApplicationBlockEventLogInstaller class is a reusable installer class you can use in every project where you want to install LAB event sources using a custom application Installer. In the constructor of your application's Installer, you just need to add an instance of LoggingApplicationBlockEventLogInstaller to the application Installer's Installers collection:

 this.Installers.Add(new LoggingApplicationBlockEventLogInstaller());

The basic outline of the LoggingApplicationBlockEventLogInstaller class looks like this:

 internal class LoggingApplicationBlockEventLogInstaller : Installer
 {
     internal LoggingApplicationBlockEventLogInstaller() { }
  
     public override void Install(IDictionary stateSaver)
     {
         this.AddEventLogInstallers();
         base.Install(stateSaver);
     }
  
     public override void Uninstall(IDictionary savedState)
     {
         this.AddEventLogInstallers(); 
         base.Uninstall(savedState);
     }
 }

All the interesting stuff happens in the AddEventLogInstallers method, which I haven't shown yet. It's a bit more complicated than I had originally thought it would be, so I'm going to walk you through it in some detail.

The issue that complicates things is that when you run a custom Installer, you principally do so from a different application than the one you are installing: Typically InstallUtil.exe, although it might also be an MSI package. In the case of InstallUtil.exe, the application configuation file is InstallUtil.exe.config, and not the configuration file for the application you're installing (henceforth called the target application for lack of a better term). Since you don't want to be putting LAB configuration into InstallUtil.exe.config, you need to somehow load the target application's config file while keeping the entire configuration loading behavior intact.

First, we need to figure out where the target config file is:

 Assembly currentAssembly = Assembly.GetExecutingAssembly();
  
 string appBase = Path.GetDirectoryName(currentAssembly.Location);
 string configFile = string.Format(CultureInfo.InvariantCulture, 
     "{0}.config", currentAssembly.Location);

This is pretty straightforward when the custom Installer is in the same assembly as the target application (as it typically is). When this is the case, the executing assembly is the assembly contained in the executable file, which also means that I can just append .config to its name to get the full path of the target config file. Nonetheless, it's a good idea to test whether the file actually exists, because otherwise the Installer should log a warning and skip installing event sources, since they can't be read:

 if (!File.Exists(configFile))
 {
     this.Context.LogMessage(string.Format(CultureInfo.CurrentCulture,
         "Warning: The configuration file \"{0}\" doesn't exist. No event sources will be installed or uninstalled.",
         configFile));
     return;
 }

To enable ConfigurationSourceFactory.Create to do its trick, we need this config file to be a proper application configuration file, which is possible if we create a new AppDomain and specify this file as the configuration file:

 AppDomainSetup domainSetup = new AppDomainSetup();
 domainSetup.ApplicationBase = appBase;
 domainSetup.ConfigurationFile = configFile;
  
 AppDomain targetAppDomain = AppDomain.CreateDomain(
     "TargetApplicationDomain", null, domainSetup);

The next part is where it gets a bit tricky. Obviously, the reason we are creating a new AppDomain is to load the application configuration there, and then communicate it back to the default AppDomain via Remoting. This means that we are going to create an instance in the new AppDomain and get a proxy to it in the default AppDomain. When you do this, a not particularly intuitive thing happens: The remotable object gets created in the new AppDomain, but when you try to cast the proxy to the same type in the default AppDomain, the cast fails since the assembly can't be resolved. To fix this issue, it's necessary to subscribe to the AssemblyResolve event of the hosting AppDomain:

 AppDomain.CurrentDomain.AssemblyResolve += 
     delegate(object sender, ResolveEventArgs e)
 {
     if (e.Name == currentAssembly.FullName)
     {
         return currentAssembly;
     }
     throw new InvalidOperationException(string.Format(
         CultureInfo.CurrentCulture,
         "No assembly resolution logic was defined for {0}",
         e.Name));
 };

The AssemblyResolve event is a bit special, since it's an event handler that requires you to return a value. In this case, I just return the current assembly, which contains the remotable object that I'm creating next:

 LoggingConfigurationLoader configLoader = 
     (LoggingConfigurationLoader)targetAppDomain.CreateInstanceAndUnwrap(
     typeof(LoggingConfigurationLoader).Assembly.FullName,
     typeof(LoggingConfigurationLoader).FullName);

The LoggingConfigurationLoader class is the remotable class where I load the LAB configuration. I'll show you how this class looks in a minute, but for now, just realize that it loads the LAB configuration section (LoggingSettings) and creates a list of the configured event logs, which it exposes in a property called EventLogs. Both the list (List<T>) and its contained elements (EventLogConfiguration, which I'll also show later) are serializable, which means that they are marshalled by value across the AppDomain boundary. This means that I can simply iterate over the list:

 foreach (EventLogConfiguration elc in configLoader.EventLogs)
 {
     EventLogInstaller logInstaller = new EventLogInstaller();
     logInstaller.Log = elc.Log;
     logInstaller.Source = elc.Source;
     this.Installers.Add(logInstaller);
 }

For each event log defined in the configuration file, I create a new EventLogInstaller and assign it with the configured event log name and event source. Adding each EventLogInstaller to the LoggingApplicationBlockEventLogInstaller's Installers list causes each Installer to have its Install (or Uninstall) method called, since I'm calling base.Install (or base.Uninstall) after adding the EventLogInstaller (as you will see if you refer back to the code for LoggingApplicationBlockEventLogInstaller earlier in this post).

The last thing to do in the AddEventLogInstallers method is just to unload the AppDomain used for loading the configuration file:

 AppDomain.Unload(targetAppDomain);

While this describes the AddEventLogInstallers method, I have still left to show you a very important part of the puzzle. The LoggingConfigurationLoader is responsible for loading the LAB configuration section and exposing the relevant data. Recall that the entirety of this code is executing in the AppDomain where the target application's config file is the config file for the AppDomain. To enable the class to execute within the created AppDomain while still being remotable from the default AppDomain, it must implement MarshalByRefObject.

 internal class LoggingConfigurationLoader : MarshalByRefObject
 {
     private List<EventLogConfiguration> eventLogs_;
  
     public LoggingConfigurationLoader()
     {
         this.eventLogs_ = new List<EventLogConfiguration>();
  
         IConfigurationSource configSource =
             ConfigurationSourceFactory.Create();
         LoggingSettings loggingConfig =
             (LoggingSettings)configSource.GetSection(
             LoggingSettings.SectionName);
         foreach (TraceListenerData tld in loggingConfig.TraceListeners)
         {
             FormattedEventLogTraceListenerData feltld =
                 tld as FormattedEventLogTraceListenerData;
             if (feltld != null)
             {
                 EventLogConfiguration elc =
                     new EventLogConfiguration(feltld.Log, feltld.Source);
                 this.eventLogs_.Add(elc);
             }
         }
     }
  
     internal IList<EventLogConfiguration> EventLogs
     {
         get { return this.eventLogs_; }
     }
 }

The constructor immediately loads the LAB's configuration section via Enterprise Library's ConfigurationSourceFactory, which adds support for more sophisticated scenarios. Since ConfigurationSourceFactory.Create can handle redirected configuration sections, LoggingApplicationBlockEventLogInstaller also supports this feature.

The rest of the initialization behavior simply iterates through the configured TraceListeners, picking out all FormattedEventLogTraceListeners. As I described before, neither LoggingSettings nor TraceListenerData are remotable or serializable, so I have to copy the relevant data to a serializable class that can be marshalled by value across AppDomain boundaries.

 [Serializable]
 internal class EventLogConfiguration
 {
     private string log_;
     private string source_;
  
     internal EventLogConfiguration(string log, string source)
     {
         this.log_ = log;
         this.source_ = source;
     }
  
     internal string Log
     {
         get { return this.log_; }
     }
  
     internal string Source
     {
         get { return this.source_; }
     }
 }

The EventLogConfiguration class is a simple data transfer object whose most remarkable feature is the Serializable attribute (and that's not particularly remarkable, as features go).

When you are using LAB to log to the Windows Event Log, you can simply add an instance of LoggingApplicationBlockEventLogInstaller to your application's custom Installer class to support installation and uninstallation of configured event sources - you just have to ensure that the application's configuration file is properly configured with the correct event sources before you run the installer.

If you ever want to change the event sources, you should first uninstall the application using the old configuration settings, as this will uninstall the same event sources as was originally installed. Then you can change the configuration file and run the installer again, effectively setting up the new event sources you just defined.

Since I had to walk you through a bit of complex code, I realize that it may not be that easy to reproduce if you should want to reuse this idea, so I've included a code file containing all three relevant classes. This should get you started in relatively little time.

LoggingApplicationBlockEventLogInstaller.cs

Comments

  • Anonymous
    January 31, 2008
    Hi Ploeh, Have you seen any good ways to get log entries written to the TestContext using app.config? It'd be nice if the EnterpiseLibrary supported the ms test testContext as a destination for writing logged events too.
  • Alex
  • Anonymous
    February 01, 2008
    Hi Alex Thank you for writing. Are you thinking about using the Properties property of TestContext? I don't see why you would want to do that, but if you are looking for a way to unit test your Enterprise Library logging code, maybe this post will be helpful: http://blogs.msdn.com/ploeh/archive/2006/04/06/UnitTestYourEnterpriseLibraryLoggingLogic.aspx

  • Anonymous
    February 01, 2008
    Hi Ploeh, thanks for the response. Hmm. Let me try to describe what I'm thinking of again: We use the enterprise logging block extensively, so lots of stuff gets logged. However, during Unit Tests, I would like those log entries to get logged in such a way that they appear in the 'unit test result details'! So it seems to me that there might be a mechanism to direct all log entries that are lgoged to the enterprise logging app to the TestContext. Or, I could figure out how to intercept calls to the logger and direct them to TestContext.WriteLine....

  • Alex
  • Anonymous
    February 01, 2008
    Hi Alex Oh, okay, in that case I should think that it should suffice to log to the standard console - it should then appear in the Results window for the test, as described here: http://blogs.msdn.com/ploeh/archive/2007/10/06/RaceTroubleshootingUsingTheConsoleOutputInVSTS.aspx To write to the standard console, you would need to configure the Logging Application Block with a TraceListener that writes to the standard console. HTH

  • Anonymous
    June 12, 2008
    HI Ploeh This is a great little piece of code, and certainly introduces some interesting concepts. I have a related question: What exactly are the permissions required and setup steps required for a web application to be able to write to the event log via LAB? We are using it, without an installer, and have found that an event has to be logged once while the app pool is set to run as local administrator. Then we can set the app pool back to run as the network service, and the logging still works. Obviously that first log is doing some setup, and I would like to know what it is doing. Any ideas? Thanks a lot, Joon

  • Anonymous
    June 12, 2008
    Hi Joon That's more or less what happens: If the event source doesn't exist, the BCL implementation creates it the first time you write to it. To create an event source, you must be an administrator. One way to do that is the way you describe. However, the correct way is to install the application using an administrator account, which alleviates the need to reconfigure the application subsequently.

  • Anonymous
    June 13, 2008
    Hi Ploeh Thanks for the feedback. I have implemented an installer using your method, although, because I am installing on a web site, I assume that the assembly that the installer is running for is in the bin folder, so I climb up one folder and get the web.config from the root of the web application. This works fine, and the installer works, and creates my event log. However, when I attempt to log to that event log, I get an error in the Application log instead, with the following text: "The description for Event ID ( 1 ) in Source ( Enterprise Library Logging ) cannot be found" I tried changing the registry permissions for my event log, with no success. Do you perhaps have any idea what would cause this error? Thanks a lot, Joon

  • Anonymous
    June 13, 2008
    Hi Joon It sounds like a resource issue. Please take a look at these articles and see if they don't help you: http://msdn.microsoft.com/en-us/magazine/cc163446.aspx http://blog.sbsfaq.com/Lists/Posts/Post.aspx?ID=49 HTH

  • Anonymous
    June 13, 2008
    Hi PLoeh Thanks for those informative links. That is what I thought also, but I could not spot any obvious issues with resource file locations etc. Something interesting: If I create a new event log completely, I get the error mentioned. If I log to the Application log with my own source, I do not get the error, and the logging works fine. The registries seem the same between the two. Any thoughts on what could be different between the Application event log and my own custom event log? Thanks, Joon

  • Anonymous
    January 18, 2009
    I found useful //check , if the source does not already exist. if(!EventLog.SourceExists(elc.Source)) { this.Installers.Add(logInstaller); }

  • Anonymous
    September 08, 2010
    //AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs e)// Tricky, and 2 thumbs up for figuring that one out ! I put some additional notes over at entlib.codeplex.com/.../View.aspx Your approach saved me from having to hard code values into the code sample I found here: support.microsoft.com/.../en-us   ("Approach 2" that is). Now the EventLog-Sources get created dynamically, and your method for ~not using InstallUtil.exe.config is ...(dare I say?).....inspiring. Thanks again.