Logowanie parametrów wywoływanych metod z użyciem MEF, Castle, Json.NET i NLog

Analizując problem z działaniem aplikacji w środowisku produkcyjnym, czasami przydałoby się mieć możliwość sprawdzanie jakie argumenty są przesyłane do wywoływanych metod, zwłaszcza, w przypadku aplikacji desktopowych, gdzie nie mamy dostępu do pełnego zbioru danych, na którym pracuje użytkownik i musimy opierać się jedynie na informacjach zawartych w logach. Rozwiązaniem tego problemu jest dołączenie interceptora do kontenera IoC, który przy wywołaniu metody zapisywałby informacje o przesłanych argumentach. Jednak należy pamiętać, że zapisywanie tych informacji dla każdej metody jest dość kosztowne pod względem czasu wykonania takiego kodu i należy nałożyć odpowiednie instrukcje warunkowe, dzięki, którym kod logowania argumentów będzie uruchamiany tylko dla odpowiedniego poziomu logowania.

Pierwszą rzeczą, którą należy wykonać w celu dołączenia interceptora w przypadku kontenera MEF jest dodanie do projektu dowiązania do biblioteki MEFContrib (mefcontrib.codeplex.com, dostępna również jako pakiet NuGet, należy wówczas zainstalować pakiet MefContrib.Interception.Castle) oraz biblioteki Json.NET, również dostępnej jako pakiet NuGet). Następnie należy skonfigurować katalog InterceptingCatalog, który jest dostępny właśnie w MEFContrib:

// Usings
//using System.ComponentModel.Composition.Hosting;
//using System.Reflection;
//using MefContrib.Hosting.Interception;
//using MefContrib.Hosting.Interception.Configuration;
//using MefContrib.Hosting.Interception.Castle;

AssemblyCatalog assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

InterceptionConfiguration cfg = new InterceptionConfiguration();
cfg.AddInterceptor(new DynamicProxyInterceptor(new CastleLoggingInterceptor()));
InterceptingCatalog interceptingCatalog = new InterceptingCatalog(assemblyCatalog, cfg);

CompositionContainer container = new CompositionContainer(interceptingCatalog);

W powyższym kodzie użyłem klasy DynamicProxyInterceptor, która jest odpowiedzialna za dołączenie naszych interceptorów do obiektów klas, które będą pobierane za pomocą MEFa. Klasa CastleLoggingInterceptor jest naszą klasą interceptora Castle, która będzie odpowiedzialna za logowanie argumentów wywoływanych metod. Podstawowa wersja tej klasy wygląda jak poniżej:

public class CastleLoggingInterceptor : Castle.DynamicProxy.IInterceptor
 {
     public void Intercept(IInvocation invocation)
     {
         invocation.Proceed();
     }
 }

Linijka invocation.Proceed() jest wymagana w celu uruchomienia metody, którą opakowujemy. Następną czynnością, którą musimy wykonać w celu logowania argumentów funkcji jest odczytanie informacji o wywoływanej metodzie i zserializować do formatu JSON, w tym celu stworzymy dwie metody jak poniżej:

private string WriteMethodNameWithParameters(IInvocation invocation)
{
    LogItem item = new LogItem();
    item.AssemblyFullName = invocation.TargetType.Assembly.FullName;
    item.TypeFullName = invocation.TargetType.FullName;
    item.MethodName = invocation.Method.Name;
    item.ReturnType = invocation.Method.ReturnType.FullName;
    item.Parameters = this.GetParameters(invocation);
    return JsonConvert.SerializeObject(item);
}

private ParameterItem[] GetParameters(IInvocation invocation)
{
    ParameterInfo[] parameters = invocation.Method.GetParameters();
    IList<ParameterItem> parametersTypes = new List<ParameterItem>();
    for (int i = 0; i < parameters.Length; ++i)
    {
        object value = null;
        if (parameters[i].ParameterType.IsPrimitive || parameters[i].ParameterType.FullName.StartsWith("MefInterception"))
        {
            value = invocation.GetArgumentValue(i);
        }

        parametersTypes.Add(new ParameterItem(parameters[i].Name, parameters[i].ParameterType.FullName, value));
    }
    return parametersTypes.ToArray();
}

W metodzie GetParameters(..) pobieramy typy wszystkich parametrów metody oraz wartości z jakimi została wywołana i zwracamy w postaci tablicy typu ParameterItem, którą dodajemy następnie do LogItem i serializujemy. Warunek:

parameters[i].ParameterType.IsPrimitive || parameters[i].ParameterType.FullName.StartsWith("MefInterception")

jest potrzebny w celu serializowania parametrów, które są albo w naszej przestrzeni nazw, w tym przypadku MefInterception albo są to typy podstawowe. Jeżeli nie użyjemy tego warunku to korzystając z bibliotek dodatkowych, np. z Prism, podczas serializacji mogą wystąpić problemy zakończone wyrzuceniem wyjątku.

Klasy LogItem oraz ParameterItem to zwykłe klasy przechowujące dane bez żadnych metod:

public class LogItem { public string AssemblyFullName { get; set; } public string TypeFullName { get; set; }     public string MethodName { get; set; }     public string ReturnType { get; set; }
public ParameterItem[] Parameters { get; set; } } public class ParameterItem { public string Name { get; set; } public string Type { get; set; } public object Value { get; set; }

public ParameterItem(string name, string type, object value) { this.Name = name; this.Type = type; this.Value = value; } }

Ostatnim krokiem będzie dodanie wpisu w naszym interceptorze, który umożliwi logowanie informacji. Nowa wersja metody Intercept(..) będzie wyglądać jak poniżej:

public void Intercept(IInvocation invocation)
{
    logger.Trace(() => this.WriteMethodNameWithParameters(invocation));
    invocation.Proceed();
}

Tradycyjnie przykładowy projekt został dołączony do poniżej tego tekstu.

W następnym poście pokaże jak zbudować własną wersje klasy DynamicProxyInterceptor użytej do opakowania interceptorów Castle w celu użycia ich z kontenerem MEF. Powodów do budowy własnej klasy opakowującej interceptory jest kilka, ale o tym w następnym poście.

Przykładowy projekt

2 Comments.

  1. dotnetomaniak.pl - trackback on 20 sierpnia 2011 at 12:58

Trackbacks and Pingbacks: