Поделиться через


Инструментирование кода для создания событий EventSource

Эта статья относится к : ✔️ .NET Core 3.1 и более поздних✔️ версий платформа .NET Framework версии 4.5 и более поздних версий.

В руководстве по началу работы показано, как создать минимальный код EventSource и собрать события в файле трассировки. В этом руководстве подробно описывается создание событий с помощью 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представляет этот одноэлементный объект.

Правила определения методов событий

  1. Любой экземпляр, не виртуальный, возвращающий пустоту метод, определенный в классе EventSource, по умолчанию является методом ведения журнала событий.
  2. Виртуальные или непустые возвращаемые методы включаются только в том случае, если они помечены с помощью System.Diagnostics.Tracing.EventAttribute
  3. Чтобы пометить подходящий метод как не ведения журнала, его необходимо декорировать с помощью System.Diagnostics.Tracing.NonEventAttribute
  4. Методы ведения журнала событий имеют идентификаторы событий, связанные с ними. Это можно сделать явно, декорируя метод или System.Diagnostics.Tracing.EventAttribute неявно порядковый номер метода в классе. Например, при использовании неявного нумерация первого метода в классе имеет идентификатор 1, второй имеет идентификатор 2 и т. д.
  5. Методы ведения журнала событий должны вызывать или WriteEventWithRelatedActivityIdWriteEventCoreWriteEventWithRelatedActivityIdCore перегрузку.WriteEvent
  6. Идентификатор события, подразумеваемый или явный, должен соответствовать первому аргументу, переданного в API WriteEvent*, который он вызывает.
  7. Число, типы и порядок аргументов, передаваемых методу EventSource, должны соответствовать тому, как они передаются в API WriteEvent*. Для WriteEvent аргументы следуют идентификатору события, для WriteEventWithRelatedActivityId аргументы следуют связанному ИдентификаторуActivityId. Для методов WriteEvent*Core аргументы должны быть сериализованы вручную в data параметр.
  8. Имена событий не могут содержать < или > символы. Хотя определяемые пользователем методы также не могут содержать эти символы, async методы будут перезаписаны компилятором для их хранения. Чтобы убедиться, что эти созданные методы не становятся событиями, помечайте все методы, не являющиеся событиями в объекте EventSourceNonEventAttribute.

Рекомендации

  1. Типы, производные от EventSource, обычно не имеют промежуточных типов в иерархии или реализуют интерфейсы. Дополнительные настройки см. ниже для некоторых исключений, где это может быть полезно.
  2. Как правило, имя класса EventSource является плохим общедоступным именем для EventSource. Общедоступные имена, имена, которые будут отображаться в конфигурациях ведения журнала и средствах просмотра журналов, должны быть глобально уникальными. Таким образом, рекомендуется предоставить имя EventSource общедоступному имени с помощью System.Diagnostics.Tracing.EventSourceAttribute. Имя "Демонстрация", используемое выше, является коротким и вряд ли будет уникальным, поэтому не является хорошим выбором для использования в рабочей среде. Обычное соглашение — использовать иерархическое имя с разделителем или - в . качестве разделителя, например MyCompany-Samples-Demo, или имя сборки или пространства имен, для которого eventSource предоставляет события. Не рекомендуется включать EventSource в качестве части общедоступного имени.
  3. Назначьте идентификаторы событий явным образом, таким образом, казалось бы, доброкачественные изменения в коде в исходном классе, например переупорядочение или добавление метода в середине, не изменит идентификатор события, связанный с каждым методом.
  4. При создании событий, представляющих начало и конец работы, по соглашению эти методы называются суффиксами Start и Stop. Например, RequestStart и RequestStop.
  5. Не указывайте явное значение для свойства Guid EventSourceAttribute, если только не требуется для обратной совместимости. Значение GUID по умолчанию является производным от имени источника, что позволяет инструментам принимать более читаемое пользователем имя и наследовать тот же GUID.
  6. Вызов IsEnabled() перед выполнением любой ресурсоемкой работы, связанной с запуском события, например вычисление дорогостоящих аргументов событий, которые не потребуются, если событие отключено.
  7. Попытайтесь обеспечить совместимость объекта EventSource и их версию соответствующим образом. Версия по умолчанию для события — 0. Версия может быть изменена с помощью параметра EventAttribute.Version. Измените версию события при изменении данных, сериализованных с ним. Всегда добавляйте новые сериализованные данные в конец объявления события, то есть в конце списка параметров метода. Если это невозможно, создайте событие с новым идентификатором, чтобы заменить старый.
  8. При объявлении методов событий укажите данные полезных данных фиксированного размера перед изменяющимся размером данных.
  9. Не используйте строки, содержащие пустые символы. При создании манифеста для ETW EventSource будет объявлять все строки как завершаемые null, несмотря на то, что в строке 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которыми связаны. Сериализуются только свойства общедоступного экземпляра с сериализуемыми типами.
  • Анонимные типы, в которых все общедоступные свойства являются сериализуемыми типами
  • Массивы сериализуемых типов
  • <Значение NULL, где T> является сериализуемым типом
  • KeyValuePair<T, где> T и вы оба сериализуемых типа
  • Типы, реализующие IEnumerable<T для одного типа T> и где T является сериализуемым типом

Устранение неполадок

Класс EventSource был разработан таким образом, чтобы он никогда не создавал исключение по умолчанию. Это полезное свойство, так как ведение журнала часто обрабатывается как необязательное, и обычно не требуется, чтобы сообщение журнала приводило к сбою приложения. Однако это затрудняет поиск ошибок в EventSource. Ниже приведены несколько методов, которые помогут устранить неполадки.

  1. Конструктор EventSource имеет перегрузки, которые принимают EventSourceSettings. Попробуйте временно включить флаг ThrowOnEventWriteErrors.
  2. Свойство EventSource.ConstructionException сохраняет любое исключение, созданное при проверке методов ведения журнала событий. Это может выявить различные ошибки разработки.
  3. EventSource регистрирует ошибки с использованием идентификатора события 0, и это событие ошибки содержит строку, описывающую ошибку.
  4. При отладке эта же строка ошибки также будет зарегистрирована с помощью Debug.WriteLine() и отображается в окне вывода отладки.
  5. 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, EventSource Параметры) или задав флаг EtwSelfDescribingEventFormat в EventSource Параметры.

Типы 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. К сожалению, перегрузка парам является относительно дорогой. В частности:

  1. Выделяет массив для хранения аргументов переменной.
  2. Приведение каждого параметра к объекту, что приводит к выделению для типов значений.
  3. Назначает эти объекты массиву.
  4. Вызывает функцию.
  5. Определяет тип каждого элемента массива, чтобы определить, как его сериализовать.

Это, вероятно, от 10 до 20 раз дороже, чем специализированные типы. Это не имеет большого значения для случаев с низким объемом, но для событий с большим объемом может быть важно. Существует два важных случая для того, чтобы заверять, что перегрузка парамс не используется:

  1. Убедитесь, что перечисленные типы имеют значение int, чтобы они соответствовали одному из быстрых перегрузок.
  2. Создайте новые быстрые перегрузки 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);
}