Menggunakan DiagnosticSource dalam .NET Core: Theory

DiagnosticSource adalah serangkaian API yang sederhana namun sangat berguna (tersedia dalam Sistem paket NuGet. Diagnostics. DiagnosticSource), yang, di satu sisi, memungkinkan berbagai perpustakaan untuk mengirim peristiwa yang disebutkan tentang pekerjaan mereka, dan di sisi lain, memungkinkan aplikasi untuk berlangganan acara dan proses ini mereka.


Setiap peristiwa tersebut mengandung informasi tambahan (payload), dan karena peristiwa diproses dalam proses yang sama dengan pengiriman, informasi ini dapat berisi hampir semua objek tanpa perlu serialisasi / deserialisasi.


DiagnosticSource sudah digunakan di AspNetCore, EntityFrameworkCore, HttpClient dan SqlClient, yang benar-benar memberikan pengembang kemampuan untuk mencegat permintaan http masuk, keluar, permintaan basis data, objek akses seperti HttpContext , DbConnection , DbCommand , HttpRequestMessage dan banyak lainnya. benda jika perlu.


Saya memutuskan untuk membagi cerita saya tentang DiagnosticSource menjadi dua artikel. Dalam artikel ini, kami akan menganalisis prinsip operasi mekanisme dengan contoh sederhana, dan di berikutnya saya akan berbicara tentang peristiwa yang ada di .NET yang dapat diproses menggunakannya dan menunjukkan beberapa contoh penggunaannya di OZON.ru.


Contoh


Untuk lebih memahami bagaimana DiagnosticSource bekerja, pertimbangkan contoh kecil mencegat kueri basis data. Bayangkan kita memiliki aplikasi konsol sederhana yang membuat permintaan ke database dan menampilkan hasilnya di konsol.


 public static class Program { public const string ConnectionString = @"Data Source=localhost;Initial Catalog=master;User ID=sa;Password=Password12!;"; public static async Task Main() { var answer = await GetAnswerAsync(); Console.WriteLine(answer); } public static async Task<int> GetAnswerAsync() { using (var connection = new SqlConnection(ConnectionString)) { // using Dapper return await connection.QuerySingleAsync<int>("SELECT 42;"); } } } 

Untuk kesederhanaan, saya mengangkat SQL Server dalam wadah buruh pelabuhan.


buruh pelabuhan lari
 docker run --rm --detach --name mssql-server \ --publish 1433:1433 \ --env ACCEPT_EULA=Y \ --env SA_PASSWORD=Password12! \ mcr.microsoft.com/mssql/server:2017-latest 

Sekarang bayangkan bahwa kita memiliki tugas: kita perlu mengukur waktu eksekusi semua pertanyaan ke database menggunakan Stopwatch dan menampilkan pasangan "Permintaan" - "Runtime" di konsol.


Tentu saja, Anda bisa membungkus panggilan QuerySingleAsync kode yang membuat dan memulai instance Stopwatch , menghentikannya setelah kueri dieksekusi dan menampilkan hasilnya, tetapi ada beberapa kesulitan sekaligus:


  • Bagaimana jika aplikasi memiliki lebih dari satu permintaan?
  • Bagaimana jika kode yang mengeksekusi permintaan sudah dikompilasi, terhubung ke aplikasi sebagai paket NuGet, dan kami tidak punya cara untuk mengubahnya?
  • Bagaimana jika kueri ke basis data tidak dilakukan melalui Dapper, tetapi melalui EntityFramework, misalnya, dan kami tidak memiliki akses ke objek DbCommand atau ke teks kueri yang dihasilkan yang sebenarnya akan dieksekusi?

Mari kita coba selesaikan masalah ini menggunakan DiagnosticSource.


Menggunakan Paket NuGet System.Diagnostics.DiagnosticSource


Hal pertama yang harus dilakukan setelah menghubungkan paket NuGet dari System.Diagnostics.DiagnosticSource adalah membuat kelas yang akan menangani acara yang menarik bagi kita:


 public sealed class ExampleDiagnosticObserver { } 

Untuk mulai memproses peristiwa, Anda harus membuat turunan dari kelas ini dan mendaftarkannya sebagai pengamat di objek statis DiagnosticListener.AllListeners (terletak di namespace System.Diagnostics System). Kami melakukan ini di awal fungsi Main :


 public static async Task Main() { var observer = new ExampleDiagnosticObserver(); IDisposable subscription = DiagnosticListener.AllListeners.Subscribe(observer); var answer = await GetAnswerAsync(); Console.WriteLine(answer); } 

Dalam kasus ini, kompiler akan memberi tahu kami dengan benar bahwa kelas ExampleDiagnosticObserver harus mengimplementasikan IObserver<DiagnosticListener> . Mari kita implementasikan:


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener> { void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { Console.WriteLine(diagnosticListener.Name); } void IObserver<DiagnosticListener>.OnError(Exception error) { } void IObserver<DiagnosticListener>.OnCompleted() { } } 

Jika kita menjalankan kode ini sekarang, kita akan melihat bahwa yang berikut ini akan ditampilkan di konsol:


 SqlClientDiagnosticListener SqlClientDiagnosticListener 42 

Ini berarti bahwa di suatu tempat di. NET dua objek dari tipe DiagnosticListener terdaftar dengan nama "SqlClientDiagnosticListener" yang berfungsi ketika kode ini dieksekusi.



IObserver<DiagnosticListener>.OnNext akan dipanggil sekali pada penggunaan pertama untuk setiap instance dari DiagnosticListener yang dibuat dalam aplikasi (biasanya mereka dibuat sebagai properti statis). Sekarang kami baru saja menampilkan nama instance DiagnosticListener di konsol, tetapi dalam praktiknya metode ini perlu memeriksa nama ini dan, jika kami tertarik untuk memproses peristiwa dari instance ini, berlangganan dengan menggunakan metode Subscribe .


Saya juga ingin mencatat bahwa ketika kita memanggil DiagnosticListener.AllListeners.Subscribe . IDisposable . subscription DiagnosticListener.AllListeners.Subscribe kita akan mendapatkan objek subscription sebagai hasilnya, yang mengimplementasikan antarmuka IDisposable . Memanggil metode Dispose pada objek ini akan menyebabkan berhenti berlangganan, yang harus diimplementasikan dalam IObserver<DiagnosticListener>.OnCompleted .


Mari kita implementasikan lagi IObserver<DiagnosticListener> :


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener> { private readonly List<IDisposable> _subscriptions = new List<IDisposable>(); void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { if (diagnosticListener.Name == "SqlClientDiagnosticListener") { var subscription = diagnosticListener.Subscribe(this); _subscriptions.Add(subscription); } } void IObserver<DiagnosticListener>.OnError(Exception error) { } void IObserver<DiagnosticListener>.OnCompleted() { _subscriptions.ForEach(x => x.Dispose()); _subscriptions.Clear(); } } 

Sekarang kompiler akan memberi tahu kita bahwa kelas ExampleDiagnosticObserver kita juga harus mengimplementasikan IObserver<KeyValuePair<string, object>> interface. Di sini kita perlu mengimplementasikan IObserver<KeyValuePair<string, object>>.OnNext , yang sebagai parameter menerima KeyValuePair<string, object> , di mana kuncinya adalah nama acara, dan nilainya adalah objek anonim (biasanya) dengan parameter acak yang dapat kita gunakan atas kebijakannya sendiri. Mari tambahkan implementasi antarmuka ini:


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object>> { // IObserver<DiagnosticListener> implementation // ... void IObserver<KeyValuePair<string, object>>.OnNext(KeyValuePair<string, object> pair) { Write(pair.Key, pair.Value); } void IObserver<KeyValuePair<string, object>>.OnError(Exception error) { } void IObserver<KeyValuePair<string, object>>.OnCompleted() { } private void Write(string name, object value) { Console.WriteLine(name); Console.WriteLine(value); Console.WriteLine(); } } 

Jika sekarang kita menjalankan kode yang dihasilkan, maka yang berikut akan ditampilkan di konsol:


 System.Data.SqlClient.WriteConnectionOpenBefore { OperationId = 3da1b5d4-9ce1-4f28-b1ff-6a5bfc9d64b8, Operation = OpenAsync, Connection = System.Data.SqlClient.SqlConnection, Timestamp = 26978341062 } System.Data.SqlClient.WriteConnectionOpenAfter { OperationId = 3da1b5d4-9ce1-4f28-b1ff-6a5bfc9d64b8, Operation = OpenAsync, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Connection = System.Data.SqlClient.SqlConnection, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 26978631500 } System.Data.SqlClient.WriteCommandBefore { OperationId = 5c6d300c-bc49-4f80-9211-693fa1e2497c, Operation = ExecuteReaderAsync, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Command = System.Data.SqlClient.SqlComman d } System.Data.SqlClient.WriteCommandAfter { OperationId = 5c6d300c-bc49-4f80-9211-693fa1e2497c, Operation = ExecuteReaderAsync, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Command = System.Data.SqlClient.SqlComman d, Statistics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 26978709490 } System.Data.SqlClient.WriteConnectionCloseBefore { OperationId = 3f6bfd8f-e5f6-48b7-82c7-41aeab881142, Operation = Close, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Connection = System.Data.SqlClient.SqlConnection, Stat istics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 26978760625 } System.Data.SqlClient.WriteConnectionCloseAfter { OperationId = 3f6bfd8f-e5f6-48b7-82c7-41aeab881142, Operation = Close, ConnectionId = 84bd0095-9831-456b-8ebc-cb9dc2017368, Connection = System.Data.SqlClient.SqlConnection, Stat istics = System.Data.SqlClient.SqlStatistics+StatisticsDictionary, Timestamp = 26978772888 } 42 

Secara total, kita akan melihat enam acara. Dua di antaranya dieksekusi sebelum dan sesudah membuka koneksi ke database, dua - sebelum dan sesudah perintah dieksekusi, dan dua lagi - sebelum dan sesudah menutup koneksi ke database.


Setiap peristiwa berisi sekumpulan parameter, seperti OperationId , ConnectionId , Connection , Command , yang biasanya dilewatkan sebagai properti dari objek anonim. Anda bisa mendapatkan nilai yang diketik untuk properti ini, misalnya, menggunakan refleksi. (Dalam praktiknya, menggunakan refleksi mungkin tidak terlalu diinginkan. Kami menggunakan DynamicMethod untuk mendapatkan parameter acara.)


Sekarang kita siap untuk menyelesaikan masalah awal - untuk mengukur waktu eksekusi semua permintaan ke database dan menampilkannya di konsol bersama dengan permintaan asli.


Ubah implementasi metode Write sebagai berikut:


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object>> { // IObserver<DiagnosticListener> implementation // ... // IObserver<KeyValuePair<string, object>> implementation // ... private readonly AsyncLocal<Stopwatch> _stopwatch = new AsyncLocal<Stopwatch>(); private void Write(string name, object value) { switch (name) { case "System.Data.SqlClient.WriteCommandBefore": { //           _stopwatch.Value = Stopwatch.StartNew(); break; } case "System.Data.SqlClient.WriteCommandAfter": { //           var stopwatch = _stopwatch.Value; stopwatch.Stop(); var command = GetProperty<SqlCommand>(value, "Command"); Console.WriteLine($"CommandText: {command.CommandText}"); Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); Console.WriteLine(); break; } } } private static T GetProperty<T>(object value, string name) { return (T) value.GetType() .GetProperty(name) .GetValue(value); } } 

Di sini kami mencegat awal dan akhir acara permintaan ke database. Sebelum menjalankan permintaan, kami membuat dan memulai stopwatch, menyimpannya dalam variabel tipe AsyncLocal<Stopwatch> , untuk mendapatkannya kembali nanti. Setelah mengeksekusi permintaan, kita mendapatkan stopwatch yang diluncurkan sebelumnya, menghentikannya, mendapatkan perintah yang dieksekusi dari parameter value melalui refleksi dan mencetak hasilnya ke konsol.


Jika sekarang kita menjalankan kode yang dihasilkan, maka yang berikut akan ditampilkan di konsol:


 CommandText: SELECT 42; Elapsed: 00:00:00.0341357 42 

Tampaknya kita telah menyelesaikan masalah kita, tetapi satu detail kecil tetap ada. Faktanya adalah bahwa ketika kita berlangganan acara DiagnosticListener , kita mulai menerima darinya bahkan peristiwa yang tidak menarik bagi kita, dan karena setiap peristiwa dikirim objek anonim dengan parameter dibuat, ini dapat membuat beban tambahan pada GC.


Untuk menghindari situasi ini dan memberi tahu Anda acara mana dari DiagnosticListener yang akan kami proses, kami dapat menentukan delegasi khusus dari tipe Predicate<string> saat berlangganan, yang menggunakan nama acara sebagai parameter dan mengembalikan true jika acara ini harus diproses.


IObserver<DiagnosticListener>.OnNext di kelas kami:


 void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { if (diagnosticListener.Name == "SqlClientDiagnosticListener") { var subscription = diagnosticListener.Subscribe(this, IsEnabled); _subscriptions.Add(subscription); } } private bool IsEnabled(string name) { return name == "System.Data.SqlClient.WriteCommandBefore" || name == "System.Data.SqlClient.WriteCommandAfter"; } 

Sekarang metode Write kita akan dipanggil hanya untuk peristiwa "System.Data.SqlClient.WriteCommandBefore" dan "System.Data.SqlClient.WriteCommandAfter" .


Menggunakan Microsoft's Paket NuGet. Ekstensi. Diagnosis Diagnosis


Karena parameter peristiwa yang kami terima dari DiagnosticListener biasanya dilewatkan sebagai objek anonim, mengambilnya melalui refleksi bisa jadi terlalu mahal. Untungnya, ada paket Microsoft.Extensions.DiagnosticAdapter NuGet yang dapat melakukan ini untuk kita, menggunakan generasi kode runtime dari System.Reflection.Emit .


Untuk menggunakan paket ini saat berlangganan acara dari instance DiagnosticListener alih-alih metode Subscribe , Anda harus menggunakan metode ekstensi SubscribeWithAdapter . Menerapkan IObserver<KeyValuePair<string, object>> dalam kasus ini tidak lagi diperlukan. Sebaliknya, untuk setiap peristiwa yang ingin kami tangani, kita perlu mendeklarasikan metode terpisah, menandainya dengan atribut DiagnosticNameAttribute (dari Microsoft.Extensions.DiagnosticAdapter namespace). Parameter metode ini akan menjadi parameter acara yang sedang diproses.


Jika kami menulis ulang kelas ExampleDiagnosticObserver kami menggunakan paket NuGet ini, kami mendapatkan kode berikut:


 public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener> { private readonly List<IDisposable> _subscriptions = new List<IDisposable>(); void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { if (diagnosticListener.Name == "SqlClientDiagnosticListener") { var subscription = diagnosticListener.SubscribeWithAdapter(this); _subscriptions.Add(subscription); } } void IObserver<DiagnosticListener>.OnError(Exception error) { } void IObserver<DiagnosticListener>.OnCompleted() { _subscriptions.ForEach(x => x.Dispose()); _subscriptions.Clear(); } private readonly AsyncLocal<Stopwatch> _stopwatch = new AsyncLocal<Stopwatch>(); [DiagnosticName("System.Data.SqlClient.WriteCommandBefore")] public void OnCommandBefore() { _stopwatch.Value = Stopwatch.StartNew(); } [DiagnosticName("System.Data.SqlClient.WriteCommandAfter")] public void OnCommandAfter(DbCommand command) { var stopwatch = _stopwatch.Value; stopwatch.Stop(); Console.WriteLine($"CommandText: {command.CommandText}"); Console.WriteLine($"Elapsed: {stopwatch.Elapsed}"); Console.WriteLine(); } } 

Dengan demikian, kita sekarang dapat mengukur waktu pelaksanaan semua pertanyaan ke database dari aplikasi kita, praktis tanpa mengubah kode aplikasi itu sendiri.


Membuat Instance DiagnosticListener Anda Sendiri


Saat menggunakan DiagnosticSource dalam praktiknya, dalam kebanyakan kasus Anda akan berlangganan acara yang ada. Anda kemungkinan besar tidak perlu membuat instance sendiri dari DiagnosticListener dan mengirim acara Anda sendiri (hanya jika Anda tidak mengembangkan perpustakaan apa pun), jadi saya tidak akan berkutat pada bagian ini untuk waktu yang lama.


Untuk membuat instance Anda sendiri dari DiagnosticListener Anda harus mendeklarasikannya sebagai variabel statis di suatu tempat dalam kode:


 private static readonly DiagnosticSource _myDiagnosticSource = new DiagnosticListener("MyLibraty"); 

Setelah itu, untuk mengirim acara, Anda dapat menggunakan desain formulir


 if (_myDiagnosticSource.IsEnabled("MyEvent")) _myDiagnosticSource.Write("MyEvent", new { /* parameters */ }); 

Untuk informasi lebih lanjut tentang cara membuat instance DiagnosticListener Anda sendiri, lihat Panduan Pengguna DiagnosticSource , yang merinci Praktik Terbaik untuk menggunakan DiagnosticSource.


Kesimpulan


Contoh yang kami teliti tentu sangat abstrak dan tidak mungkin berguna dalam proyek nyata. Tetapi ini dengan sempurna menunjukkan bagaimana mekanisme ini dapat digunakan untuk memantau dan mendiagnosis aplikasi Anda.


Pada artikel selanjutnya, saya akan memberikan daftar peristiwa yang saya ketahui yang dapat diproses melalui DiagnosticSource, dan menunjukkan beberapa contoh praktis penggunaannya.

Source: https://habr.com/ru/post/id435896/


All Articles