Прочитать на английском

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


DiagnosticSource и DiagnosticListener

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

System.Diagnostics.DiagnosticSource — это модуль, позволяющий инструментировать код для ведения журнала полезных данных в рабочей среде для использования в процессе инструментирования. Во время выполнения потребители могут динамически обнаруживать источники данных и подписываться на интересующие их источники. System.Diagnostics.DiagnosticSource разработан для предоставления средствам внутрипроцессного доступа к богатым данным. При использовании System.Diagnostics.DiagnosticSourceпредполагается, что потребитель находится в том же процессе и в результате может передаваться несериализируемые типы (например, HttpResponseMessage или HttpContext) предоставляющие клиентам много данных для работы.

Начало работы с DiagnosticSource

В этом пошаговом руководстве показано, как создать событие DiagnosticSource и код инструментирования с System.Diagnostics.DiagnosticSourceпомощью . Затем он объясняет, как использовать событие, найдя интересные средства ДиагностикиListeners, подписавшись на их события и декодируя полезные данные событий. Она завершается путем описания фильтрации, которая позволяет передавать только определенные события через систему.

Реализация DiagnosticSource

Вы будете работать со следующим кодом. Этот код представляет собой класс HttpClient с методом SendWebRequest , который отправляет HTTP-запрос в URL-адрес и получает ответ.

C#
using System.Diagnostics;
MyListener TheListener = new MyListener();
TheListener.Listening();
HTTPClient Client = new HTTPClient();
Client.SendWebRequest("https://learn.microsoft.com/dotnet/core/diagnostics/");

class HTTPClient
{
    private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
    public byte[] SendWebRequest(string url)
    {
        if (httpLogger.IsEnabled("RequestStart"))
        {
            httpLogger.Write("RequestStart", new { Url = url });
        }
        //Pretend this sends an HTTP request to the url and gets back a reply.
        byte[] reply = new byte[] { };
        return reply;
    }
}
class Observer<T> : IObserver<T>
{
    public Observer(Action<T> onNext, Action onCompleted)
    {
        _onNext = onNext ?? new Action<T>(_ => { });
        _onCompleted = onCompleted ?? new Action(() => { });
    }
    public void OnCompleted() { _onCompleted(); }
    public void OnError(Exception error) { }
    public void OnNext(T value) { _onNext(value); }
    private Action<T> _onNext;
    private Action _onCompleted;
}
class MyListener
{
    IDisposable networkSubscription;
    IDisposable listenerSubscription;
    private readonly object allListeners = new();
    public void Listening()
    {
        Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
        {
            Console.WriteLine($"Data received: {data.Key}: {data.Value}");
        };
        Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
        {
            Console.WriteLine($"New Listener discovered: {listener.Name}");
            //Subscribe to the specific DiagnosticListener of interest.
            if (listener.Name == "System.Net.Http")
            {
                //Use lock to ensure the callback code is thread safe.
                lock (allListeners)
                {
                    if (networkSubscription != null)
                    {
                        networkSubscription.Dispose();
                    }
                    IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                    networkSubscription = listener.Subscribe(iobserver);
                }

            }
        };
        //Subscribe to discover all DiagnosticListeners
        IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
        //When a listener is created, invoke the onNext function which calls the delegate.
        listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
    }
    // Typically you leave the listenerSubscription subscription active forever.
    // However when you no longer want your callback to be called, you can
    // call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
}

Запуск предоставленной реализации выводится на консоль.

Консоль
New Listener discovered: System.Net.Http
Data received: RequestStart: { Url = https://learn.microsoft.com/dotnet/core/diagnostics/ }

Регистрация события

Тип DiagnosticSource — это абстрактный базовый класс, определяющий методы, необходимые для журналов событий. Класс, содержащий реализацию DiagnosticListener. Первым шагом в инструментировании кода DiagnosticSource является создание DiagnosticListenerкода. Например:

C#
private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");

Обратите внимание, что httpLogger типичен как .DiagnosticSource Это связано с тем, что этот код записывает только события и поэтому связан только с DiagnosticSource методами, DiagnosticListener реализующими. DiagnosticListeners присваиваются имена при создании, и это имя должно быть именем логической группировки связанных событий (обычно компонент). Позже это имя используется для поиска прослушивателя и подписки на любой из его событий. Таким образом, имена событий должны быть уникальными только в компоненте.


Интерфейс DiagnosticSource ведения журнала состоит из двух методов:

C#
    bool IsEnabled(string name)
    void Write(string name, object value);

Это инструмент для конкретного сайта. Чтобы узнать, какие типы передаются вIsEnabled, необходимо проверка сайт инструментирования. Это дает вам информацию, чтобы узнать, к чему приведение полезных данных.

Типичный сайт вызова будет выглядеть следующим образом:

C#
if (httpLogger.IsEnabled("RequestStart"))
{
    httpLogger.Write("RequestStart", new { Url = url });
}

Каждое string событие имеет имя (например, RequestStart) и ровно одно object как полезные данные. Если вам нужно отправить несколько элементов, это можно сделать, создав object свойства для представления всех его сведений. Функция анонимного типа C#обычно используется для создания типа для передачи "на лету" и делает эту схему очень удобной. На сайте инструментирования необходимо защитить вызов Write() с IsEnabled() помощью проверка с тем же именем события. Без этого проверка, даже если инструментирование неактивно, правила языка C# требуют всей работы по созданию полезных данных object и вызовуWrite(), даже если ничего не прослушивает данные. Заключив Write() вызов, вы делаете его эффективным, если источник не включен.

Объединение всего, что у вас есть:

C#
class HTTPClient
{
    private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
    public byte[] SendWebRequest(string url)
    {
        if (httpLogger.IsEnabled("RequestStart"))
        {
            httpLogger.Write("RequestStart", new { Url = url });
        }
        //Pretend this sends an HTTP request to the url and gets back a reply.
        byte[] reply = new byte[] { };
        return reply;
    }
}

Обнаружение диагностических списков

Первым шагом в получении событий является обнаружение интересующих DiagnosticListeners вас событий. DiagnosticListener поддерживает способ обнаружения DiagnosticListeners , которые активны в системе во время выполнения. API для выполнения этого является свойством AllListeners .

Observer<T> Реализуйте класс, наследующий от IObservable интерфейса, который является версией интерфейса обратного IEnumerable вызова. Дополнительные сведения см. на сайте "Реактивные расширения". Имеет IObserver три обратных вызова, OnNextи OnCompleteOnError. Имеет IObservable один метод, который Subscribe вызывается, который передает один из этих наблюдателей. После подключения наблюдатель получает обратные вызовы (в основном OnNext обратные вызовы) при выполнении действий.

Обычное AllListeners использование статического свойства выглядит следующим образом:

C#
class Observer<T> : IObserver<T>
{
    public Observer(Action<T> onNext, Action onCompleted)
    {
        _onNext = onNext ?? new Action<T>(_ => { });
        _onCompleted = onCompleted ?? new Action(() => { });
    }
    public void OnCompleted() { _onCompleted(); }
    public void OnError(Exception error) { }
    public void OnNext(T value) { _onNext(value); }
    private Action<T> _onNext;
    private Action _onCompleted;
}
class MyListener
{
    IDisposable networkSubscription;
    IDisposable listenerSubscription;
    private readonly object allListeners = new();
    public void Listening()
    {
        Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
        {
            Console.WriteLine($"Data received: {data.Key}: {data.Value}");
        };
        Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
        {
            Console.WriteLine($"New Listener discovered: {listener.Name}");
            //Subscribe to the specific DiagnosticListener of interest.
            if (listener.Name == "System.Net.Http")
            {
                //Use lock to ensure the callback code is thread safe.
                lock (allListeners)
                {
                    if (networkSubscription != null)
                    {
                        networkSubscription.Dispose();
                    }
                    IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                    networkSubscription = listener.Subscribe(iobserver);
                }

            }
        };
        //Subscribe to discover all DiagnosticListeners
        IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
        //When a listener is created, invoke the onNext function which calls the delegate.
        listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
    }
    // Typically you leave the listenerSubscription subscription active forever.
    // However when you no longer want your callback to be called, you can
    // call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
}

Этот код создает делегат обратного вызова и, используя метод, запрашивает вызов делегата AllListeners.Subscribe для каждого активного DiagnosticListener в системе. Решение о том, следует ли подписаться на прослушиватель, проверяя его имя. Приведенный выше код ищет прослушиватель System.Net.Http, созданный ранее.

Как и все вызовы Subscribe(), этот возвращает объект IDisposable , представляющий саму подписку. Обратные вызовы будут продолжаться до тех пор, пока ничего не вызывается Dispose() в этом объекте подписки. Пример кода никогда не вызывается Dispose(), поэтому он будет получать обратные вызовы навсегда.

При подписке AllListenersна вы получаете обратный вызов для ALL ACTIVE DiagnosticListeners. Таким образом, при подписке вы получаете простой обратный вызов для всех существующих DiagnosticListeners, и по мере создания новых, вы получаете обратный вызов для тех, а также. Вы получите полный список всех возможных подписок.

Подписка на DiagnosticListeners

Интерфейс DiagnosticListener реализуется IObservable<KeyValuePair<string, object>> , поэтому его можно также вызвать Subscribe() . В следующем коде показано, как заполнить предыдущий пример:

C#
IDisposable networkSubscription;
IDisposable listenerSubscription;
private readonly object allListeners = new();
public void Listening()
{
    Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
    {
        Console.WriteLine($"Data received: {data.Key}: {data.Value}");
    };
    Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
    {
        Console.WriteLine($"New Listener discovered: {listener.Name}");
        //Subscribe to the specific DiagnosticListener of interest.
        if (listener.Name == "System.Net.Http")
        {
            //Use lock to ensure the callback code is thread safe.
            lock (allListeners)
            {
                if (networkSubscription != null)
                {
                    networkSubscription.Dispose();
                }
                IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                networkSubscription = listener.Subscribe(iobserver);
            }

        }
    };
    //Subscribe to discover all DiagnosticListeners
    IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
    //When a listener is created, invoke the onNext function which calls the delegate.
    listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
}

В этом примере после поиска System.Net.Http DiagnosticListenerсоздается действие, которое выводит имя прослушивателя, события и payload.ToString().

Примечание

DiagnosticListener реализует IObservable<KeyValuePair<string, object>>. Это означает, что при каждом обратном вызове KeyValuePairмы получаем . Ключом этой пары является имя события, а значение — полезные данные object. В примере просто записывается эта информация в консоль.

Важно следить за подписками.DiagnosticListener В предыдущем коде networkSubscription переменная запоминает это. Если вы формируете другой creation, необходимо отменить подписку на предыдущий прослушиватель и подписаться на новую.

Код DiagnosticSource/DiagnosticListener является потокобезопасной, но код обратного вызова также должен быть потокобезопасн. Чтобы убедиться, что код обратного вызова является потокобезопасной, используются блокировки. Одновременно можно создать два с одинаковым DiagnosticListeners именем. Чтобы избежать условий гонки, обновления общих переменных выполняются при защите блокировки.

После выполнения предыдущего кода при следующем Write() выполнении файла System.Net.Http DiagnosticListener данные будут записаны в консоль.

Подписки не зависят друг от друга. В результате другой код может сделать точно то же самое, что и пример кода, и создать два "канала" сведений ведения журнала.

Декодирование полезных данных

Обратный KeyvaluePair вызов передает имя события и полезные данные, но полезные данные введите просто как полезные objectданные. Существует два способа получения более конкретных данных:

Если полезные данные являются хорошо известным типом (например, a stringили an HttpMessageRequest), то можно просто привести object его к ожидаемому типу (с помощью as оператора, чтобы не вызвать исключение при неправильном) и получить доступ к полям. Это очень эффективно.

Используйте API отражения. Например, предположим, что следующий метод присутствует.

C#
    /// Define a shortcut method that fetches a field of a particular name.
    static class PropertyExtensions
    {
        static object GetProperty(this object _this, string propertyName)
        {
            return _this.GetType().GetTypeInfo().GetDeclaredProperty(propertyName)?.GetValue(_this);
        }
    }

Чтобы декодировать полезные данные более полно, можно заменить listener.Subscribe() вызов следующим кодом.

C#
    networkSubscription = listener.Subscribe(delegate(KeyValuePair<string, object> evnt) {
        var eventName = evnt.Key;
        var payload = evnt.Value;
        if (eventName == "RequestStart")
        {
            var url = payload.GetProperty("Url") as string;
            var request = payload.GetProperty("Request");
            Console.WriteLine("Got RequestStart with URL {0} and Request {1}", url, request);
        }
    });

Обратите внимание, что использование отражения относительно дорого. Однако использование отражения является единственным вариантом, если полезные данные были созданы с помощью анонимных типов. Это может быть уменьшено путем быстрого получения специализированных свойств с помощью PropertyInfo.GetMethod.CreateDelegate() или System.Reflection.Emit пространства имен, но это выходит за рамки область этой статьи. (Пример быстрого получения свойства на основе делегатов см. в разделе . Класс PropertySpec , используемый DiagnosticSourceEventSourceв .)

Фильтрация

В предыдущем примере код использует IObservable.Subscribe() метод для подключения обратного вызова. Это приводит к тому, что все события будут переданы обратному вызову. Однако имеет перегрузкиSubscribe(), DiagnosticListener позволяющие контроллеру управлять заданными событиями.

Вызов listener.Subscribe() в предыдущем примере можно заменить следующим кодом, чтобы продемонстрировать.

C#
    // Create the callback delegate.
    Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) =>
        Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString());

    // Turn it into an observer (using the Observer<T> Class above).
    Observer<KeyValuePair<string, object>> observer = new Observer<KeyValuePair<string, object>>(callback);

    // Create a predicate (asks only for one kind of event).
    Predicate<string> predicate = (string eventName) => eventName == "RequestStart";

    // Subscribe with a filter predicate.
    IDisposable subscription = listener.Subscribe(observer, predicate);

    // subscription.Dispose() to stop the callbacks.

Это эффективно подписывается только на события RequestStart. Все остальные события вызывают DiagnosticSource.IsEnabled() возврат false метода и, таким образом, будут эффективно отфильтрованы.

Примечание

Фильтрация предназначена только в качестве оптимизации производительности. Прослушиватель может получать события, даже если они не удовлетворяют фильтру. Это может произойти, так как другой прослушиватель подписался на событие или так как источник события не проверка IsEnabled() перед отправкой. Если вы хотите убедиться, что данное событие удовлетворяет фильтру, необходимо проверка его внутри обратного вызова. Например:

C#
    Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) =>
        {
            if(predicate(evnt.Key)) // only print out events that satisfy our filter
            {
                Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString());
            }
        };
Фильтрация на основе контекста

Для некоторых сценариев требуется расширенная фильтрация на основе расширенного контекста. Производители могут вызывать DiagnosticSource.IsEnabled перегрузки и предоставлять дополнительные свойства событий, как показано в следующем коде.

C#
//aRequest and anActivity are the current request and activity about to be logged.
if (httpLogger.IsEnabled("RequestStart", aRequest, anActivity))
    httpLogger.Write("RequestStart", new { Url="http://clr", Request=aRequest });

В следующем примере кода показано, что потребители могут использовать такие свойства для более точного фильтрации событий.

C#
    // Create a predicate (asks only for Requests for certain URIs)
    Func<string, object, object, bool> predicate = (string eventName, object context, object activity) =>
    {
        if (eventName == "RequestStart")
        {
            if (context is HttpRequestMessage request)
            {
                return IsUriEnabled(request.RequestUri);
            }
        }
        return false;
    }

    // Subscribe with a filter predicate
    IDisposable subscription = listener.Subscribe(observer, predicate);

Производители не знают о фильтре, предоставленном потребителем. DiagnosticListener вызовет предоставленный фильтр, пропуская дополнительные аргументы при необходимости, поэтому фильтр должен ожидать получения контекста null . Если производитель вызывает IsEnabled() имя события и контекст, эти вызовы заключены в перегрузку, которая принимает только имя события. Потребители должны убедиться, что их фильтр позволяет событиям без контекста передаваться.