星期四, 九月 20, 2007

Installing Event Sources For The Logging Application Block



 
 

Sent to you by Hudong via Google Reader:

 
 

via MSDN Blogs by ploeh on 9/20/07

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.


 
 

Things you can do from here:

 
 

没有评论:

发表评论