إن DiagnosticSource عبارة عن مجموعة بسيطة ولكنها مفيدة للغاية من واجهات برمجة التطبيقات (متوفرة في حزمة نظام NuGet. Diagnostics.DiagnosticSource) ، والتي ، من ناحية ، تسمح للمكتبات المختلفة بإرسال أحداث محددة حول عملها ، ومن ناحية أخرى ، تسمح للتطبيقات بالاشتراك في هذه الأحداث والعملية لهم.
يحتوي كل حدث من هذا القبيل على معلومات إضافية (الحمولة النافعة) ، وبما أن الأحداث تتم معالجتها في نفس عملية الإرسال ، يمكن أن تحتوي هذه المعلومات على أي كائنات تقريبًا دون الحاجة إلى إجراء تسلسل / إلغاء تسلسل.
تستخدم أداة DiagnosticSource بالفعل في AspNetCore و EntityFrameworkCore و HttpClient و SqlClient ، مما يمنح المطورين القدرة على اعتراض طلبات HTTP الواردة والصادرة وطلبات قاعدة البيانات وكائنات الوصول مثل HttpContext
و DbConnection
و DbCommand
و HttpRequestMessage
وغيرها الأشياء إذا لزم الأمر.
قررت تقسيم قصتي حول DiagnosticSource إلى مقالتين. في هذه المقالة ، سنقوم بتحليل مبدأ تشغيل الآلية بمثال بسيط ، وفي اليوم التالي سأتحدث عن الأحداث الموجودة في .NET والتي يمكن معالجتها باستخدامها وإظهار عدة أمثلة على استخدامها في OZON.ru.
مثال
لفهم كيفية عمل DiagnosticSource بشكل أفضل ، ضع في اعتبارك مثال صغير على اعتراض استعلامات قاعدة البيانات. تخيل أن لدينا تطبيق وحدة تحكم بسيط يقدم طلبًا إلى قاعدة البيانات ويعرض النتيجة في وحدة التحكم.
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)) {
للبساطة ، لقد أثرت SQL Server في حاوية عامل ميناء.
تشغيل عامل الميناء 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
الآن تخيل أن لدينا مهمة: نحن بحاجة إلى قياس وقت تنفيذ جميع الاستعلامات إلى قاعدة البيانات باستخدام Stopwatch
وعرض أزواج "طلب" - "وقت التشغيل" في وحدة التحكم.
بالطبع ، يمكنك ببساطة التفاف مكالمة QuerySingleAsync
برمز يقوم بإنشاء مثيل Stopwatch
الإيقاف وتبدأ تشغيله ، ويتوقف عن ذلك بعد تنفيذ الاستعلام ويعرض النتيجة ، ولكن هناك العديد من الصعوبات في آن واحد:
- ماذا لو كان التطبيق يحتوي على أكثر من طلب واحد؟
- ماذا لو تم بالفعل ترجمة الكود الذي ينفذ الطلب ، ومتصل بالتطبيق كحزمة NuGet ، وليس لدينا أي طريقة لتغييره؟
- ماذا لو لم يتم إجراء الاستعلام إلى قاعدة البيانات من خلال Dapper ، ولكن من خلال EntityFramework ، على سبيل المثال ، وليس لدينا إمكانية الوصول إلى كائن
DbCommand
، ولا إلى نص الاستعلام الذي تم إنشاؤه والذي سيتم تنفيذه بالفعل؟
دعنا نحاول حل هذه المشكلة باستخدام DiagnosticSource.
باستخدام حزمة NuGet System.Diagnostics.DiagnosticSource
أول ما يجب فعله بعد توصيل حزمة NuGet من System.Diagnostics.DiagnosticSource هو إنشاء فئة تعالج الأحداث التي تهمنا:
public sealed class ExampleDiagnosticObserver { }
لبدء معالجة الأحداث ، تحتاج إلى إنشاء مثيل لهذه الفئة وتسجيلها كمراقب في الكائن الثابت DiagnosticListener.AllListeners
(الموجود في مساحة اسم System.Diagnostics
). نحن نفعل هذا في بداية الوظيفة Main
:
public static async Task Main() { var observer = new ExampleDiagnosticObserver(); IDisposable subscription = DiagnosticListener.AllListeners.Subscribe(observer); var answer = await GetAnswerAsync(); Console.WriteLine(answer); }
في هذه الحالة ، IObserver<DiagnosticListener>
المحول البرمجي بحق أنه يجب على فئة IObserver<DiagnosticListener>
تطبيق IObserver<DiagnosticListener>
. لننفذه:
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() { } }
إذا قمنا بتشغيل هذا الرمز الآن ، فسنرى أنه سيتم عرض ما يلي في وحدة التحكم:
SqlClientDiagnosticListener SqlClientDiagnosticListener 42
هذا يعني أنه في مكان ما في .NET ، يتم تسجيل كائنين من نوع DiagnosticListener
يحمل الاسم "SqlClientDiagnosticListener"
والذي تم تنفيذه عند تنفيذ هذا الرمز.
ها هم على github.comيوجد ثلاثة منهم بالفعل ، لكن بما أننا لم نستخدم SqlTransaction
، فقد عمل اثنان فقط:
سيتم استدعاء IObserver<DiagnosticListener>.OnNext
مرة واحدة عند أول استخدام لكل مثيل من DiagnosticListener
الذي تم إنشاؤه في التطبيق (عادةً ما يتم إنشاؤه كخصائص ثابتة). الآن عرضنا للتو اسم مثيلات DiagnosticListener
في وحدة التحكم ، ولكن من الناحية العملية تحتاج هذه الطريقة إلى التحقق من هذا الاسم ، وإذا كنا مهتمين بمعالجة الأحداث من هذه الحالة ، فقم بالاشتراك فيها باستخدام طريقة Subscribe
.
أريد أيضًا أن أشير إلى أنه عندما نسمي DiagnosticListener.AllListeners.Subscribe
سنحصل على كائن subscription
نتيجة لذلك ، والذي ينفذ واجهة IDisposable
. سيؤدي استدعاء الأسلوب Dispose
على هذا الكائن إلى إلغاء الاشتراك ، والذي يجب تنفيذه في الأسلوب IObserver<DiagnosticListener>.OnCompleted
.
لنقم بتطبيق 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(); } }
الآن IObserver<KeyValuePair<string, object>>
المترجم أن فئة IObserver<KeyValuePair<string, object>>
يجب أن تطبق أيضًا IObserver<KeyValuePair<string, object>>
. نحن هنا بحاجة إلى تطبيق IObserver<KeyValuePair<string, object>>.OnNext
، والتي كمعلمة تقبل KeyValuePair<string, object>
، حيث المفتاح هو اسم الحدث ، والقيمة هي كائن مجهول (عادةً) مع معلمات تعسفية يمكننا استخدامها حسب تقديرها. لنقم بإضافة تطبيق لهذه الواجهة:
public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object>> {
إذا قمنا الآن بتشغيل الكود الناتج ، فسيتم عرض ما يلي في وحدة التحكم:
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
في المجموع ، سوف نرى ستة أحداث. يتم تنفيذ اثنين منهم قبل وبعد فتح الاتصال بقاعدة البيانات ، واثنين - قبل وبعد تنفيذ الأمر ، واثنين آخرين - قبل وبعد إغلاق الاتصال بقاعدة البيانات.
يحتوي كل حدث على مجموعة من المعلمات ، مثل OperationId
، و ConnectionId
، و Connection
، و Command
، والتي يتم تمريرها عادةً كخصائص لكائن مجهول. يمكنك الحصول على قيم مكتوبة لهذه الخصائص ، على سبيل المثال ، باستخدام الانعكاس. (في الممارسة العملية ، قد لا يكون استخدام الانعكاس مرغوبًا للغاية. نستخدم DynamicMethod للحصول على معلمات الحدث.)
نحن الآن على استعداد لحل المشكلة الأولية - لقياس وقت تنفيذ جميع الطلبات إلى قاعدة البيانات وعرضها في وحدة التحكم مع الطلب الأصلي.
تغيير تطبيق أسلوب Write
كما يلي:
public sealed class ExampleDiagnosticObserver : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object>> {
نحن هنا نعترض بداية ونهاية أحداث الطلب إلى قاعدة البيانات. قبل تنفيذ الطلب ، نقوم بإنشاء ساعة AsyncLocal<Stopwatch>
وبدء تشغيلها ، وحفظها في متغير من AsyncLocal<Stopwatch>
، من أجل استعادته لاحقًا. بعد تنفيذ الطلب ، نحصل على ساعة توقيت تم إطلاقها مسبقًا ، ونوقفها ، ونحصل على الأمر المنفذ من معلمة value
عبر الانعكاس ونطبع النتيجة على وحدة التحكم.
إذا قمنا الآن بتشغيل الكود الناتج ، فسيتم عرض ما يلي في وحدة التحكم:
CommandText: SELECT 42; Elapsed: 00:00:00.0341357 42
يبدو أننا قد حللنا بالفعل مشكلتنا ، ولكن لا يزال هناك تفصيل صغير. الحقيقة هي أنه عندما نشترك في أحداث DiagnosticListener
، نبدأ في تلقيها حتى تلك الأحداث التي لا تهمنا ، وحيث أن كل حدث يتم إرساله ، يتم إنشاء كائن مجهول مع المعلمات ، وهذا يمكن أن يخلق حمولة إضافية على GC.
لتجنب هذا الموقف وإخبارك بأية أحداث من DiagnosticListener
سنقوم بمعالجتها ، يمكننا تحديد مفوض خاص من نوع Predicate<string>
عند الاشتراك ، والذي يأخذ اسم الحدث كمعلمة ويعود true
إذا كان يجب معالجة هذا الحدث.
IObserver<DiagnosticListener>.OnNext
في IObserver<DiagnosticListener>.OnNext
:
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"; }
الآن سيتم استدعاء أسلوب Write
بنا فقط للأحداث "System.Data.SqlClient.WriteCommandBefore"
و "System.Data.SqlClient.WriteCommandAfter"
.
باستخدام Microsoft NuGet Package.Extensions.DiagnosticAdapter
نظرًا لأن معلمات الأحداث التي نتلقاها من DiagnosticListener
يتم تمريرها عادةً ككائن مجهول ، فإن استردادها من خلال الانعكاس يمكن أن يكون مكلفًا للغاية. لحسن الحظ ، هناك حزمة Microsoft.Extensions.DiagnosticAdapter NuGet التي يمكن أن تفعل هذا من أجلنا ، وذلك باستخدام إنشاء رمز وقت التشغيل من System.Reflection.Emit
من أجل استخدام هذه الحزمة عند الاشتراك في الأحداث من مثيل DiagnosticListener
بدلاً من طريقة Subscribe
، يجب عليك استخدام طريقة تمديد SubscWithAdapter. تطبيق IObserver<KeyValuePair<string, object>>
الواجهة في هذه الحالة لم تعد مطلوبة. بدلاً من ذلك ، بالنسبة لكل حدث نرغب في معالجته ، نحتاج إلى إعلان طريقة منفصلة ، ووضع علامة عليه مع سمة DiagnosticNameAttribute
(من مساحة الاسم Microsoft.Extensions.DiagnosticAdapter
). ستكون معلمات هذه الطرق هي معلمات الحدث قيد المعالجة.
إذا أعدنا كتابة مثال ExampleDiagnosticObserver باستخدام حزمة NuGet هذه ، فسوف نحصل على الكود التالي:
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(); } }
وبالتالي ، يمكننا الآن قياس وقت تنفيذ جميع الاستعلامات إلى قاعدة البيانات من تطبيقنا ، عملياً دون تغيير رمز التطبيق نفسه.
إنشاء مثيلات DiagnosticListener الخاصة بك
عند استخدام DiagnosticSource في الممارسة ، في معظم الحالات ، سوف تشترك في الأحداث الحالية. على الأرجح لن تضطر إلى إنشاء مثيلات خاصة من DiagnosticListener
وإرسال الأحداث الخاصة بك (فقط إذا كنت لا تقوم بتطوير أي مكتبة) ، لذلك لن أتناول هذا القسم لفترة طويلة.
لإنشاء نسخة خاصة بك من DiagnosticListener
، ستحتاج إلى إعلانها كمتغير ثابت في مكان ما في التعليمات البرمجية:
private static readonly DiagnosticSource _myDiagnosticSource = new DiagnosticListener("MyLibraty");
بعد ذلك ، لإرسال حدث ، يمكنك استخدام تصميم النموذج:
if (_myDiagnosticSource.IsEnabled("MyEvent")) _myDiagnosticSource.Write("MyEvent", new { });
لمزيد من المعلومات حول إنشاء مثيلات خاصة لـ DiagnosticListener
، راجع دليل مستخدم DiagnosticSource ، والذي يعرض أفضل الممارسات لاستخدام DiagnosticSource.
الخاتمة
المثال الذي درسناه هو بكل تأكيد مجردة للغاية ومن غير المرجح أن يكون مفيدًا في مشروع حقيقي. لكنه يوضح تمامًا كيف يمكن استخدام هذه الآلية لمراقبة وتشخيص التطبيقات الخاصة بك.
في المقالة التالية ، سأقدم قائمة بالأحداث المعروفة لي والتي يمكن معالجتها من خلال DiagnosticSource ، وأعرض بعض الأمثلة العملية لاستخدامها.