فصل ۱۳: Event Log، Performance Counter، Stopwatch و ابزارهای Cross-Platform
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
فصل ۱۳: Windows Event Log، Performance Counterها و ابزارهای Cross-Platform
ادامهٔ مثال نوشتن در Windows Event Log:
const string SourceName = "MyCompany.WidgetServer";
// CreateEventSource requires administrative permissions, so this would
// typically be done in application setup.
if (!EventLog.SourceExists (SourceName))
EventLog.CreateEventSource (SourceName, "Application");
EventLog.WriteEntry (SourceName,
"Service started; using configuration file=...",
EventLogEntryType.Information);
EventLogEntryType میتواند Information، Warning، Error، SuccessAudit یا FailureAudit باشد. هرکدام با Icon متفاوتی در Windows Event Viewer نمایش داده میشوند. همچنین میتوانید Category و Event ID اختیاری مشخص کنید ـ هرکدام Number انتخابی خودتان ـ و Binary Data اختیاری نیز ارائه دهید.
CreateEventSource اجازه میدهد Machine Name نیز مشخص کنید؛ برای نوشتن در Event Log Computer دیگری، در صورتی که Permission کافی داشته باشید.
خواندن Event Log
برای خواندن Event Log، Class با نام EventLog را با Name مربوط به Log موردنظر Instantiate کنید و بهصورت اختیاری Name Computer دیگری را که Log روی آن قرار دارد بدهید. سپس هر Log Entry از طریق Property با نام Entries خوانده میشود:
EventLog log = new EventLog ("Application");
Console.WriteLine ("Total entries: " + log.Entries.Count);
EventLogEntry last = log.Entries [log.Entries.Count - 1];
Console.WriteLine ("Index: " + last.Index);
Console.WriteLine ("Source: " + last.Source);
Console.WriteLine ("Type: " + last.EntryType);
Console.WriteLine ("Time: " + last.TimeWritten);
Console.WriteLine ("Message: " + last.Message);
میتوانید همهٔ Logها را برای Computer فعلی یا Computer دیگری با Method استاتیک EventLog.GetEventLogs Enumerate کنید؛ برای دسترسی کامل Permission مدیریتی لازم است:
foreach (EventLog log in EventLog.GetEventLogs())
Console.WriteLine (log.LogDisplayName);
این معمولاً حداقل Application، Security و System را چاپ میکند.
Monitoring Event Log
از طریق Event با نام EntryWritten میتوانید هر زمان Entryای در Windows Event Log نوشته شد Alert دریافت کنید. این قابلیت برای Event Logهای Computer محلی کار میکند و مستقل از اینکه کدام Application Event را Log کرده است Fire میشود.
برای فعالکردن Monitoring مربوط به Log:
- یک
EventLog Instantiate کنید و Property با نام EnableRaisingEvents را true قرار دهید. - Event با نام
EntryWritten را Handle کنید.
مثال:
using (var log = new EventLog ("Application"))
{
log.EnableRaisingEvents = true;
log.EntryWritten += DisplayEntry;
Console.ReadLine();
}
void DisplayEntry (object sender, EntryWrittenEventArgs e)
{
EventLogEntry entry = e.Entry;
Console.WriteLine (entry.Message);
}
Performance Counterها
مکانیزمهای Logging که تاکنون بررسی کردیم برای ثبت Information جهت Analysis آینده مفیدند. اما برای فهم State فعلی یک Application یا کل System، رویکرد Real-timeتری لازم است. راهحل Win32 برای این نیاز Infrastructure مربوط به Performance Monitoring است؛ شامل مجموعهای از Performance Counterها که System و Applicationها Expose میکنند و Snap-inهای Microsoft Management Console یا MMC که برای Monitor کردن Real-time این Counterها استفاده میشوند.
Performance Counterها در Categoryهایی مانند System، Processor، .NET CLR Memory و غیره Group میشوند. GUI Toolها گاهی این Categoryها را «Performance Object» مینامند. هر Category مجموعهٔ مرتبطی از Counterها را Group میکند که یک جنبه از System یا Application را Monitor میکنند. نمونههای Counter در Category با نام .NET CLR Memory عبارتاند از % Time in GC، # Bytes in All Heaps و Allocated bytes/sec.
هر Category میتواند بهصورت اختیاری یک یا چند Instance داشته باشد که مستقل Monitor شوند. برای مثال Counter با نام % Processor Time در Category با نام Processor برای Monitor کردن CPU Utilization مفید است. روی Machine چندپردازندهای، این Counter برای هر CPU یک Instance دارد تا Utilization هر CPU را جداگانه ببینید.
بخشهای بعدی کارهای رایج مانند مشخصکردن Counterهای Exposeشده، Monitor کردن یک Counter و ساخت Counterهای خودتان برای Expose کردن Status Application را نشان میدهند.
Enumerate کردن Counterهای موجود
مثال زیر همهٔ Performance Counterهای موجود روی Computer را Enumerate میکند. برای Categoryهایی که Instance دارند، Counterهای هر Instance را نیز Enumerate میکند:
PerformanceCounterCategory[] cats =
PerformanceCounterCategory.GetCategories();
foreach (PerformanceCounterCategory cat in cats)
{
Console.WriteLine ("Category: " + cat.CategoryName);
string[] instances = cat.GetInstanceNames();
if (instances.Length == 0)
{
foreach (PerformanceCounter ctr in cat.GetCounters())
Console.WriteLine (" Counter: " + ctr.CounterName);
}
else // Dump counters with instances
{
foreach (string instance in instances)
{
Console.WriteLine (" Instance: " + instance);
if (cat.InstanceExists (instance))
foreach (PerformanceCounter ctr in cat.GetCounters (instance))
Console.WriteLine (" Counter: " + ctr.CounterName);
}
}
}
مثال بعد با LINQ فقط .NET Performance Counterها را میگیرد و Result را در XML File مینویسد:
var x =
new XElement ("counters",
from PerformanceCounterCategory cat in
PerformanceCounterCategory.GetCategories()
where cat.CategoryName.StartsWith (".NET")
let instances = cat.GetInstanceNames()
select new XElement ("category",
new XAttribute ("name", cat.CategoryName),
instances.Length == 0
?
from c in cat.GetCounters()
select new XElement ("counter",
new XAttribute ("name", c.CounterName))
:
from i in instances
select new XElement ("instance", new XAttribute ("name", i),
!cat.InstanceExists (i)
?
null
:
from c in cat.GetCounters (i)
select new XElement ("counter",
new XAttribute ("name", c.CounterName))
)
)
);
x.Save ("counters.xml");
خواندن Data مربوط به Performance Counter
برای گرفتن Value یک Performance Counter، Object با نام PerformanceCounter را Instantiate و سپس Method با نام NextValue یا NextSample را فراخوانی کنید. NextValue یک Float ساده برمیگرداند؛ NextSample یک CounterSample میدهد که Propertyهای پیشرفتهتری مانند CounterFrequency، TimeStamp، BaseValue و RawValue را Expose میکند.
Constructor مربوط به PerformanceCounter، Category Name، Counter Name و Instance اختیاری میگیرد. بنابراین برای نمایش Processor Utilization فعلی همهٔ CPUها:
using PerformanceCounter pc = new PerformanceCounter ("Processor",
"% Processor Time",
"_Total");
Console.WriteLine (pc.NextValue());
یا برای نمایش مصرف Memory «واقعی» ـ Private ـ Process فعلی:
string procName = Process.GetCurrentProcess().ProcessName;
using PerformanceCounter pc = new PerformanceCounter ("Process",
"Private Bytes",
procName);
Console.WriteLine (pc.NextValue());
PerformanceCounter Event با نام ValueChanged Expose نمیکند، بنابراین برای Monitor کردن Change باید Poll کنید. در مثال بعد هر 200 ms Poll میکنیم تا با یک EventWaitHandle Signal مربوط به Quit دریافت شود:
// need to import System.Threading as well as System.Diagnostics
static void Monitor (string category, string counter, string instance,
EventWaitHandle stopper)
{
if (!PerformanceCounterCategory.Exists (category))
throw new InvalidOperationException ("Category does not exist");
if (!PerformanceCounterCategory.CounterExists (counter, category))
throw new InvalidOperationException ("Counter does not exist");
if (instance == null) instance = ""; // "" == no instance (not null!)
if (instance != "" &&
!PerformanceCounterCategory.InstanceExists (instance, category))
throw new InvalidOperationException ("Instance does not exist");
float lastValue = 0f;
using (PerformanceCounter pc = new PerformanceCounter (category,
counter, instance))
while (!stopper.WaitOne (200, false))
{
float value = pc.NextValue();
if (value != lastValue) // Only write out the value
{ // if it has changed.
Console.WriteLine (value);
lastValue = value;
}
}
}
این Method را میتوان برای Monitor همزمان Activity مربوط به Processor و Hard Drive چنین استفاده کرد:
EventWaitHandle stopper = new ManualResetEvent (false);
new Thread (() =>
Monitor ("Processor", "% Processor Time", "_Total", stopper)
).Start();
new Thread (() =>
Monitor ("LogicalDisk", "% Idle Time", "C:", stopper)
).Start();
Console.WriteLine ("Monitoring - press any key to quit");
Console.ReadKey();
stopper.Set();
ساخت Counter و نوشتن Performance Data
پیش از نوشتن Performance Counter Data باید Performance Category و Counter بسازید. باید Performance Category را همراه با تمام Counterهای متعلق به آن در یک Step بسازید:
string category = "Nutshell Monitoring";
// We'll create two counters in this category:
string eatenPerMin = "Macadamias eaten so far";
string tooHard = "Macadamias deemed too hard";
if (!PerformanceCounterCategory.Exists (category))
{
CounterCreationDataCollection cd = new CounterCreationDataCollection();
cd.Add (new CounterCreationData (eatenPerMin,
"Number of macadamias consumed, including shelling time",
PerformanceCounterType.NumberOfItems32));
cd.Add (new CounterCreationData (tooHard,
"Number of macadamias that will not crack, despite much effort",
PerformanceCounterType.NumberOfItems32));
PerformanceCounterCategory.Create (category, "Test Category",
PerformanceCounterCategoryType.SingleInstance, cd);
}
Counterهای جدید سپس در Windows Performance Monitoring Tool هنگام انتخاب Add Counters ظاهر میشوند. اگر بعداً بخواهید Counter بیشتری در همان Category تعریف کنید، ابتدا باید Category قدیمی را با PerformanceCounterCategory.Delete حذف کنید.
بعد از ساخت Counter، میتوانید Value آن را با Instantiate کردن PerformanceCounter، قرار دادن ReadOnly=false و تنظیم RawValue Update کنید. همچنین Methodهای Increment و IncrementBy برای Update کردن Value موجود وجود دارند:
string category = "Nutshell Monitoring";
string eatenPerMin = "Macadamias eaten so far";
using (PerformanceCounter pc = new PerformanceCounter (category,
eatenPerMin, ""))
{
pc.ReadOnly = false;
pc.RawValue = 1000;
pc.Increment();
pc.IncrementBy (10);
Console.WriteLine (pc.NextValue()); // 1011
}
Class با نام Stopwatch
Class با نام Stopwatch مکانیزم راحتی برای اندازهگیری Execution Time فراهم میکند. Stopwatch از بالاترین Resolution موجود در OS و Hardware استفاده میکند که معمولاً کمتر از یک Microsecond است. در مقابل، DateTime.Now و Environment.TickCount Resolution حدود 15 ms دارند.
برای استفاده از Stopwatch، StartNew را فراخوانی کنید؛ این Method یک Stopwatch میسازد و شروع به Tick کردن میکند. بهعنوان جایگزین میتوانید دستی Instantiate و سپس Start را فراخوانی کنید. Property با نام Elapsed Interval سپریشده را بهصورت TimeSpan برمیگرداند:
Stopwatch s = Stopwatch.StartNew();
System.IO.File.WriteAllText ("test.txt", new string ('*', 30000000));
Console.WriteLine (s.Elapsed); // 00:00:01.4322661
Stopwatch همچنین ElapsedTicks را Expose میکند که تعداد Tickهای سپریشده را بهصورت long برمیگرداند. برای تبدیل Tick به Second، بر StopWatch.Frequency تقسیم کنید. Property با نام ElapsedMilliseconds نیز وجود دارد که اغلب راحتترین گزینه است.
فراخوانی Stop مقدار Elapsed و ElapsedTicks را Freeze میکند. Stopwatch در حالت «Running» هیچ Background Activity ایجاد نمیکند، بنابراین Call کردن Stop اختیاری است.
ابزارهای Diagnostic چندسکویی (Cross-Platform Diagnostic Tools)
در این بخش Toolهای Cross-platform Diagnostic در .NET را بهاختصار معرفی میکنیم:
dotnet-counters- نمای کلی از State یک Application در حال اجرا ارائه میکند.
dotnet-trace- برای Performance Monitoring و Event Monitoring با Detail بیشتر.
dotnet-dump- برای گرفتن Memory Dump بهصورت On-demand یا پس از Crash.
این Toolها به Administrative Elevation نیاز ندارند و برای هر دو Environment با نام Development و Production مناسباند.
dotnet-counters
Tool با نام dotnet-counters مصرف Memory و CPU یک .NET Process را Monitor و Data را در Console یا File مینویسد.
برای Install کردن Tool، Command زیر را از Command Prompt یا Terminalای که dotnet در Path آن است اجرا کنید:
dotnet tool install --global dotnet-counters
سپس میتوانید Process را Monitor کنید:
dotnet-counters monitor System.Runtime --process-id <<ProcessID>>
System.Runtime یعنی میخواهیم همهٔ Counterهای Category با نام System.Runtime را Monitor کنیم. میتوانید Category یا Counter Name مشخص کنید؛ Command با نام dotnet-counters list همهٔ Categoryها و Counterهای موجود را List میکند.
Output بهصورت مداوم Refresh میشود و شبیه زیر است:
Press p to pause, r to resume, q to quit.
Status: Running
[System.Runtime]
# of Assemblies Loaded 63
% Time in GC (since last GC) 0
Allocation Rate (Bytes / sec) 244,864
CPU Usage (%) 6
Exceptions / sec 0
GC Heap Size (MB) 8
Gen 0 GC / sec 0
Gen 0 Size (B) 265,176
Gen 1 GC / sec 0
Gen 1 Size (B) 451,552
Gen 2 GC / sec 0
Gen 2 Size (B) 24
LOH Size (B) 3,200,296
Monitor Lock Contention Count / sec 0
Number of Active Timers 0
ThreadPool Completed Work Items / sec 15
ThreadPool Queue Length 0
ThreadPool Threads Count 9
Working Set (MB) 52
Commandهای موجود:
Commandهای dotnet-counters| Command | کاربرد |
list | List مربوط به Counter Nameها را همراه Description هرکدام نمایش میدهد. |
ps | List مربوط به dotnet Processهای واجد شرایط Monitoring را نمایش میدهد. |
monitor | Valueهای Counterهای انتخابشده را نمایش میدهد و دورهای Refresh میکند. |
collect | Counter Information را در File ذخیره میکند. |
Parameterها و Optionهای پشتیبانیشده:
Optionها و Argumentهای dotnet-counters| Option / Argument | کاربرد |
--version | Version مربوط به dotnet-counters را نمایش میدهد. |
-h, --help | Help برنامه را نمایش میدهد. |
-p, --process-id | ID مربوط به dotnet Process برای Monitor؛ در Commandهای monitor و collect کاربرد دارد. |
--refresh-interval | Refresh Interval موردنظر را بر حسب Second تعیین میکند؛ برای monitor و collect. |
-o, --output | Output File Name را تعیین میکند؛ برای collect. |
--format | Output Format را تعیین میکند؛ Valueهای معتبر csv یا json هستند؛ برای collect. |
dotnet-trace
Traceها Recordهای Timestamped از Eventهای Program شما هستند؛ مانند فراخوانی یک Method یا Query شدن Database. Traceها میتوانند Performance Metric و Custom Event نیز داشته باشند و Context محلی مانند Value مربوط به Local Variable را شامل شوند. بهطور سنتی .NET Framework و Frameworkهایی مانند ASP.NET از ETW استفاده میکردند. در .NET 5، Application Traceها روی Windows در ETW و روی Linux در LTTng نوشته میشوند.
برای Install:
dotnet tool install --global dotnet-trace
برای شروع Recording Eventهای Program:
dotnet-trace collect --process-id <<ProcessId>>
این Command، dotnet-trace را با Profile پیشفرض اجرا میکند که CPU و .NET Runtime Eventها را Collect و در File با نام trace.nettrace مینویسد. میتوانید Profileهای دیگر را با Switch با نام --profile مشخص کنید: gc-verbose Garbage Collection و Sampled Object Allocation را Track میکند و gc-collect Garbage Collection را با Overhead کم Track میکند. Switch با نام -o اجازه میدهد Output Filename متفاوتی بدهید.
Output پیشفرض File با Format مربوط به .netperf است که روی Windows مستقیماً با Tool با نام PerfView قابل Analysis است. همچنین میتوانید به dotnet-trace بگویید File سازگار با Speedscope بسازد؛ Speedscope یک سرویس رایگان Online Analysis است. آدرس متن منبع: https://speedscope.app. برای ساخت File با پسوند .speedscope.json از Option با نام --format speedscope استفاده کنید.
Commandهای پشتیبانیشده:
Commandهای dotnet-trace| Command | کاربرد |
collect | Recording مربوط به Counter/Event Information را در File شروع میکند. |
ps | List مربوط به dotnet Processهای واجد شرایط Monitoring را نشان میدهد. |
list-profiles | Profileهای آمادهٔ Tracing را همراه Description مربوط به Providerها و Filterهای هرکدام List میکند. |
convert <file> | Format با نام nettrace یا .netperf را به Format جایگزین تبدیل میکند؛ در حال حاضر فقط speedscope Target است. |
Custom Trace Eventها
Application شما میتواند با تعریف Custom EventSource Event سفارشی Emit کند:
[EventSource (Name = "MyTestSource")]
public sealed class MyEventSource : EventSource
{
public static MyEventSource Instance = new MyEventSource ();
MyEventSource() : base (EventSourceSettings.EtwSelfDescribingEventFormat)
{
}
public void Log (string message, int someNumber)
{
WriteEvent (1, message, someNumber);
}
}
Method با نام WriteEvent Overload شده تا ترکیبهای مختلفی از Typeهای ساده ـ عمدتاً String و Integer ـ بگیرد. سپس میتوانید چنین فراخوانی کنید:
MyEventSource.Instance.Log ("Something", 123);
هنگام فراخوانی dotnet-trace باید Name هر Custom Event Sourceای را که میخواهید Record شود مشخص کنید:
dotnet-trace collect --process-id <<ProcessId>> --providers MyTestSource
dotnet-dump
Dump که گاهی Core Dump نامیده میشود Snapshot از State مربوط به Virtual Memory یک Process است. میتوانید از Process در حال اجرا بهصورت On-demand Dump بگیرید یا OS را Configure کنید تا هنگام Crash Application Dump بسازد.
در Ubuntu Linux، Command زیر Core Dump هنگام Crash Application را فعال میکند؛ Stepهای لازم بین Flavorهای مختلف Linux میتواند متفاوت باشد:
در Windows از regedit.exe برای ساخت یا ویرایش Key زیر در Local Machine Hive استفاده کنید:
SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps
زیر آن Keyای با همان Name مربوط به Executable خود، مثلاً foo.exe، اضافه کنید و زیر آن Key موارد زیر را بسازید:
DumpFolder با Type با نام REG_EXPAND_SZ و Value مربوط به Path مقصد Dump Fileها.
DumpType با Type با نام REG_DWORD و Value برابر 2 برای درخواست Full Dump.
- بهصورت اختیاری
DumpCount با Type با نام REG_DWORD برای تعیین حداکثر تعداد Dump File پیش از حذف قدیمیترین File.
برای Install Tool:
dotnet tool install --global dotnet-dump
بعد از Install میتوانید بدون پایاندادن Process بهصورت On-demand Dump بگیرید:
dotnet-dump collect --process-id <<YourProcessId>>
Command زیر Interactive Shell را برای Analysis یک Dump File آغاز میکند:
dotnet-dump analyze <<dumpfile>>
اگر Exception باعث Down شدن Application شده باشد، میتوانید از Command با نام printexceptions ـ کوتاهشده pe ـ برای نمایش Detailهای آن Exception استفاده کنید. Shell مربوط به dotnet-dump Commandهای اضافی زیادی دارد که با Command با نام help میتوانید آنها را List کنید.