当前位置:网站首页>ASP. Net core 6 framework unveiling example demonstration [12]: advanced usage of diagnostic trace

ASP. Net core 6 framework unveiling example demonstration [12]: advanced usage of diagnostic trace

2022-06-10 16:35:00 Artech

A good programmer can locate the root cause of the error and find the right solution immediately after the system has a problem , A better programmer can predict future problems based on the current running state , And strangle the problem in the cradle . Diagnostic tracking can help us correct and troubleshoot errors effectively 《 Several basic diagnostic trace programming methods 》 Provides 7 An example demonstrates TraceSource、EventSource and DiagnosticSource The basic usage of , In fact, they also have a more “ senior ” How to use .( The examples provided in this article have been summarized to 《ASP.NET Core 6 The framework reveals - Example Demo 》)

[S708]DefaultTraceListener Log output for file ( Source code
[S709] utilize DelimitedListTraceListener Output log to CSV file ( Source code
[S710] More complete EventListener The type definition ( Source code
[S711] utilize EventSource Event log output call chain ( Source code
[S712]AnonymousObserver<T> Application ( Source code
[S713] Strongly typed diagnostic event subscriptions ( Source code

[S708]DefaultTraceListener Log output for file

In the trace log framework , We use registered TraceListener Object to persist and store trace log messages ( Such as saving formatted log messages in files or databases ) Or visual display ( If output to console ), Or send them to a remote service for further processing . The trace logging system defines several native TraceListener type . The default is as follows DefaultTraceListener type . Created TraceSource Object will automatically register with the following definitions DefaultTraceListener object , The latter will send the log message to the debugger as debugging information .DefaultTraceListener Object can also help us write the log contents to the specified file , The path of the file can be through LogFileName Property to specify .

public class DefaultTraceListener: TraceListener
{
    public string LogFileName { get; set; }

    public override void Write(string message);
    public override void WriteLine(string message);
    ...
}

We use a simple program to demonstrate DefaultTraceListener Log output for file . As the code snippet shows , Creating a TraceSource After object , We will register by default TraceListener eliminate , Then registered according to the specified log file (trace.log) Created DefaultTraceListener object , Then a log is output for each event type .

using System.Diagnostics;

var source = new TraceSource("Foobar", SourceLevels.All);
source.Listeners.Clear();
source.Listeners.Add(new DefaultTraceListener { LogFileName = "trace.log" });
var eventTypes = (TraceEventType[])Enum.GetValues(typeof(TraceEventType));
var eventId = 1;
Array.ForEach(eventTypes, it => source.TraceEvent(it, eventId++, $"This is a {it} message."));

After running the program, we will find that a... Will be generated in the compiled output directory trace.log file , Generated in the program 10 Trace logs will be written to the file one by one ( See the picture 1).DefaultTraceListener When performing log output for files , Just append the formatted log message to (Append) Is written to the specified file .

image
chart 1  For static types Trace Tracking event distribution processing mechanism

[S709] utilize DelimitedListTraceListener Output log to CSV file

DelimitedListTraceListener yes TextWriterTraceListener Subclasses of , It will use the specified separator when formatting the trace log information . Semicolons are used by default (“;”) As a separator . Because of the content of the trace log, the payload will eventually be formatted into a string , The output of a string can be generated by a TextWriter Object to complete . One TextWriterTraceListener Objects take advantage of encapsulated TextWriter Object to complete the output of the trace log content . This is shown in the following code snippet , This TextWriter The object is embodied in TextWriterTraceListener Of Writer Attribute .

public class DelimitedListTraceListener : TextWriterTraceListener
{
    public string Delimiter { get; set; }

    public DelimitedListTraceListener(Stream stream);
    public DelimitedListTraceListener(TextWriter writer);
    public DelimitedListTraceListener(string fileName);
    public DelimitedListTraceListener(Stream stream, string name);
    public DelimitedListTraceListener(TextWriter writer, string name);
    public DelimitedListTraceListener(string fileName, string name);

    public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data);
    public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data);
    public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message);
    public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args);
}

Separator based formatting is implemented in rewritten TraceData Methods and TraceEvent In the method , So call TraceSource Object's Write perhaps WriteLine Method, the output will not be separated by a separator . For the second TraceData Method overloading , If the incoming content load object is an array , Then each element will also be separated by a separator . The separator used by default is comma (“,”), But if Delimiter The main delimiter represented by the attribute is comma , This separator will choose a semicolon (“;”). The code snippet shown below shows that the default delimiter is selected by TraceData Methods and TraceEvent Method output text format .

TraceData 1:

{SourceName};{EventType};{EventId};{Data};{ProcessId};{LogicalOperationStack};{ThreadId};{DateTime};{Timestamp};

TraceData 2:

{SourceName};{EventType};{EventId};{Data1},{Data2},...,{DataN};{ProcessId};{LogicalOperationStack};{ThreadId};{DateTime};{Timestamp};

TraceEvent

{SourceName};{EventType};{EventId};{Message};;{ProcessId};{LogicalOperationStack};{ThreadId};{DateTime};{Timestamp};

Placeholders in the trace log output format shown above “{LogicalOperationStack}” Represents the call stack of the current logical operation . The above code snippet also reveals another detail , That's right. TraceEvent Method , In the... That represents the body of the log message “{Message}” And represent processes ID Of “{ProcessId}” Two separators appear between , This may be a loophole (Bug). If we use commas (“,”) As a separator , Then the final output is one CSV(Comma Separated Value) file . For example, in the example demonstration shown below , We will name the current directory as trace.csv As a log file , Then according to the of this file FileStream Created a DelimitedListTraceListener Object and register it with the TraceSource On the object , Finally, for each event type 10 Logs .

using System.Diagnostics;

var fileName = "trace.csv";
File.AppendAllText(fileName, @$"SourceName,EventType,EventId,Message,N/A,ProcessId,LogicalOperationStack, hreadId, DateTime, Timestamp,{ Environment.NewLine}");

using (var fileStream = new FileStream(fileName, FileMode.Append))
{
    var options = TraceOptions.Callstack | TraceOptions.DateTime |TraceOptions.LogicalOperationStack | TraceOptions.ThreadId | TraceOptions.Timestamp;
    var listener = new DelimitedListTraceListener(fileStream){ TraceOutputOptions = options, Delimiter = "," };
    var source = new TraceSource("Foobar", SourceLevels.All);
    source.Listeners.Add(listener);
    var eventTypes = (TraceEventType[])Enum.GetValues(typeof(TraceEventType));
    for (int index = 0; index < eventTypes.Length; index++)
    {
        var enventType = eventTypes[index];
        var eventId = index + 1;
        Trace.CorrelationManager.StartLogicalOperation($"Op{eventId}");
        source.TraceEvent(enventType, eventId, $"This is a {enventType} message.");
    }
    source.Flush();
}

In order to demonstrate the call stack of the logical operation mentioned above , We make use of Trace Type gets a CorrelationManager object , And call it StartLogicalOperation Method starts a to “Op{EventId}” Logical operation of format naming . because DelimitedListTraceListener The buffer mechanism is used inside the object , So we artificially called TraceSource Object's Flush Method to force the trace log in the output buffer . Output after the program runs 10 All trace logs will be recorded in trace.csv In file , If directly used Excel Open this file , You will see the picture 2 What is shown .

image
chart 2  adopt DelimitedListTraceListener Output log file

[S710] More complete EventListener The type definition

The last one already demonstrated EventSource and EventListener Simple usage , Let's do a more complete demonstration . Let's predefine several in the form of constants EventTask and EventTags object , The former indicates the application level where the operation is executed , The latter represents three typical relational database types .

public class Tasks
{
    public const EventTask UI 		= (EventTask)1;
    public const EventTask Business 	= (EventTask)2;
    public const EventTask DA 		= (EventTask)3;
}

public class Tags
{
    public const EventTags MSSQL 	= (EventTags)1;
    public const EventTags Oracle 	= (EventTags)2;
    public const EventTags Db2 	= (EventTags)3;
}

We still follow the implementation of SQL Command application scenarios , To this end, we have a review of the previously defined DatabaseSource Make some simple changes to the type . As the code snippet shows , We are in the log method OnCommandExecute Marked on EventAttribute Feature sets all its properties accordingly , among Task and Tags Property uses the constants defined above . It is worth noting that , We are Message Property sets a template containing placeholders .OnCommandExecute Method is calling WriteEvent Method before sending log events , First use of IsEnabled Method validation EventSource Object has been subscribed to for the specified level and output channel .

[EventSource(Name = "Artech-Data-SqlClient")]
public sealed class DatabaseSource : EventSource
{
    public static DatabaseSource Instance = new DatabaseSource();
    private DatabaseSource() {}

    [Event(1, Level = EventLevel.Informational, Keywords = EventKeywords.None, Opcode = EventOpcode.Info, Task = Tasks.DA, Tags = Tags.MSSQL, Version = 1, Message = "Execute SQL command. Type: {0}, Command Text: {1}")]
    public void OnCommandExecute(CommandType commandType, string commandText)
    {
        if (IsEnabled(EventLevel.Informational, EventKeywords.All, EventChannel.Debug))
        {
            WriteEvent(1, (int)commandType, commandText);
        }
    }
}

We use the following code for the log event subscriber DatabaseSourceListener Type has been redefined . To verify whether the received log events are consistent with OnCommandExecute The subscriptions in the method are consistent , We are rewriting OnEventWritten Method outputs EventWrittenEventArgs All properties of the object .

public class DatabaseSourceListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if (eventSource.Name == "Artech-Data-SqlClient")
        {
            EnableEvents(eventSource, EventLevel.LogAlways);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        Console.WriteLine($"EventId: {eventData.EventId}");
        Console.WriteLine($"EventName: {eventData.EventName}");
        Console.WriteLine($"Channel: {eventData.Channel}");
        Console.WriteLine($"Keywords: {eventData.Keywords}");
        Console.WriteLine($"Level: {eventData.Level}");
        Console.WriteLine($"Message: {eventData.Message}");
        Console.WriteLine($"Opcode: {eventData.Opcode}");
        Console.WriteLine($"Tags: {eventData.Tags}");
        Console.WriteLine($"Task: {eventData.Task}");
        Console.WriteLine($"Version: {eventData.Version}");
        Console.WriteLine($"Payload");
        var index = 0;
        if (eventData.PayloadNames != null)
        {
            foreach (var payloadName in eventData.PayloadNames)
            {
                Console.WriteLine($"\t{payloadName}:{eventData.Payload?[index++]}");
            }
        }
    }
}

In the demonstration program shown above , We created one DatabaseSourceListener object . After extracting the single example form DatabaseSource After the object , We called its OnCommandExecute Sent a message about SQL Log events of command execution .

using App;
using System.Data;

_ = new DatabaseSourceListener();
DatabaseSource.Instance.OnCommandExecute(CommandType.Text, "SELECT * FROM T_USER");

After the program runs , The details of log events are shown in the figure 3 Output to the console in the form shown in . We can find from the output results EventWrittenEventArgs Of Message Property still returns the raw information without any formatting , I think this is something worth improving .

image
chart 3  For static types Trace Tracking event distribution processing mechanism

[S711] utilize EventSource Event log output call chain

Next, let's demonstrate how to use custom EventSource and EventListener To complete the tracking of activities . Suppose a complete call chain consists of Foo、Bar、Baz and Qux These four activities make up , For this purpose, we define the following FoobarSource, Four groups of corresponding event methods are defined for the four activities , among “{Op}Start” and “{Op}Stop” Methods correspond to the start and end events of the activity respectively , The load information of the former includes the time stamp of the start of the activity , The load information of the latter includes the operation time .

[EventSource(Name = "Foobar")]
public sealed class FoobarSource : EventSource
{
    public static readonly FoobarSource Instance = new();

    [Event(1)]
    public void FooStart(long timestamp) => WriteEvent(1, timestamp);
    [Event(2)]
    public void FooStop(double elapsed) => WriteEvent(2, elapsed);

    [Event(3)]
    public void BarStart(long timestamp) => WriteEvent(3, timestamp);
    [Event(4)]
    public void BarStop(double elapsed) => WriteEvent(4, elapsed);

    [Event(5)]
    public void BazStart(long timestamp) => WriteEvent(5, timestamp);
    [Event(6)]
    public void BazStop(double elapsed) => WriteEvent(6, elapsed);

    [Event(7)]
    public void QuxStart(long timestamp) => WriteEvent(7, timestamp);
    [Event(8)]
    public void QuxStop(double elapsed) => WriteEvent(8, elapsed);
}

As shown below FoobarListener Will subscribe to the above events , And save the received call chain information to a .csv In file (log.csv). In rewriting OnEventSourceCreated In the method , We're based on EventSource The name of the subscription is by FoobarSource Out of eight events , You also need to subscribe TplEtwProvider Event sent to save activity flow information . In line with the principle of minimizing the scope of subscription , We're calling EnableEvents Method uses log level and keyword to filter subscription events . In rewriting OnEventWritten In the method , Event information we will capture ( name 、 Activity start timestamp and elapsed time 、ActivityId and RelatedActivityId) Write the specified after formatting .csv In file .

public sealed class FoobarListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if (eventSource.Name == "System.Threading.Tasks.TplEventSource")
        {
            EnableEvents(eventSource, EventLevel.Informational, (EventKeywords)0x80);
        }

        if (eventSource.Name == "Foobar")
        {
            EnableEvents(eventSource, EventLevel.LogAlways);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (eventData.EventSource.Name == "Foobar")
        {
            var timestamp = eventData.PayloadNames?[0] == "timestamp"? eventData.Payload?[0]: "";
            var elapsed = eventData.PayloadNames?[0] == "elapsed"? eventData.Payload?[0]: "";
            var relatedActivityId = eventData.RelatedActivityId == default? "": eventData.RelatedActivityId.ToString();
            var line = @$"{eventData.EventName},{timestamp},{elapsed},{ eventData.ActivityId},{ relatedActivityId}";
            File.AppendAllLines("log.csv", new string[] { line });
        }
    }
}

The code snippet shown below can be simulated by Foo、Bar、Baz and Qux These four activities form a call chain . The control of these activities is implemented in InvokeAsync In the method , Parameters of this method start and stop The provided delegate object is used to send the start event and end event of the activity respectively , As for parameters body Back to Task Object represents the operation of the activity itself . In order to simulate the activity, it takes time , We waited artificially for a random time .

using App;
using System.Diagnostics;

var random = new Random();
File.AppendAllLines("log.csv",  new string[] { @"EventName,StartTime,Elapsed,ActivityId,RelatedActivityId" });
var listener = new FoobarListener();
await FooAsync();

Task FooAsync() => InvokeAsync(FoobarSource.Instance.FooStart, FoobarSource.Instance.FooStop, async () =>
{
    await BarAsync();
    await QuxAsync();
});

Task BarAsync() => InvokeAsync(FoobarSource.Instance.BarStart, FoobarSource.Instance.BarStop, BazAsync);
Task BazAsync() => InvokeAsync(FoobarSource.Instance.BazStart,FoobarSource.Instance.BazStop, () => Task.CompletedTask);
Task QuxAsync() => InvokeAsync(FoobarSource.Instance.QuxStart, FoobarSource.Instance.QuxStop, () => Task.CompletedTask);

async Task InvokeAsync(Action<long> start, Action<double> stop, Func<Task> body)
{
    start(Stopwatch.GetTimestamp());
    var sw = Stopwatch.StartNew();
    await Task.Delay(random.Next(10, 100));
    await body();
    stop(sw.ElapsedMilliseconds);
}

The four activities are implemented in four corresponding methods (FooAsync、BarAsync、BazAsync and QuxAsync), In order to simulate based on Task Asynchronous programming , We let the four methods return one Task object . From the definition of these four methods, we can see , The call chain they embody is shown in the figure below 4 Shown .

image
chart 4  Call chain constructed by related activities

We called in the demo program. FooAsync Method , And created one before that FoobarListener Object to subscribe to log events , Then, the formatted event information is written into the specified .csv In file . After the program runs , We will be in .csv In the file 8 Corresponding log event records . Pictured 5 Shown ,Start Events and Stop The event records the start timestamp and time consumption of the activity respectively , and ActivityId and RelatedActivityId It can clearly reflect the flow of the whole call chain .

image
chart 5  Recorded in the .csv Call chain information in the file

[S712]AnonymousObserver<T> Application

Whether it's on behalf of the publisher DiagnosticListener object , Or is it defined on this type to represent all DiagnosticListener Object's static properties AllListeners, They are all embodied in a IObservable<T> object . To complete the subscription for them , We need to create a corresponding IObserver<T> object ,AnonymousObserver<T> That's right IObserver<T> A simple implementation of the interface .AnonymousObserver<T> It's defined in NuGet package “System.Reactive.Core” in , It uses the... Provided by the opening demonstration example Observer<T> The same implementation , That is, through the specified delegate object (Action<T> and Action<Exception>) To achieve IObservable<T> Three methods of interface . The code snippet shown below shows Web The server is for one time HTTP Log output of request processing , After receiving the request, the server outputs the request context information and current timestamp in the form of log , After the response is successfully sent, the response message is output and the time-consuming of the whole request processing .

using System.Diagnostics;
using System.Net;

DiagnosticListener.AllListeners.Subscribe(listener =>
{
    if (listener.Name == "Web")
    {
        listener.Subscribe(eventData =>
        {
            if (eventData.Key == "ReceiveRequest" && eventData.Value != null)
            {
                dynamic payload = eventData.Value;
                var request = (HttpRequestMessage)(payload.Request);
                var timestamp = (long)payload.Timestamp;
                Console.WriteLine(@$"Receive request. Url: {request.RequestUri};Timstamp:{ timestamp}");
            }
            if (eventData.Key == "SendReply" && eventData.Value != null)
            {
                dynamic payload = eventData.Value;
                var response = (HttpResponseMessage)(payload.Response);
                var elaped = (TimeSpan)payload.Elaped;
                Console.WriteLine($"Send reply. Status code: {response.StatusCode}; Elaped: {elaped}");
            }
        });
    }
});

var source = new DiagnosticListener("Web");
var stopwatch = Stopwatch.StartNew();
if (source.IsEnabled("ReceiveRequest"))
{
    var request = new HttpRequestMessage(HttpMethod.Get, "https://www.artech.top");
    source.Write("ReceiveRequest", new
    {
        Request 	= request,
        Timestamp 	= Stopwatch.GetTimestamp()
    });
}
await Task.Delay(100);
if (source.IsEnabled("SendReply"))
{
    var response = new HttpResponseMessage(HttpStatusCode.OK);
    source.Write("SendReply", new
    {
        Response 	= response,
        Elaped 	= stopwatch.Elapsed
    });
}

For the published part of the diagnostic log , We need to create a file named Web Of DiagnosticListener object , And use the open Stopwatch To calculate the request processing time . We use manually created HttpRequestMessage Object to simulate the received request , Calling Write Method sends a message named “ReceiveRequest” Log events for , The HttpRequestMessage Object, together with the current timestamp, is used as the content load object of the log in the form of an anonymous object . Waiting artificially 100 Milliseconds to simulate the time taken for request processing , We call DiagnosticListener Object's Write The method emits a call called “SendReply” Log events for , Indicates that the processing of the current request has ended , Anonymous objects as content loads contain a manually created HttpResponseMessage Objects and simulations are time consuming .

The subscription to log events in the first half of the program is through the call Subscribe Implemented by extension method , At the designated Action<DiagnosticListener> In the delegate object , We filter out the subscription targets by name DiagnosticListener object , Then subscribe to its ReceiveRequest and SendReply event . For subscribed ReceiveRequest event , We use dynamic types (dynamic) The way to get the... On behalf of the current request HttpRequestMessage Object and timestamp , And will request URL And time stamp print out .SendReply Events extract the information representing the response message in the same way HttpResponseMessage Object and time consuming , And print out the response status code and time-consuming . After the program runs , The output results seen on the console are shown in the figure 6 Shown .

image
chart 6 Tracking of requests

[S713] Strongly typed diagnostic event subscriptions

In order to reduce the coupling between log event publishers and subscribers , In many cases, the content load of log events will be represented by anonymous objects . Because of that , In the examples shown at the beginning of this chapter and earlier , We can only use dynamic Keyword the required members can be extracted only after the load object is converted to dynamic type . Because anonymous types are not public types , Therefore, the above method is only used when the publisher and subscriber are in the same assembly . When you cannot extract data members using dynamic types , We have to use reflection or expression tree to solve this problem , Although feasible, it will become cumbersome .

Strongly typed log event subscriptions are used in a very simple way “ grace ” The way to solve this problem . Simply speaking , The so-called strong type log event subscription is to define the log subscription processing logic in the method corresponding to a certain type , This method can define the corresponding parameters according to the member structure of the log content load object . To implement strongly typed log event subscription, you need to implement two “ binding ”, That is, the binding between log events and methods , And the binding between the data member of the load and the subscription method parameters .

Parameter binding is realized by the mapping between the attribute name and parameter name of the load member , Therefore, the subscription method only needs to determine the type and name of the corresponding parameters according to the attribute members of the load object . The mapping between log events and methods can take advantage of the following DiagnosticNameAttribute Features to achieve , We only need to mark this method on the subscription method and specify the name of the mapped log event .

public class DiagnosticNameAttribute : Attribute
{
    public string Name { get; }
    public DiagnosticNameAttribute(string name);
}

The subscription object of strongly typed diagnostic log events can be through DiagnosticListener The following extension methods are used to complete , They are defined in NuGet package “Microsoft.Extensions.DiagnosticAdapter” in . these SubscribeWithAdapter Method overload adapts between the specified object and the standard subscription object , Converts the specified object to a IObserver<KeyValuePair<string, object>> object .

public static class DiagnosticListenerExtensions
{
    public static IDisposable SubscribeWithAdapter(this DiagnosticListener diagnostic, object target);
    public static IDisposable SubscribeWithAdapter(this DiagnosticListener diagnostic, object target, Func<string, bool> isEnabled);
    public static IDisposable SubscribeWithAdapter(this DiagnosticListener diagnostic, object target, Func<string, object, object, bool> isEnabled);
}

Next, we will transform the previous example into a strongly typed log event subscription . First define the following DiagnosticCollector As log event subscription type . It can be seen that this is just a common without implementing any interface or inheriting any base class POCO type . We defined OnReceiveRequest and OnSendReply Two log event methods , Applied to them DiagnosticNameAttribute Property sets the corresponding event name . In order to automatically obtain the log content , You can define parameters for these two methods according to the data structure of the load object .

public sealed class DiagnosticCollector
{
    [DiagnosticName("ReceiveRequest")]
    public void OnReceiveRequest(HttpRequestMessage request, long timestamp)
        => Console.WriteLine($"Receive request. Url: {request.RequestUri}; Timstamp:{timestamp}");

    [DiagnosticName("SendReply")]
    public void OnSendReply(HttpResponseMessage response, TimeSpan elaped)
        => Console.WriteLine($"Send reply. Status code: {response.StatusCode}; Elaped: {elaped}");
}

Next, we just need to change the previous log event subscription method . This is shown in the following code snippet , Find the target of the subscription by name DiagnosticListener After object , Let's create DiagnosticCollector object , And call it as a parameter SubscribeWithAdapter Extension method to register . After the modified program runs , The diagram will also be output on the console 6 The results shown .

using App;
using System.Diagnostics;
using System.Net;

DiagnosticListener.AllListeners.Subscribe(listener =>
{
    if (listener.Name == "Web")
    {
        listener.SubscribeWithAdapter(new DiagnosticCollector());
    }
});

...
原网站

版权声明
本文为[Artech]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/03/202203020959249029.html