Pull to refresh

PostSharp. Решение задач логгирования и аудита

Reading time12 min
Views6.5K
Original author: Matthew D. Groves
И снова здравствуйте! В прошлый раз при обсуждении АОП, мы с вами говорили о решении задач кэширования. Сегодня мы поговорим о не менее часто встречающейся задаче – задаче логгирования и аудита. Нам часто приходится сталкиваться с такими ситуациями, как, например, анализ чужого кода. Представьте себе, что вам дали задачу интеграции с библиотекой стороннего производителя. Основной инструментарий чуть ли не каждого разработчика — .net reflector, ILSpy, dotPeek дают отличное представление о коде программы. О ее алгоритмах, структуре, возможных местах ошибок. И покрывают большой процент вопросов к программному продукту. Однако так происходит до тех пор, пока вы не начинаете ее активно использовать. Тут могут возникнуть как проблемы с производительностью, так и с непонятными «багами» внутри стороннего продукта, которые, если не иметь дизассемблера с функцией отладчика, не так-то просто найти. Или, например, вам необходимо просто следить за значениями переменных, не останавливая продукт в точках останова, real-time. Такие места часто необходимо срочно и быстро исправлять. А как при исправлении этого кода не написать программу, которая потом будет в топе сайта govnokod.ru? О таких ситуациях мы сейчас и поговорим.


Логгирование и аудит – это те возможности, в которых PostSharp один из лучших. Это один из основополагающих примеров, показывающих принцип «cross-cutting». Ведь вместо того чтобы вкраплять код логгирования в каждое место, где это должно быть сделано, вы вводите логгирование только в одну точку, автоматически распространяя код на все необходимые методы. И, конечно, вы можете сделать это выборочно, на нескольких методах, полях или свойствах. Вы можете писать в лог много очень полезной информации, однако, за время существования фреймворка и практики работы с ним, я выделю следующие популярные категории:

  • Информация об исполняемом коде: имя функции, имя класса, значения параметров, и проч. Это может вам сильно помочь в сокращении различных сценариев, которые могли привести к результату;
  • Информация о производительности: вы можете узнать, какое время понадобилось на выполнение методов;
  • Исключения: существует возможность перехватить исключение, сохранить информацию о нем в лог и перегенерировать это исключение чтобы не нарушать логику работу исходного приложения.


Помимо logging/tracing, которые, на самом деле, являются более техническими вопросами, можно также заниматься аудитом приложения, что очень похоже на logging/tracing, за исключением того что аудит – это отслеживание информации, носящей более «деловую» активность. Для примера, можно взять логгирование значений какого-либо параметра при поиске ошибки, или же вы или ваш менеджер хотите выяснить, как часто и как долго выполняются операции с депозитом. Всю информацию вы можете заносить в файл отчета или в таблицы вашей БД и выводить на корпоративном сайте в виде красивых графиков.
Давайте будем использовать некоторую программу кассира банка. И давайте предположим что это приложение может использовать счетчики купюр, а также написано с использованием WinForms. Также (в нашей очень наивной и упрощенной модели) пусть у банка есть только один клиент (например, президент), и пусть у нас только один статический класс, предоставляющий всю бизнес-логику.

public class BankAccount
{
    static BankAccount()
    {
        Balance = 0;
    }
    public static decimal Balance { get; private set; }

    public static void Deposit(decimal amount)
    {
        Balance += amount;
    }

    public static void Withdrawl(decimal amount)
    {
        Balance -= amount;
    }

    public static void Credit(decimal amount)
    {
        Balance += amount;
    }

    public static void Fee(decimal amount)
    {
        Balance -= amount;
    }
}

* This source code was highlighted with Source Code Highlighter.


Конечно же, в реальном приложении, вы будете использовать специализированный слой сервисов, dependency injection и прочие архитектурные решения вместо просто статического класса. И может быть, вы даже захотели бы использовать «ленивую загрузку» зависимостей, однако давайте отбросим все лишнее и сконцентрируемся на основном – логгировании и аудите.
Приложение работает великолепно все время, пока у компании есть честные служащие и заказчики. Однако, финансовый директор этого банка находится на стадии увольнения, т.к. миллионы долларов вдруг пропали без вести, и никто не может понять, почему. Она нанимает вас, скромного специалиста postsharp, чтобы с вашей помощью выяснить, в чем дело. Она хочет, чтобы вы изменили программу таким образом, чтобы вести учет всех операций и транзакций (да, учет есть, но она хочет сделать это на уровне методов программы, чтобы узнать реальное их количество, а не то что фигурирует в отчетах). Вы могли бы в такой ситуации, запастись терпением и ввести в каждый метод программы бизнес-логики (в нашем примере это будут всего 4 метода, однако в реальном приложении их могло бы оказаться несколько тысяч). Либо же, вы можете написать всего один метод, и применить его ко всем методам определенной группы классов или методов, сразу же, без монотонной обезьяньей работы. Мало того, расставляя код по всей программы, вы расставляете себе капканы, поскольку его надо будет потом удалять (либо делать дополнительные действия, чтобы его не было в release-сборке). И вторая причина неудобности такого подхода – вы можете наделать ошибок, пока его пишите. Давайте посмотрим на код, который мы напишем, используя PostSharp:

[Serializable]
public class TransactionAuditAttribute : OnMethodBoundaryAspect
{
    private string _methodName;
    private Type _className;
    private int _amountParameterIndex = -1;

    public override bool CompileTimeValidate(MethodBase method)
    {
        if(_amountParameterIndex == -1)
        {
            Message.Write(SeverityType.Warning, "999",
             "TransactionAudit was unable to find an audit 'amount' in {0}.{1}",
             _className, _methodName);
            return false;
        }
        return true;
    }

    public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
    {
        _methodName = method.Name;
        _className = method.DeclaringType;

        var parameters = method.GetParameters();
        for(int i=0;i<parameters.Length;i++)
        {
            if(parameters[i].Name == "amount")
            {
                _amountParameterIndex = i;
            }
        }
    }

    public override void OnEntry(MethodExecutionArgs args)
    {
        if (_amountParameterIndex != -1)
        {
            Logger.Write(_className + "." + _methodName + ", amount: "
                + args.Arguments[_amountParameterIndex]);
        }
    }
}

* This source code was highlighted with Source Code Highlighter.


Напомню, что CompileTimeInitialize используется чтобы получить название методов и параметров во время компиляции, чтобы минимизировать количество использования рефлексии во время работы приложения (кстати, использование рефлексии можно вообще свести к нулю, используя build-time код) и чтобы удостовериться что существует параметр amount, за которым мы будем следить. Если мы его не находим, то оповещаем об этом разработчика при помощи warning’а.
Используя этот аспект и какое-либо хранилище для хранения собранных данных, вы сможете создавать для своего финансового директора некоторую аналитическую информацию.

Однако, пока идет расследование, вы начинаете понимать что с системой могут быть и другие проблемы: например, вы узнаете что интерфейс к пользователю работает не стабильно и приложение постоянно «падает». Для того чтобы узнать место падений, вы можете расставить try/catch в различные места программы (который может быть огромное множество), чтобы понять, какое конкретно место дает сбои. Или же вы можете написать один класс, после чего аудит исключений включится на всех методах интерфейса автоматически (да, вы можете легко ограничить область действия этого класса). Чтобы не быть голословным, давайте посмотрим простой пример:

[Serializable]
public class ExceptionLoggerAttribute : OnExceptionAspect
{
    private string _methodName;
    private Type _className;

    public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
    {
        _methodName = method.Name;
        _className = method.DeclaringType;
    }

    public override bool CompileTimeValidate(MethodBase method)
    {
        if(!typeof(Form).IsAssignableFrom(method.DeclaringType))
        {
            Message.Write(SeverityType.Error, "003",
             "ExceptionLogger can only be used on Form methods in {0}.{1}",
             method.DeclaringType.BaseType, method.Name);
            return false;
        }
        return true;
    }

    public override void OnException(MethodExecutionArgs args)
    {
        Logger.Write(_className + "." + _methodName + " - " + args.Exception);
        MessageBox.Show("There was an error!");
        args.FlowBehavior = FlowBehavior.Return;
    }
}

* This source code was highlighted with Source Code Highlighter.


И, опять же, я использую CompileTimeInitialize только для того, чтобы уменьшить количество обращений к рефлексии. Чтобы применить этот аспект (для парочки функций) необходимо пометить соответствующие методы/классы/сборки/namespaces(в этом случае помечается сборка, дополнительно указывается фильтр по полному имени члена сборки) атрибутом:



Чтобы пометить класс, можно либо пометить сам класс, либо, пометить сборку, указав имя класса:

// in AssemblyInfo.cs
[assembly: ExceptionLogger(AttributeTargetTypes = "LoggingAuditing.BankAccountManager")]

* This source code was highlighted with Source Code Highlighter.


После того, как вы включили логгирование и провели аудит приложение, выяснились ужасные вещи:

  • Клиенты могут снимать отрицательные суммы со свих счетов, и менее честные клиенты этим частенько пользуются!
  • При вводе информации в формы интерфейса, люди постоянно делают опечатки, не проверяя ввод. Например, если кассир вводит «$ 5,19», это вызовет необработанное исключение и крах всего приложения!


С двумя очень простыми аспектами, вы сможете решить эти вопиющие недостатки приложения достаточно быстро, дающие бизнес пользователям аудит потока транзакций, и давая разработчикам в вашей команде логгировать и отслеживать исключительные ситуации в процессе отгрузки и работы у пользователя. Вы должны распознать, что жестко сломанное приложение на стадии установки конечному пользователю, на стадии работы у конечно пользователя, может создать огромное количество проблем. Особенно, с командой разработки. Однако, если вы используете аспекты, вы сможете их очень быстро диагностировать и исправить.
Теперь, давайте заглянем под капот и посмотрим какой код на самом деле генерируется. Не паникуйте, если не все еще понимаете. PostSharp очень прост в использовании и результаты его работы могут быть легко открыты в любом disassembler’е. Но давайте все равно посмотрим на результирующий код. Мы же хотим во всем разобраться.
Здесь представлен метод “Credit” без использования PostSharp. Как вы видите, он достаточно прост:

public static void Credit(decimal amount)
{
    Balance += amount;
}

* This source code was highlighted with Source Code Highlighter.


Далее, посмотрим на этот же метод, после применения аспекта TransactionAudit. Помните, что этот код будет находиться только в результирующей сборке (dll и exe файлы) и не будет находиться в ваших исходных текстах:

public static void Credit(decimal amount)
{
    Arguments CS$0$0__args = new Arguments();
    CS$0$0__args.Arg0 = amount;
    MethodExecutionArgs CS$0$1__aspectArgs = new MethodExecutionArgs(null, CS$0$0__args);
    CS$0$1__aspectArgs.Method = <>z__Aspects.m11;
    <>z__Aspects.a14.OnEntry(CS$0$1__aspectArgs);
    if (CS$0$1__aspectArgs.FlowBehavior != FlowBehavior.Return)
    {
        try
        {
            Balance += amount;
            <>z__Aspects.a14.OnSuccess(CS$0$1__aspectArgs);
        }
        catch (Exception CS$0$3__exception)
        {
            CS$0$1__aspectArgs.Exception = CS$0$3__exception;
            <>z__Aspects.a14.OnException(CS$0$1__aspectArgs);
            CS$0$1__aspectArgs.Exception = null;
            switch (CS$0$1__aspectArgs.FlowBehavior)
            {
                case FlowBehavior.Continue:
                case FlowBehavior.Return:
                    return;
            }
            throw;
        }
        finally
        {
            <>z__Aspects.a14.OnExit(CS$0$1__aspectArgs);
        }
    }
}

* This source code was highlighted with Source Code Highlighter.


И опять же: не паникуйте! :) Этот код может показаться сумбурным из-за использования автоматически генерируемых имен переменных, однако в реальности он очень простой. К оригинальному коду добавляется обертка из try/catch и вызывается код определенного аспекта. Как вы видите, здесь реально используется только переопределенный метод onEntry, в котором вы будете делать свои действия. Однако, переопределять можно и другие методы(onExit, onSuccess, onException), если бы вы решали какие-либо задачи, где их переопределение было бы необходимо.
Этот код, который был приведен выше, генерирует бесплатная версия postsharp. Полнофункциональная версия программы работает, оптимизируя результирующий код, как показано ниже. В нашем случае программа поймет, что вы используете только метод onEntry и что в данном случае нет никакой необходимости генерировать огромное количество кода. И в этом случае вы получите такой короткий код:

public static void Credit(decimal amount)
{
    Arguments CS$0$0__args = new Arguments();
    CS$0$0__args.Arg0 = amount;
    MethodExecutionArgs CS$0$1__aspectArgs = new MethodExecutionArgs(null, CS$0$0__args);
    <>z__Aspects.a14.OnEntry(CS$0$1__aspectArgs);
    Balance += amount;
}

* This source code was highlighted with Source Code Highlighter.


Полнофункциональная версия по-умному генерирует только тот код, который нужен вам. Если вы пишите приложение с полным использованием аспектов postsharp, использование полнофункциональной версии было бы хорошим решением, чтобы улучшить производительность приложения.
Тем не менее, несмотря на забавные имена автоматически сгенерированных членов классов и локальных переменных, я надеюсь, что вы получили хорошее представление о том, что такое PostSharp.

Ссылки:
Tags:
Hubs:
Total votes 23: ↑16 and ↓7+9
Comments9

Articles