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)) {
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.
Di sini mereka ada di github.comSebenarnya ada tiga, tetapi karena kami tidak menggunakan SqlTransaction
, hanya dua yang berfungsi:
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>> {
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>> {
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 { });
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.