Wpisy na temat śledzenia i logowania w .NET pojawiały się już wcześniej na tym blogu (np. „Śledzenie i logowanie zdarzeń (tracing and logging) na platformie .NET (przykłady w oparciu o C#).” i „Śledzimy w .NET dalej (dzisiaj uruchomimy własny podsłuch)”). Mechanizm wykorzystania elementów platformy .NET, jak: TraceSource, TraceListener i innych związanych z nimi jest dość wygodny, tym bardziej, że są dostępne gotowe klasy typu TraceListener zapisujące w plikach logi przy pomocy XML-a lub w których każdy element jest oddzielony jakimś znakiem rozdzielającym (np. przecinkiem, czy średnikiem). Te klasy to XmlWriterTraceListener lub DelimitedListTraceListener. Klasy te można łatwo utworzyć w pliku app.config przekazując nazwę pliku, do którego
zostanie zapisany log. Niestety w nazwie pliku trzeba posłużyć się pełną ścieżką, by móc w pełni kontrolować jego lokalizację, w przeciwnym razie plik zostanie utworzony w kartotece, z której uruchamiana jest aplikacja. Zobaczmy jak można spowodować, by w nazwie wykorzystywać dodatkowo „foldery specjalne” i zmienne
systemowe.
Otóż klasy XmlWriterTraceListener lub DelimitedListTraceListener,
dziedziczą po wspólnej klasie TextWriterTraceListener, która
zawiera definicję i obsługę strumienia (TextWriter), do
którego realizowany jest zapis. Strumień ten jest dostępny we
właściwości Writer. Do opracowania funkcjonalności, w
której do stworzenia nazwy pliku wykorzystamy „foldery specjalne”
i zmienne systemowe należy więc spowodować zmianę lokalizacji
strumienia. Ponieważ poszczególne TraceListener'y są najczęściej
deklarowane w pliku konfiguracyjnym (app.config), więc zmianę
lokalizacji strumienia musimy wykonać już na etapie konstruktora.
Dlatego najlepszym rozwiązaniem wydaje się odziedziczenie (po
XmlWriterTraceListener lub DelimitedListTraceListener)
i stworzenie własnych klas, które w konstruktorze zmienią
lokalizację strumienia. Najprostszym rozwiązaniem mogłoby być
wykonanie w konstruktorze podstawienia typu: this.Writer
= new StreamWriter( newFileName );, ale powstaje
pytanie co z poprzednim strumieniem? Czy może nie trzeba go zamknąć?
Wydaje mi się, że lepiej sprawdzić przy pomocy IlDasm lub
Reflector, jak jest zbudowana klasa TextWriterTraceListener.
Otóż okazuje się, że w konstruktorze
TextWriterTraceListener(string) wcale nie jest tworzony
strumień, a jedynie nazwa postawiana jest do prywatnego pola
fileName. Strumień tworzony jest dopiero później przy pomocy
niepublicznej funkcji EnsureWriter(), która wywoływana jest
przed każdym dostępie do strumienia. Otóż okazuje się, że
EnsureWriter robi znacznie więcej, niż tylko tworzy
strumień. Funkcja ta jest odpowiedzialna za obsługę błędów,
które mogą się pojawić podczas tworzenia strumienia, potrafi
nawet zmienić tak nazwę pliku (np. na Guid), by utworzenie takiego
pliku było możliwe. Dlatego wydaje się, że lepszym rozwiązaniem
jest podstawienie nowej nazwy właśnie do tego prywatnego pola
fileName, a zrobić to można przy pomocy Reflection.
Ostatecznie kod
dostarczających nowy TraceListener'ów może wyglądać następująco:
using System; using System.Collections; using System.Diagnostics; using System.Reflection; using System.Text.RegularExpressions; namespace AdvancedListener { internal class AdvancedTraceListenerHelper { private class AdvancedTraceListenerException: Exception { internal AdvancedTraceListenerException( string message ) : base( message ) { } } const string specialFormatToken = "|{0}|"; const string environmentVariablePattern = "\\%{0}\\%"; private static string PrepareFileName( string fileName ) { foreach ( Environment.SpecialFolder folder in Enum.GetValues( typeof( Environment.SpecialFolder ) ) ) { if ( !fileName.Contains( "|" ) ) break; fileName = fileName.Replace( string.Format( specialFormatToken, folder.ToString() ), Environment.GetFolderPath( folder ) ); } foreach ( DictionaryEntry variable in Environment.GetEnvironmentVariables() ) { if ( !fileName.Contains( "%" ) ) break; fileName = Regex.Replace( fileName, string.Format( environmentVariablePattern, (string)variable.Key ), (string)variable.Value, RegexOptions.IgnoreCase ); } return fileName; } internal static void PrepareAndUpdateFilename( TextWriterTraceListener textWriterTraceListener, string fileName ) { try { string org_filename = fileName; fileName = AdvancedTraceListenerHelper.PrepareFileName( fileName ); if ( org_filename != fileName ) { FieldInfo fi = typeof( TextWriterTraceListener ).GetField( "fileName", BindingFlags.NonPublic | BindingFlags.Instance ); if ( fi != null ) fi.SetValue( textWriterTraceListener, fileName ); else throw new AdvancedTraceListenerException( "Current .NET framework is not supported." ); } } catch ( AdvancedTraceListenerException ex ) { throw ex; } catch ( Exception ) { } } } public class AdvancedDelimitedListTraceListener: DelimitedListTraceListener { public AdvancedDelimitedListTraceListener( string fileName ) : base( fileName ) { AdvancedTraceListenerHelper.PrepareAndUpdateFilename( this, fileName ); } } public class AdvancedXmlWriterTraceListener: XmlWriterTraceListener { public AdvancedXmlWriterTraceListener( string fileName ) : base( fileName ) { AdvancedTraceListenerHelper.PrepareAndUpdateFilename( this, fileName ); } } }
Powyższy kod podstawia wartości zmiennych środowiskowych, gdy ich
nazwę umieścimy między znakami % (podobnie jak w
skryptach konsolowych), oraz lokalizację folderów specjalnych, gdy
ich nazwę umieścimy między znakami | (podobnie jak w
przypadku ConnectionString'ów w aplikacjach ASP.NET).
Jeżeli powyższy
kod umieścimy w assembly o nazwie „AdvancedListener”, to może
zostać wykorzystany następująco:
<sharedListeners> <add name="LogFileDelimited" type="AdvancedListener.AdvancedDelimitedListTraceListener,AdvancedListener" initializeData ="|CommonApplicationData|\Application_Main2.log" traceOutputOptions="DateTime"> <filter type="System.Diagnostics.EventTypeFilter" initializeData="All" /> </add> <add name="LogFileXml" type="AdvancedListener.AdvancedXmlWriterTraceListener,AdvancedListener" initializeData ="%ALLUSERSPROFILE%\Application_Main3.log" traceOutputOptions="DateTime"> <filter type="System.Diagnostics.EventTypeFilter" initializeData="All" /> </add> </sharedListeners>
Proste prawda?
Brak komentarzy:
Prześlij komentarz