Inicializace Log4Net v serverových WCF aplikacích

Každá rozumná serverová aplikace by měla, kromě toho že něco dělá, poskytovat informace pro diagnostiku případných problémů. Typicky ve formě log souboru. Pro .NET existuje několik různých logovacích frameworků, zde se budu věnovat poměrně oblíbenému Log4Net.

Log4Net vyžaduje počáteční inicializaci jeho konfigurace voláním metody XmlConfigurator.Configure(). Toto není nijak složité v konzolových a uživatelských (WinForms / WPF) aplikacích, stejně tak ani ve Windows Services. Stačí ji zavolat jako jeden z prvních řádek kódu metody Main()

[code:c#]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using log4net.Config;

namespace PetrVones.Samples.ConsoleApplication
{
  class Program
  {
    static void Main(string[] args)
    {
      XmlConfigurator.Configure();

      // application startup code here ...
    }
  }
}

[/code]

Log4Net sice umožňuje i jiný způsob inicializace deklarativním způsobem pomocí assembly atributu XmlConfiguratorAttribute, ale ten se mi v serverových aplikacích bežících pod IIS neosvědčil, protože inicializace pak nefungovala spolehlivě.

V případě "starých" ASP.NET Web Services které využívají ASP.NET framework (a podporují pouze HTTP protokol) byla inicializace také poměrně snadná, pomocí souboru Global.asax a jeho metody Application_Start(). V případě WCF Services však nic podobného není, protože tyto service mohou být hostovány i mimo IIS, případně pracují s jiným než HTTP protokolem.

 

Jak tedy spolehlivě odchytit okamžik, kdy došlo k první aktivaci WCF Service ?

WCF Service se hostují pomocí třídy ServiceHost, kterou IIS vytvoří prostřednictvím ServiceHostFactory, a to pro každý contract (.SVC soubor) v dané assembly. Odvozením vlastní třídy od ServiceHost (a k tomu i příslušné ServiceHostFactory) získáme možnost odchycení inicializace a zároveň logovat například Open a Close události, které oznamují životnost aplikace, tedy kdy IIS aplikaci recykluje.

Vytvoříme tedy nové třídy Log4NetServiceHostFactory a Log4NetServiceHost

[code:c#]

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using log4net;
using log4net.Config;

namespace PetrVones.Samples.SampleWcfService
{
  public sealed class Log4NetServiceHostFactory : ServiceHostFactory
  {
    protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
      return new Log4NetServiceHost(serviceType, baseAddresses);
    }
  }

  public class Log4NetServiceHost : ServiceHost
  {
    public Log4NetServiceHost(Type serviceType, params Uri[] baseAddresses)
      : base(serviceType, baseAddresses)
    {
    }

    static Log4NetServiceHost()
    {
      InitializeLog4Net();
    }

    private static volatile bool log4netInitialized = false;
    private static readonly object lockInitialization = new object();
    private static readonly ILog log = LogManager.GetLogger(typeof(Log4NetServiceHost));

    private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
      string exceptionText = e.ExceptionObject == null ? "Null" : e.ExceptionObject.ToString();
      log.FatalFormat("UnhandledException: {0}", exceptionText);
      if (e.IsTerminating)
        LogManager.Shutdown();
    }

    protected override void OnClosed()
    {
      if (log.IsInfoEnabled)
      {
        ThreadContext.Properties.Clear();
        log.InfoFormat("ServiceHost for service \"{0}\" closed", Description.ServiceType);
      }
      base.OnClosed();
    }

    protected override void OnOpened()
    {
      if (log.IsInfoEnabled)
      {
        ThreadContext.Properties.Clear();
        log.InfoFormat("ServiceHost for service \"{0}\" opened", Description.ServiceType);
      }
      base.OnOpened();
    }

    public static void InitializeLog4Net()
    {
      if (!log4netInitialized)
        lock (lockInitialization)
        {
          if (!log4netInitialized)
          {
            XmlConfigurator.Configure();
            AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
            log4netInitialized = true;
          }
        }
    }

  }

}
[/code]
Metoda InitializeLog4Net inicializuje konfiguraci Log4Net. Je možné ji volat vícekrát, inicializace se provede ale pouze při prvním volání. Toho lze využít v případě, že jsou v jedné assembly různé typy WCF služeb, například WCF Data Service nebo WCF RIA Service, které mají i své specifické "host" třídy odvozené od ServiceHost. Dále přidává handler neošetřených vyjímek UnhandledException které je vhodné vždy zalogovat.

Aby se WCF service aktivovala pomocí třídy Log4NetServiceHostFactory, je nutné upravit všechny .SVC soubory přidáním atributu Factory, který určuje jméno factory třídy zodpovědné za hostováni service, tedy PetrVones.Samples.SampleWcfService.Log4NetServiceHostFactory.

<%@ ServiceHost Language="C#" Debug="true" Service="PetrVones.Samples.SampleWcfService.SampleService" CodeBehind="SampleService.svc.cs" Factory="PetrVones.Samples.SampleWcfService.Log4NetServiceHostFactory" %>

Metody OnOpen a OnClosed logují příslušné události. Volání ThreadContext.Properties.Clear() je nutné, protože thready pro vyřizování požadavků se vybírají z poolu a může dojít k situaci, že v ThreadContextu Log4Net zbydou hodnoty po volání některé WCF metody a působilo by to v logu zmatečně.

Pro úplnost ještě konfigurační soubor:

[code:xml]

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  </configSections>
  <!-- WCF -->
  <system.serviceModel>
    <services>
      <service behaviorConfiguration="SampleServiceBehavior" name="PetrVones.Samples.SampleWcfService.SampleService">
        <endpoint address="" binding="wsHttpBinding" contract="PetrVones.Samples.SampleWcfService.ISampleService">
          <identity>
            <dns value="localhost"/>
          </identity>
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="SampleServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
  <!-- Log4Net -->
  <log4net debug="true">
    <appender name="LogFileAppender" type="log4net.Appender.FileAppender">
      <file value="App_Data\Server.log"/>
      <immediateFlush value="true" />
      <appendToFile value="true"/>
      <filter type="log4net.Filter.LevelRangeFilter">
        <acceptOnMatch value="true"/>
        <levelMin value="DEBUG"/>
        <levelMax value="FATAL"/>
      </filter>
      <layout type="log4net.Layout.PatternLayout,log4net">
        <conversionPattern value="%date %-5level [%property] %logger - %message%newline"/>
      </layout>
    </appender>
    <root>
      <appender-ref ref="LogFileAppender"/>
    </root>
  </log4net>
</configuration>

[/code]

 

Novinky ve WCF v .NET 4.0

Jedna z novinek je Configuration-Based Activation in IIS and WAS kde již není nutné vytvářet fyzické .SVC soubory, ale je možné URL jednotlivých WCF services definovat v konfiguračním souboru. Pak by se do konfiguračního souboru přidalo toto:

[code:xml]

<configuration>
  <system.serviceModel>
 
    <serviceHostingEnvironment>
      <serviceActivations>
        <add relativeAddress="SampleService.svc" service="PetrVones.Samples.SampleWcfService.SampleService" factory="PetrVones.Samples.SampleWcfService.Log4NetServiceHostFactory" />
      </serviceActivations>
    </serviceHostingEnvironment>
 
  </system.serviceModel>
</configuration>

[/code]

 

Kompletní příklad ke stažení pro Visual Studio 2008 i 2010 zde: Log4NetInitializationWCF.zip (205,50 kb)

Komentáře jsou uzavřeny