Всем привет.
Я написал статью о том "за бесплатно", системными методами, сделать так, чтобы логи писали себя сами (https://habr.com/ru/articles/931416/). Статья описывает как сделать так чтобы в логах отражались аргументы вызова метода и возвращаемый результат, при этом не обработанные исключения, тоже будут попадать в логи.
В первом же комментарии к той статье, мне дали ссылку на альтернативный способ (https://github.com/vescon/MethodBoundaryAspect.Fody). Способ через разработку атрибута и применение этого атрибута ко всем методам, которые хочется видеть в логах.
Способ на самом деле удобный, но имеющий свои нюансы. Самый не очевидный состоит в том, что NuGet пакет MethodBoundaryAspect.Fody надо добавить во все проекты где вам нужны логи.
Нюансы использования NuGet пакета MethodBoundaryAspect.Fody
Первое
В моём случае, у меня разработан проект-библиотека которая непосредственно пишет логи, и этот проект добавлен как зависимость в другие проекты. В классах я проставил атрибут на методы, которые хотел логировать. Код успешно компилировался, но логи не писались, пока я в эти проекты не добавил пакет MethodBoundaryAspect.Fody.
Восемь раз подряд одни и те же действия (у меня в солюшене 17 проектов, в восьми нужны были логи), это было пыткой.
Если добавлять в один проект за раз (за день), то конечно ни чего страшного.
Второе
При добавлении NuGet пакета MethodBoundaryAspect.Fody, автоматически создаются два файла:
FodyWeavers.xml
FodyWeavers.xsd
Это файл конфигурации (FodyWeavers.xml) и описание формата файла конфигурации (FodyWeavers.xsd).
Эти файлы будут во всех проектах, у меня в солюшене например 17 проектов, из них в 8 эти файлы добавлены, вопрос зачем ? Нельзя ли их куда то задвинуть подальше, чтобы в корне не болтались ?
В остальном всё супер, и все прочие сложности пришлось бы разруливать при любом другом способе логирования.
Порядок действий
Действие первое - добавить пакет:
Install-Package MethodBoundaryAspect.Fody
Добавить можно разными способами, надеюсь разберётесь.
Класс атрибута
Второе, разрабатываем класс который непосредственно будет писать логи. Класс должен быть наследником от OnMethodBoundaryAspect.
Класс атрибут целиком
using MethodBoundaryAspect.Fody.Attributes;
namespace archivist
{
public sealed class LogAttribute : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
IndependentFileArchivist Archivist = new();
string correlationId = string.Empty;
if (args.Instance != null )
{
var instance = (ILogable)args.Instance;
correlationId = instance.GetCorrelationId();
}
Archivist.Initialize(correlationId, args.Method);
Archivist.Before(DateTime.Now, args.Arguments);
args.MethodExecutionTag = Archivist;
}
public override void OnExit(MethodExecutionArgs args)
{
var Archivist = GetArchivist(args);
Archivist.After(DateTime.Now, args.ReturnValue);
}
public override void OnException(MethodExecutionArgs args)
{
var Archivist = GetArchivist(args);
Archivist.Exception(DateTime.Now, args.Exception);
}
private static IndependentFileArchivist GetArchivist(MethodExecutionArgs args)
{
return (IndependentFileArchivist)args.MethodExecutionTag;
}
}
}
Каждый метод логера в своей сигнатуре имеет MethodExecutionArgs args. В свойство MethodExecutionTag можно сохранить произвольный объект (можно сохранить состояние логера). Это свойство имеет самый широкий тип object, поэтому при чтении из него надо выполнять приведение типа к нужному.
У статических методов args.Instance будет null, если собираетесь вызывать какой то метод класса для которого задан класс атрибут, то сделайте проверку на null. Как при этом передавать correlation id я не разбирался, возможно это минус этого способа логирования.
Как я использую свойство MethodExecutionTag
# При первом вызове класса обёртки (атрибута) создаём экземпляр "своего" логера
IndependentFileArchivist Archivist = new();
# Записываем экземпляр логера (его состояние) в специальное свойство класса
args.MethodExecutionTag = Archivist;
# При следующих вызовах, восстанавливаем
var Archivist = (IndependentFileArchivist)args.MethodExecutionTag
Первый вызов (OnEntry)
Создаём писателя логов и записываем его в MethodExecutionTag:
public override void OnEntry(MethodExecutionArgs args)
{
IndependentFileArchivist Archivist = new();
args.MethodExecutionTag = Archivist;
Инициализируем писателя логов
string correlationId = string.Empty;
Archivist.Initialize(correlationId, args.Method);
Логируем вызов метода помеченного атрибутом LogAttribute
Archivist.Before(DateTime.Now, args.Arguments);
}
Последующие вызовы (OnExit, OnException)
Вызов на возврате результата
Восстанавливаем писателя:
public override void OnExit(MethodExecutionArgs args)
{
var Archivist = GetArchivist(args);
Логируем:
Archivist.After(DateTime.Now, args.ReturnValue);
}
Аналогично с вызовом на необработанном исключении:
public override void OnException(MethodExecutionArgs args)
{
var Archivist = GetArchivist(args);
Archivist.Exception(DateTime.Now, args.Exception);
}
Особенности работы класса атрибута
Класс атрибут не статический, но видимо под капотом на каждый вызов создаётся новый экземпляр, поэтому у класса на самом деле нет состояния, поэтому приходиться упражняться с args.MethodExecutionTag. Конечно на отладке вы это сами увидите.
Пример использования в коде
Собственно последний третий этап - это применить класс атрибут в коде.
Всё использование заключается только в установке атрибута для метода. Устанавливать атрибут на сигнатуру метода в интерфейсе бесполезно, приходиться в каждом классе реализации интерфейса помечать атрибутами все нужные методы.
Собственно всё использование это строка 28.
Пример использования класса атрибута
using archivist;
using Newtonsoft.Json;
namespace archivist
{
public interface ILogable
{
public string GetCorrelationId();
}
}
namespace PayloadProcessor
{
public class Processor : ILogable
{
public Processor(string correlationId)
{
CorrelationId = correlationId;
}
public string CorrelationId { get; }
public string GetCorrelationId()
{
return CorrelationId;
}
[Log]
public string HandlePayload(string payload)
{
var doc = JsonConvert.DeserializeXmlNode(payload);
var result = doc?.InnerXml;
return result ?? "";
}
}
}
Класс имеет имя LogAttribute, класс в имени имеет "суффикс" Attribute его можно опустить. В приведённом примере используется короткая форма записи - Log, Компилятор понимает о чём речь. Если класс атрибут не имеет суффикса Attribute, то имя класса надо писать полностью.
Проблемы с логированием генераторов
Вызов генератора логируется не пойми как. Сначала в логах появляется результат метода в котором вызывается генератор, потом в логах появляются собственно вызовы генератора. причём по факту бы один вызов генератора, а в логах их два. То есть видимо, один раз был вызов OnExit при yield return, и второй раз OnExit был вызыван на завершеннии работы метода.
Ниже код который был залогирован совсем не в том порядке, не с теми временными отметками, которые были на самом деле.
[Log]
public override string Run(IArgumentStorage arguments)
{
var medium = arguments.Extract(
MediumIndex,
"Тип источника сообщений не задан"
);
var options = arguments.Extract(
OptionsIndex,
"Не определены настройки получения сообщений из источника"
);
var messageList = new List<string>();
foreach (var message in ReadMessages(medium, options))
{
messageList.Add(message.ToString());
Console.WriteLine(message);
}
return new archivist.ArrayStringPrinter(messageList.ToArray()).ToString();
}
[Log]
public IEnumerable<string> ReadMessages(
string medium,
string options
)
{
var reader = new ReadingInboxLibrary
.InboxReaderFactory()
.Make(medium, CorrelationId);
reader.GetReadyForReading(options);
foreach (var body in reader.LetReadTheMessages())
{
yield return body;
}
}
Как было:
вызывается метод Run
внутри метода Run вызывается метод ReadMessages, который является генератором
метод ReadMessages внутри себя вызывает другой генератор DirectoryObserver.LetReadTheMessages()
возвращается значение из LetReadTheMessages
возвращается значение из ReadMessages
делаем ещё один вызов ReadMessages
делаем ещё один вызов LetReadTheMessages
из LetReadTheMessages возвращается ответ что в генераторе закончились значения
из ReadMessages возвращается ответ что в генераторе закончились значения
метод Run возвращает свой результат
Что записано в логах:

Состав колонок :
Самая левая колонка это отметки времени в тиках процессора;
Идентификатор запуска;
Собственно был запуск или было завершение;
Аргументы вызова или результат выполнения;
Как видим в логах всё было последовательно:
Concierge.CLI.Program -> ReadMessages (строки 1 и 2)
ReadingInboxLibrary.DirectoryObserver -> GetReadyForReading (строки 3 и 4)
ReadingInboxLibrary.DirectoryObserver -> LetReadTheMessages (строки 5 и 6)
ReadingInboxLibrary.DirectoryObserver -> GetReadyForReading (строки 7 и 8)
ReadingInboxLibrary.DirectoryObserver -> LetReadTheMessages (строки 9 и 10)
Нет вложенности LetReadTheMessages в ReadMessages.
Имейте в виду эту особенность работы класса атрибута с генераторами.
Наверное это повод стать контрибьютором.
Выводы
Выводы у нас на канале каждый делает для себя сам :-)
Мне понравилось, я буду использовать.
Ссылки по теме
Комментарии (8)
ValeriyPus
05.08.2025 18:45Попробуйте статический класс логгера создать, создание сервисов руками - плохая практика.
BeforeExecution, afterExecution - да, здоро что есть логирование времени исполнения, но это лучше вынести в Debug (#if DEBUG ... #endif). Начал дебажить - смотри сколько какой метод исполняется.
Да и в общем, в VisualStudio есть профилирование, скорее всего эти отметки по времени ничего не дадут. (а метод может быть вызван и 1м раз в минуту :) )
dyadyaSerezha
05.08.2025 18:45В код толком не вникал, но если я правильно понимаю, и этот вариант, и вариант из первой статьи основаны на reflection. Отсюда сразу вопрос: а сколько времени занимает собственно это автоматическое логирование? При какой частоте вызовов это становится проблемой? Или тут все делается в начале, а потом идут чистые оберточные вызовы?
ValeriyPus
05.08.2025 18:45Fody - работа с IL готовой сборки.
Логирование начала конца метода сделано зря.
System.Reflection тут нет.
dyadyaSerezha
05.08.2025 18:45Эээ... Это не очень поясняет, но ладно.
SbWereWolf Автор
05.08.2025 18:45120 мс выполнение тестов с логированием через MethodBoundaryAspect
100 мс выполнение тестов с логированием через Reflection
070 мс выполнение тестов без логирования
В документации у MethodBoundaryAspect.Fody есть цифры по влиянию логирования, почитайте, что пишут разработчики.
Логирование через Reflection более гибкое, но приходиться создавать обёртку, и каждый метод для логирования приходиться добавлять в интерфейс.
Логирование через MethodBoundaryAspect.Fody даёт возможность логировать приватные методы, но приходиться каждый метод отмечать атрибутом.
Пока позволяет логика логирования пользуемся MethodBoundaryAspect.Fody, когда гибкости перестаёт хватать переписываем на Reflection.
nronnie
05.08.2025 18:45А нахера логировать вызовы методов? Видел в жизни слишком много идиотского кода где буквально каждый метод был обернут в
log(Begin ...")
/log("End ...")
и еще довесок и вtry/catch
, причем, часто был обернут и сам метод и его вызовы. Больший идиотизм и придумать сложно.
navferty
Если нужно какие-то зависимости, свойства и т.п. добавить сразу во все проекты в солюшне, можно просто добавить один .props файл (например, его часто используют для добавления анализаторов исходного кода).
https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory