Enterprise Library Logging Application Block - część IV 

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2013-11-22

Wprowadzenie

Poprzednia część artykułu mówiła o infrastrukturze Event Tracing for Windows (ETW), ta zaś dotyczy monitorowania źródła zdarzeń. Można tego dokonać, umieszczając kod w tym samym procesie co źródło logów albo w procesie zupełnie odseparowanym od niego, co gwarantuje większą stabilność w przypadku krytycznych błędów.

Hosting w tym samym procesie

Najprostszą metodą jest zapisanie/odebranie wszystkich logów w tym samym procesie, w którym znajduje się generująca je aplikacja. W wielu scenariuszach takie podejście okazuje się poprawne i nie warto poświęcać czasu na pisanie nowej usługi zapisującej logi.

W poprzednim artykule zaprezentowano typową implementację EventSource (przykład z MSDN):

[EventSource(Name = "MyCompany")]
    internal class MyCompanyEventSource : EventSource
    {
        public class Keywords
        {
            public const EventKeywords Page = (EventKeywords) 1;
            public const EventKeywords DataBase = (EventKeywords) 2;
            public const EventKeywords Diagnostic = (EventKeywords) 4;
            public const EventKeywords Perf = (EventKeywords) 8;
        }

        public class Tasks
        {
            public const EventTask Page = (EventTask) 1;
            public const EventTask DBQuery = (EventTask) 2;
        }

        [Event(1, Message = "Application Falure: {0}", Level = EventLevel.Error, Keywords = Keywords.Diagnostic)]
        public void Failure(string message)
        {
            WriteEvent(1, message);
        }

        [Event(2, Message = "Starting up.", Keywords = Keywords.Perf, Level = EventLevel.Informational)]
        public void Startup()
        {
            WriteEvent(2);
        }

        [Event(3, Message = "loading page {1} activityID={0}", Opcode = EventOpcode.Start,
            Task = Tasks.Page, Keywords = Keywords.Page, Level = EventLevel.Informational)]
        public void PageStart(int ID, string url)
        {
            if (IsEnabled()) WriteEvent(3, ID, url);
        }

        [Event(4, Opcode = EventOpcode.Stop, Task = Tasks.Page, Keywords = Keywords.Page,
            Level = EventLevel.Informational)]
        public void PageStop(int ID)
        {
            if (IsEnabled()) WriteEvent(4, ID);
        }

        [Event(5, Opcode = EventOpcode.Start, Task = Tasks.DBQuery, Keywords = Keywords.DataBase,
            Level = EventLevel.Informational)]
        public void DBQueryStart(string sqlQuery)
        {
            WriteEvent(5, sqlQuery);
        }

        [Event(6, Opcode = EventOpcode.Stop, Task = Tasks.DBQuery, Keywords = Keywords.DataBase,
            Level = EventLevel.Informational)]
        public void DBQueryStop()
        {
            WriteEvent(6);
        }

        [Event(7, Level = EventLevel.Verbose, Keywords = Keywords.DataBase)]
        public void Mark(int ID)
        {
            if (IsEnabled()) WriteEvent(7, ID);
        }
    }

Odbieranie logów w tym samym procesie jest bardzo proste. Wystarczy:

var source = new MyCompanyEventSource();
         
var listener = new ObservableEventListener();
            listener.EnableEvents(source,EventLevel.Informational,MyCompanyEventSource.Keywords.Perf|MyCompanyEventSource.Keywords.DataBase);
listener.LogToConsole();
            
source.Startup();
source.DBQueryStart("Select * From Users;");
source.DBQueryStop();

ObservableEventListener to omówiony w poprzedniej części „konsument”. Za pomocą metody EnableEvents należy określić, co powinno być monitorowane. Umożliwia to filtrowanie za pomocą poziomu zdarzenia czy wspomnianych słów kluczowych.

Przedstawiony przykład przekazuje logi po prostu na konsolę, ale jest również możliwość zapisania ich np. w pliku tekstowym:

var source = new MyCompanyEventSource();
         
var listener = new ObservableEventListener();
            listener.EnableEvents(source,EventLevel.Informational,MyCompanyEventSource.Keywords.Perf|MyCompanyEventSource.Keywords.DataBase);
listener.LogToFlatFile("c:\\setup\\1.txt");
            
source.Startup();
source.DBQueryStart("Select * From Users;");
source.DBQueryStop();

Standardowo do dyspozycji jest jeszcze metoda LogToRollingFlatFile. Istnieje oczywiście możliwość zapisu do innych lokalizacji, takich jak relacyjna baza danych czy Azure Tables.

Każdą wiadomość można odpowiednio sformatować – w zależności od potrzeb. Jeśli np. log ma zostać zapisany w postaci XML, wystarczy:

listener.LogToConsole(new XmlEventTextFormatter());

Inne powszechnie wykorzystywane formatowania to EventTextFormatter i JsonEventTextFormatter.

Należy też pamiętać o zwolnieniu obserwatora, jeśli nie ma już potrzeby monitorowania zdarzeń:

listener.DisableEvents(source);
        listener.Dispose();

Hosting w osobnym procesie

Czasami – w celu zagwarantowania wysokiej stabilności i niezawodności – hostuje się powyższą logikę w osobnym procesie, który będzie działał nawet wówczas, gdy aplikacja produkująca zdarzenia wyrzuci wyjątek. Zwykle tym procesem jest usługa Windows lub klasyczna aplikacja konsolowa. Na potrzeby testów najczęściej używa się aplikacji konsolowej, a w środowisku produkcyjnym – usługi Windows.

Nowego procesu nie trzeba tworzyć od zera. Wraz z Enterprise Library dostarczono bowiem aplikację o nazwie SemanticLogging-svc.exe, która stanowi opisywany wyżej proces. Wystarczy odpowiednio zmienić plik konfiguracyjny i już można zapisywać własne logi. Zadanie sprowadza się wyłącznie do zmiany pliku XML; nie pisze się nowego kodu. Przykładowo, aby zapisać logi w pliku tekstowym test.txt, należy zmodyfikować SemanticLogging-svc.xml w następujący sposób:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns="http://schemas.microsoft.com/practices/2013/entlib/semanticlogging/etw"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://schemas.microsoft.com/practices/2013/entlib/semanticlogging/etw SemanticLogging-svc.xsd">
 
  <traceEventService/>
  <sinks>
    <flatFileSink name="svcRuntime" fileName="test.log" >
      <sources>
        <eventSource name="MyCompany" level="LogAlways"/>
      </sources>
      <eventTextFormatter header="----------"/>
    </flatFileSink>
  </sinks>

</configuration>

Następnie wystarczy uruchomić proces z parametrem –c:

SemanticLogging-svc –c

Parametr –c uruchamia proces jako aplikację konsolową. Można oczywiście hostować za pomocą usługi Windows. Trzeba tylko użyć następujących komend:

SemanticLogging-svc  -install
SemanticLogging-svc.exe -start

Możliwe jest również odinstalowanie usługi za pomocą przełącznika –uninstall.

Wygenerowany plik będzie wyglądał następująco:

Plik wygenerowany przez SemanticLogging-svc.exe

Rys. 1. Plik wygenerowany przez SemanticLogging-svc.exe.

Testowanie

Czasami trudno dostrzec błędy w źródle danych. Jedną z częstych pomyłek jest przekazanie w atrybucie innego identyfikatora niż w metodzie WriteEvent, np.:

public class MyCompanyEventSource : EventSource
    {
        [Event(7, Level = EventLevel.Verbose)]
        public void LogSomething(int id)
        {
            if (IsEnabled()) WriteEvent(9, id);
        }
    }

Kod skompiluje się bez problemu i dopiero podczas działania aplikacji pojawią się błędy. Na szczęście z EntLib bardzo łatwo napisać prosty test jednostkowy, a mianowicie:

[Test]
        public void ShouldValidateEventSource()
        {
            var eventSource = new MyCompanyEventSource();
            EventSourceAnalyzer.InspectAll(eventSource);
        }

Metoda InspectAll sprawdzi poprawność źródła danych poprzez wywołanie wszystkich zdefiniowanych metod. Powyższy test oczywiście się nie powiedzie – zostanie wyrzucony wyjątek „Event LogSomething is givien event ID 7 but 9 was passed to WriteEvent.”

Możliwości rozszerzeń

Tak samo jak w innych blokach Enterprise Library istnieje możliwość rozszerzenia funkcjonalności i dostosowania jej do danego projektu. Zwykle nie ma takiej potrzeby albo wystarczy skorzystać z rozwiązań już dostępnych na NuGet lub w internecie. Jeśli jednak jakiś element trzeba koniecznie dostosować do danego projektu, istnieje kilka dobrych rozwiązań:

  • zaimplementowanie nowych klas formatujących – jeśli formaty takie jak JSON czy XML nie są wystarczające, można napisać własną klasę;
  • stworzenie nowych klas („event sinks”) przechowujących logi w innych lokalizacjach niż aktualnie dostępne (m.in. baza danych, plik, Azure Tables itp.);
  • napisanie własnej aplikacji hostującej (na wzór SemanticLogging-svc.exe);
  • zdefiniowanie własnych filtrów.

Zakończenie

Hostowanie konsumenta w innym procesie nie jest zbyt trudne i sprowadza się do modyfikacji pliku XML. Jeśli aplikacja nie należy do skomplikowanych, rozdzielanie logiki na dwa osobne procesy nie ma sensu. Jeśli jednak ma się do czynienia z rozproszoną aplikacją, której logi są przetrzymywane również w chmurze (np. Azure Tables), wtedy zdecydowanie należy skorzystać z drugiego, niezależnego procesu – w razie awarii systemu będzie on nadal działał.

          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.