Инструментирование кода для создания событий EventSource
Эта статья относится к: ✔️ .NET Core 3.1 и более поздних версий ✔️ .NET Framework 4.5 и более поздних версий
В руководстве по началу работы показано, как создать минимальный источник событий и собрать события в файле трассировки. В этом руководстве подробно описывается создание событий с помощью System.Diagnostics.Tracing.EventSource.
Минималистичный EventSource
[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
}
Базовая структура производного EventSource всегда одинакова. В частности:
- Класс наследует от System.Diagnostics.Tracing.EventSource
- Для каждого типа создаваемого события необходимо определить метод. Этот метод должен быть назван с помощью имени создаваемого события. Если событие содержит дополнительные данные, они должны передаваться с помощью аргументов. Эти аргументы событий необходимо сериализовать, поэтому разрешены только некоторые типы.
- Каждый метод имеет тело, которое вызывает WriteEvent, передавая идентификатор (числовое значение, представляющее событие) и аргументы этого метода события. Идентификатор должен быть уникальным в EventSource. Идентификатор явно назначается с помощью System.Diagnostics.Tracing.EventAttribute
- EventSources предназначены для одноэлементных экземпляров. Таким образом, можно определить статическую переменную, по соглашению с именем
Log
, представляющую этот одиночный экземпляр.
Правила определения методов событий
- Любой экземпляр, не виртуальный, возвращающий пустоту метод, определенный в классе EventSource, по умолчанию является методом ведения журнала событий.
- Виртуальные методы или методы без возвращаемого значения включаются только в том случае, если они помечены System.Diagnostics.Tracing.EventAttribute.
- Чтобы пометить подходящий метод как не требующий ведения журнала, необходимо указать его с помощью System.Diagnostics.Tracing.NonEventAttribute
- Методы ведения журнала событий имеют идентификаторы событий, связанные с ними. Это можно сделать явно, декорируя метод с помощью System.Diagnostics.Tracing.EventAttribute, или неявно с помощью порядкового номера метода в классе. Например, при использовании неявной нумерации первый метод в классе имеет идентификатор 1, второй имеет идентификатор 2 и так далее.
- Методы ведения журнала событий должны вызывать перегрузку WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId или WriteEventWithRelatedActivityIdCore.
- Идентификатор события, подразумеваемый или явный, должен соответствовать первому аргументу, переданного в API WriteEvent*, который он вызывает.
- Число, типы и порядок аргументов, передаваемых методу EventSource, должны соответствовать тому, как они передаются в API WriteEvent*. Для WriteEvent аргументы следуют за идентификатором события, для WriteEventWithRelatedActivityId аргументы следуют за связанным идентификатором активности. Для методов WriteEvent*Core аргументы должны быть сериализованы вручную в параметр
data
. - Имена событий не могут содержать
<
или>
символов. Хотя определяемые пользователем методы также не могут содержать эти символы,async
методы будут перезаписаны компилятором для их хранения. Чтобы убедиться, что эти созданные методы не становятся событиями, пометьте все методы, отличные от событий, на EventSource с помощью NonEventAttribute.
Лучшие практики
- Типы, производные от EventSource, обычно не имеют промежуточных типов в иерархии или реализуют интерфейсы. См. Расширенные настройки ниже для некоторых исключений, где это может быть полезно.
- Как правило, имя класса EventSource является плохим общедоступным именем для EventSource. Общедоступные имена, имена, которые будут отображаться в конфигурациях ведения журнала и средствах просмотра журналов, должны быть глобально уникальными. Поэтому рекомендуется присвоить EventSource общедоступное имя с помощью System.Diagnostics.Tracing.EventSourceAttribute. Имя "Демо", используемое выше, является коротким и вряд ли будет уникальным, поэтому не является хорошим выбором для использования в производственной среде. Общее соглашение — использовать иерархическое имя с
.
или-
в качестве разделителя, например MyCompany-Samples-Demo, или имя сборки или пространства имен, для которого предоставляет события EventSource. Не рекомендуется включать EventSource в качестве части общедоступного имени. - Назначайте идентификаторы событий явно, чтобы казалось бы незначительные изменения в коде исходного класса, такие как его переупорядочивание или добавление метода в середине, не изменяли идентификатор события, связанный с каждым методом.
- При создании событий, представляющих начало и конец работы, по соглашению эти методы называются суффиксами Start и Stop. Например, RequestStart и RequestStop.
- Не указывайте явное значение для свойства Guid EventSourceAttribute, если только это необходимо для обеспечения обратной совместимости. Значение GUID по умолчанию выводится из имени источника, что позволяет инструментам воспринимать более удобочитаемое имя и получать тот же GUID.
- Вызов IsEnabled() перед выполнением любой ресурсоемкой работы, связанной с запуском события, например вычисление дорогостоящего аргумента события, который не потребуется, если событие отключено.
- Старайтесь поддерживать обратную совместимость объекта EventSource и соответствующим образом версионировать их. Версия по умолчанию для события — 0. Версию можно изменить, задав EventAttribute.Version. Измените версию события при изменении данных, сериализованных с ним. Всегда добавляйте новые сериализованные данные в конец объявления события, то есть в конце списка параметров метода. Если это невозможно, создайте событие с новым идентификатором, чтобы заменить старый.
- При объявлении методов событий укажите данные фиксированного размера перед данными переменного размера.
- Не используйте строки, содержащие пустые символы. При создании манифеста для ETW EventSource будет объявлять все строки как завершаемые нулевым символом, несмотря на то, что в строке C# может содержаться нулевой символ. Если строка содержит пустой символ, вся строка будет записана в полезные данные события, но любой средство синтаксического анализа будет обрабатывать первый пустой символ как конец строки. Если после строки есть аргументы полезной нагрузки, оставшаяся часть строки будет проанализирована вместо предполагаемого значения.
Типичные настройки событий
Настройка уровней детализации событий
Каждое событие имеет уровень детализации и подписчики событий часто включают все события в EventSource до определенного уровня детализации. События определяют уровень детализации с помощью свойства Level. Например, в этом EventSource подписчик, который запрашивает события уровня "Информационный" и ниже, не будет регистрировать событие Verbose DebugMessage.
[EventSource(Name = "MyCompany-Samples-Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1, Level = EventLevel.Informational)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
[Event(2, Level = EventLevel.Verbose)]
public void DebugMessage(string message) => WriteEvent(2, message);
}
Если уровень детализации события не указан в EventAttribute, он по умолчанию имеет значение "Информационный".
Лучшие практики
Используйте уровни меньше, чем информационные для относительно редких предупреждений или ошибок. При сомнениях используйте режим "Информационный" по умолчанию и переключайтесь на "Подробный" для событий, которые происходят чаще 1000 раз в секунду.
Настройка ключевых слов событий
Некоторые системы трассировки событий поддерживают ключевые слова в качестве дополнительного механизма фильтрации. В отличие от детализации, которая классифицирует события по уровню детализации, ключевые слова предназначены для классификации событий на основе других критериев, таких как функциональные области кода или которые могут быть полезны для диагностики определенных проблем. Ключевые слова называются битовые флаги, и каждое событие может иметь любое сочетание ключевых слов, примененных к нему. Например, в приведенном ниже коде EventSource определяются некоторые события, связанные с обработкой запросов и другими событиями, связанными с запуском. Если разработчик хотел проанализировать производительность запуска, он может включить ведение журнала событий, помеченных ключевым словом запуска.
[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1, Keywords = Keywords.Startup)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
[Event(2, Keywords = Keywords.Requests)]
public void RequestStart(int requestId) => WriteEvent(2, requestId);
[Event(3, Keywords = Keywords.Requests)]
public void RequestStop(int requestId) => WriteEvent(3, requestId);
public class Keywords // This is a bitvector
{
public const EventKeywords Startup = (EventKeywords)0x0001;
public const EventKeywords Requests = (EventKeywords)0x0002;
}
}
Ключевые слова должны быть определены с помощью вложенного класса, называемого Keywords
, и каждое отдельное ключевое слово определяется элементом типа public const EventKeywords
.
Лучшие практики
Ключевые слова более важны при различии событий большого объема. Это позволяет потребителю событий увеличить детализацию до высокого уровня, но контролировать влияние на производительность и размер журнала, включая только узкие подмножества событий. События, которые срабатывают свыше 1000 раз в секунду, являются хорошими кандидатами для уникального идентификатора.
Поддерживаемые типы параметров
EventSource требует сериализации всех параметров события, поэтому он принимает только ограниченный набор типов. Ниже приведены следующие параметры:
- Примитивы: bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr и UIntPtr, Guid, decimal, string, DateTime, DateTimeOffset, TimeSpan
- Перечисления
- Структуры, приписываемые System.Diagnostics.Tracing.EventDataAttribute. Сериализуются только свойства общедоступного экземпляра с сериализуемыми типами.
- Анонимные типы, в которых все общедоступные свойства являются сериализуемыми типами
- Массивы сериализуемых типов
- Nullable<T>, где T является сериализуемым типом
- KeyValuePair<T, U>, где и T, и U являются сериализуемыми типами
- Типы, реализующие IEnumerable<T> для одного типа T и где T является сериализуемым типом
Устранение неполадок
Класс EventSource был разработан таким образом, чтобы он никогда не создавал исключение по умолчанию. Это полезное свойство, так как логирование часто рассматривается как необязательная задача, и вы обычно не хотите, чтобы ошибка при записи сообщения в журнал приводила к сбою вашего приложения. Однако это затрудняет поиск ошибок в EventSource. Ниже приведены несколько методов, которые помогут устранить неполадки.
- Конструктор EventSource имеет перегрузки, которые принимают EventSourceSettings. Попробуйте временно включить флаг ThrowOnEventWriteErrors.
- Свойство EventSource.ConstructionException сохраняет любое исключение, созданное при проверке методов ведения журнала событий. Это может выявить различные ошибки разработки.
- EventSource регистрирует ошибки с использованием идентификатора события 0, и это событие ошибки содержит строку, описывающую ошибку.
- При отладке эта же строка ошибки также будет зарегистрирована с помощью Debug.WriteLine() и отображается в окне вывода отладки.
- EventSource внутренне генерирует и затем перехватывает исключения при возникновении ошибок. Чтобы наблюдать за этими исключениями, включите исключения первого шанса в отладчике или используйте трассировку событий с включенными событиями исключений среды выполнения .NET.
Дополнительные настройки
Настройка OpCodes и задач
В ETW присутствуют такие понятия, как Задачи и OpCodes, которые являются дополнительными механизмами для маркировки и фильтрации событий. События можно связать с определенными задачами и опкодами с помощью свойств Task и Opcode. Ниже приведен пример:
[EventSource(Name = "Samples-EventSourceDemos-Customized")]
public sealed class CustomizedEventSource : EventSource
{
static public CustomizedEventSource Log { get; } = new CustomizedEventSource();
[Event(1, Task = Tasks.Request, Opcode=EventOpcode.Start)]
public void RequestStart(int RequestID, string Url)
{
WriteEvent(1, RequestID, Url);
}
[Event(2, Task = Tasks.Request, Opcode=EventOpcode.Info)]
public void RequestPhase(int RequestID, string PhaseName)
{
WriteEvent(2, RequestID, PhaseName);
}
[Event(3, Keywords = Keywords.Requests,
Task = Tasks.Request, Opcode=EventOpcode.Stop)]
public void RequestStop(int RequestID)
{
WriteEvent(3, RequestID);
}
public class Tasks
{
public const EventTask Request = (EventTask)0x1;
}
}
Можно неявно создавать объекты EventTask, объявляя два метода события с последующими идентификаторами событий, имеющими шаблон именования <EventName>Start и <EventName>Stop. Эти события должны быть объявлены последовательно в определении класса, и метод <EventName>Start должен быть первым.
Самоописывающиеся (трассировочные) события против форматов событий манифеста
Эта концепция имеет значение только при подписке на EventSource из ETW. ETW имеет два разных способа для регистрации событий: формат манифеста и самоописывающий (иногда называемый трассировкой). Объекты EventSource на основе манифеста создают и регистрируют XML-документ, представляющий события, определенные в классе при инициализации. Для этого требуется, чтобы EventSource выполнял отражение для создания метаданных поставщика и событий. В формате самоописания метаданные для каждого события передаются встроенно вместе с данными события, вместо того, чтобы заранее. Самоописывающий подход поддерживает более гибкие методы Write, которые могут отправлять произвольные события без создания предварительно определенного метода логирования событий. Это также немного быстрее при запуске, потому что оно избегает излишнего отражения. Однако дополнительные метаданные, создаваемые каждым событием, добавляют небольшие затраты на производительность, которые могут быть не желательными при отправке большого объема событий.
Чтобы использовать формат событий с самоописанием, создайте EventSource, воспользовавшись конструктором EventSource(String), конструктором EventSource(String, EventSourceSettings), или установив флаг EtwSelfDescribingEventFormat в EventSourceSettings.
Типы EventSource, реализующие интерфейсы
Тип EventSource может реализовать интерфейс, чтобы легко интегрироваться в различные расширенные системы ведения журнала, использующие интерфейсы для определения общего целевого объекта ведения журнала. Ниже приведен пример возможного использования:
public interface IMyLogging
{
void Error(int errorCode, string msg);
void Warning(string msg);
}
[EventSource(Name = "Samples-EventSourceDemos-MyComponentLogging")]
public sealed class MyLoggingEventSource : EventSource, IMyLogging
{
public static MyLoggingEventSource Log { get; } = new MyLoggingEventSource();
[Event(1)]
public void Error(int errorCode, string msg)
{ WriteEvent(1, errorCode, msg); }
[Event(2)]
public void Warning(string msg)
{ WriteEvent(2, msg); }
}
Необходимо указать EventAttribute в методах интерфейса, в противном случае (по соображениям совместимости) метод не будет рассматриваться как метод ведения журнала. Явная реализация метода интерфейса запрещена для предотвращения конфликтов имен.
Иерархии классов EventSource
В большинстве случаев вы сможете записывать типы, которые непосредственно являются производными от класса EventSource. Иногда полезно определить функциональные возможности, которые будут совместно использоваться несколькими производными типами EventSource, такими как настраиваемые перегрузки WriteEvent (см. оптимизацию производительности для событий большого объема ниже).
Абстрактные базовые классы можно использовать, если они не определяют ключевые слова, задачи, опкоды, каналы или события. Ниже приведен пример, в котором класс UtilBaseEventSource определяет оптимизированную перегрузку WriteEvent, необходимую для нескольких производных источников EventSource в одном компоненте. Один из этих производных типов показан ниже как OptimizedEventSource.
public abstract class UtilBaseEventSource : EventSource
{
protected UtilBaseEventSource()
: base()
{ }
protected UtilBaseEventSource(bool throwOnEventWriteErrors)
: base(throwOnEventWriteErrors)
{ }
protected unsafe void WriteEvent(int eventId, int arg1, short arg2, long arg3)
{
if (IsEnabled())
{
EventSource.EventData* descrs = stackalloc EventSource.EventData[2];
descrs[0].DataPointer = (IntPtr)(&arg1);
descrs[0].Size = 4;
descrs[1].DataPointer = (IntPtr)(&arg2);
descrs[1].Size = 2;
descrs[2].DataPointer = (IntPtr)(&arg3);
descrs[2].Size = 8;
WriteEventCore(eventId, 3, descrs);
}
}
}
[EventSource(Name = "OptimizedEventSource")]
public sealed class OptimizedEventSource : UtilBaseEventSource
{
public static OptimizedEventSource Log { get; } = new OptimizedEventSource();
[Event(1, Keywords = Keywords.Kwd1, Level = EventLevel.Informational,
Message = "LogElements called {0}/{1}/{2}.")]
public void LogElements(int n, short sh, long l)
{
WriteEvent(1, n, sh, l); // Calls UtilBaseEventSource.WriteEvent
}
#region Keywords / Tasks /Opcodes / Channels
public static class Keywords
{
public const EventKeywords Kwd1 = (EventKeywords)1;
}
#endregion
}
Оптимизация производительности для событий большого объема
Класс EventSource имеет ряд перегрузок метода WriteEvent, включая перегрузку для переменного числа аргументов. Если ни одна из других перегрузок не соответствует, вызывается метод params. К сожалению, перегрузка параметров относительно дорогостоящая. В частности:
- Выделяет массив для хранения аргументов переменной.
- Приведение каждого параметра к объекту, что приводит к выделению памяти для типов значений.
- Присваивает эти объекты массиву.
- Вызывает функцию.
- Определяет тип каждого элемента массива, чтобы определить, как его сериализовать.
Это, вероятно, от 10 до 20 раз дороже, чем специализированные типы. Это не имеет большого значения для случаев с низким объемом, но для событий с большим объемом может быть важно. Есть два важных случая, когда нужно убедиться, что перегрузка параметров не используется.
- Убедитесь, что перечисляемые типы приведены к типу int, чтобы они соответствовали одной из быстрых перегрузок.
- Создайте новые быстрые перегрузки методов WriteEvent для большого количества данных.
Ниже приведен пример добавления перегрузки WriteEvent, которая принимает четыре целочисленных аргумента.
[NonEvent]
public unsafe void WriteEvent(int eventId, int arg1, int arg2,
int arg3, int arg4)
{
EventData* descrs = stackalloc EventProvider.EventData[4];
descrs[0].DataPointer = (IntPtr)(&arg1);
descrs[0].Size = 4;
descrs[1].DataPointer = (IntPtr)(&arg2);
descrs[1].Size = 4;
descrs[2].DataPointer = (IntPtr)(&arg3);
descrs[2].Size = 4;
descrs[3].DataPointer = (IntPtr)(&arg4);
descrs[3].Size = 4;
WriteEventCore(eventId, 4, (IntPtr)descrs);
}