DiagnosticSource 및 DiagnosticListener
이 문서의 적용 대상: ✔️ .NET Core 3.1 이상 버전 ✔️ .NET Framework 4.5 이상 버전
System.Diagnostics.DiagnosticSource는 계측된 프로세스 내에서 사용하기 위한 다양한 데이터 페이로드의 프로덕션 시간 로깅에 대한 코드를 계측할 수 있게 해 주는 모듈입니다. 런타임에 소비자는 데이터 원본을 동적으로 검색하고 관심 있는 데이터 원본을 구독할 수 있습니다. System.Diagnostics.DiagnosticSource는 In-Process 도구가 풍부한 데이터에 액세스할 수 있도록 설계되었습니다. System.Diagnostics.DiagnosticSource를 사용할 때 소비자는 동일한 프로세스 내에 있는 것으로 간주되므로 직렬화할 수 없는 형식(예: HttpResponseMessage
또는 HttpContext
)을 전달하여 고객에게 작업할 많은 데이터를 제공할 수 있습니다.
DiagnosticSource 시작
이 연습에서는 System.Diagnostics.DiagnosticSource를 사용하여 DiagnosticSource 이벤트 및 계측 코드를 만드는 방법을 보여 줍니다. 그런 다음 흥미로운 DiagnosticListener를 찾고, 이벤트를 구독하고, 이벤트 데이터 페이로드를 디코딩하여 이벤트를 사용하는 방법을 설명합니다. 그리고 특정 이벤트만 시스템을 통과할 수 있도록 필터링을 설명하는 것으로 마무리합니다.
DiagnosticSource 구현
다음 코드로 작업합니다. 이 코드는 URL에 HTTP 요청을 보내고 회신을 받는 SendWebRequest
메서드가 있는 HttpClient 클래스입니다.
using System.Diagnostics;
MyListener TheListener = new MyListener();
TheListener.Listening();
HTTPClient Client = new HTTPClient();
Client.SendWebRequest("https://zcusa.951200.xyz/dotnet/core/diagnostics/");
class HTTPClient
{
private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
public byte[] SendWebRequest(string url)
{
if (httpLogger.IsEnabled("RequestStart"))
{
httpLogger.Write("RequestStart", new { Url = url });
}
//Pretend this sends an HTTP request to the url and gets back a reply.
byte[] reply = new byte[] { };
return reply;
}
}
class Observer<T> : IObserver<T>
{
public Observer(Action<T> onNext, Action onCompleted)
{
_onNext = onNext ?? new Action<T>(_ => { });
_onCompleted = onCompleted ?? new Action(() => { });
}
public void OnCompleted() { _onCompleted(); }
public void OnError(Exception error) { }
public void OnNext(T value) { _onNext(value); }
private Action<T> _onNext;
private Action _onCompleted;
}
class MyListener
{
IDisposable networkSubscription;
IDisposable listenerSubscription;
private readonly object allListeners = new();
public void Listening()
{
Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
{
Console.WriteLine($"Data received: {data.Key}: {data.Value}");
};
Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
{
Console.WriteLine($"New Listener discovered: {listener.Name}");
//Subscribe to the specific DiagnosticListener of interest.
if (listener.Name == "System.Net.Http")
{
//Use lock to ensure the callback code is thread safe.
lock (allListeners)
{
if (networkSubscription != null)
{
networkSubscription.Dispose();
}
IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
networkSubscription = listener.Subscribe(iobserver);
}
}
};
//Subscribe to discover all DiagnosticListeners
IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
//When a listener is created, invoke the onNext function which calls the delegate.
listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
}
// Typically you leave the listenerSubscription subscription active forever.
// However when you no longer want your callback to be called, you can
// call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
}
제공된 구현을 실행하면 콘솔에 출력됩니다.
New Listener discovered: System.Net.Http
Data received: RequestStart: { Url = https://zcusa.951200.xyz/dotnet/core/diagnostics/ }
이벤트 로그
DiagnosticSource
형식은 이벤트를 로그하는 데 필요한 메서드를 정의하는 추상 기본 클래스입니다. 구현을 보유한 클래스는 DiagnosticListener
입니다.
DiagnosticSource
로 코드를 계측하는 첫 번째 단계에서는 DiagnosticListener
를 만듭니다 . 예시:
private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
httpLogger
는 DiagnosticSource
로 형식이 지정됩니다.
이 코드는 이벤트만 작성하며, 그에 따라 DiagnosticListener
가 구현하는 DiagnosticSource
메서드와만 관련이 있기 때문입니다. DiagnosticListeners
는 만들 때 이름이 지정되며, 이 이름은 관련 이벤트(일반적으로 구성 요소)의 논리적 그룹화 이름이어야 합니다.
나중에 이 이름은 수신기를 찾고 해당 이벤트를 구독하는 데 사용됩니다.
따라서 이벤트 이름은 구성 요소 내에서만 고유해야 합니다.
DiagnosticSource
로깅 인터페이스는 다음 두 가지 방법으로 구성됩니다.
bool IsEnabled(string name)
void Write(string name, object value);
이는 계측 사이트별로 달라집니다. 계측 사이트에서 IsEnabled
로 전달되는 형식을 확인해야 합니다. 이렇게 하면 페이로드를 캐스팅할 항목을 파악할 수 있는 정보가 제공됩니다.
일반적인 호출 사이트는 다음과 같습니다.
if (httpLogger.IsEnabled("RequestStart"))
{
httpLogger.Write("RequestStart", new { Url = url });
}
모든 이벤트에는 string
이름(예: RequestStart
)이 있고 정확히 하나는 object
페이로드입니다.
둘 이상의 항목을 보내야 하는 경우 모든 정보를 나타내는 속성으로 object
를 만들어 이 작업을 수행할 수 있습니다. C#의 무명 형식 기능은 일반적으로 '즉석'으로 전달할 형식을 만드는 데 사용되며 이 체계를 매우 편리하게 만듭니다. 계측 사이트에서 동일한 이벤트 이름에 대한 IsEnabled()
검사를 수행하여 Write()
호출을 보호해야 합니다. 이 검사가 없으면 계측이 비활성 상태인 경우에도 C# 언어 규칙은 실제로 데이터를 수신 대기하지 않더라도 페이로드 object
를 만들고 Write()
를 호출하는 모든 작업을 수행해야 합니다. Write()
호출을 보호하면 원본이 사용하도록 설정되지 않은 경우에 효율성을 높일 수 있습니다.
보유한 모든 요소를 결합합니다.
class HTTPClient
{
private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
public byte[] SendWebRequest(string url)
{
if (httpLogger.IsEnabled("RequestStart"))
{
httpLogger.Write("RequestStart", new { Url = url });
}
//Pretend this sends an HTTP request to the url and gets back a reply.
byte[] reply = new byte[] { };
return reply;
}
}
DiagnosticListeners 검색
이벤트를 수신하는 첫 번째 단계는 관심 있는 DiagnosticListeners
이벤트를 검색하는 것입니다. DiagnosticListener
는 런타임에 시스템에서 활성 상태인 DiagnosticListeners
의 검색 방법을 지원합니다. 이 작업을 수행하는 API는 AllListeners 속성입니다.
IEnumerable
인터페이스의 '콜백' 버전인 IObservable
인터페이스에서 상속되는 Observer<T>
클래스를 구현합니다. 반응형 확장 사이트에서 자세히 알아볼 수 있습니다.
IObserver
에는 세 개의 콜백, 즉 OnNext
, OnComplete
및 OnError
가 있습니다. IObservable
에는 관찰자 중 하나를 전달하는 Subscribe
라는 단일 메서드가 있습니다. 연결되면 관찰자는 상황이 발생할 때 콜백(주로 OnNext
콜백)을 가져옵니다.
AllListeners
정적 속성의 일반적인 사용은 다음과 같습니다.
class Observer<T> : IObserver<T>
{
public Observer(Action<T> onNext, Action onCompleted)
{
_onNext = onNext ?? new Action<T>(_ => { });
_onCompleted = onCompleted ?? new Action(() => { });
}
public void OnCompleted() { _onCompleted(); }
public void OnError(Exception error) { }
public void OnNext(T value) { _onNext(value); }
private Action<T> _onNext;
private Action _onCompleted;
}
class MyListener
{
IDisposable networkSubscription;
IDisposable listenerSubscription;
private readonly object allListeners = new();
public void Listening()
{
Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
{
Console.WriteLine($"Data received: {data.Key}: {data.Value}");
};
Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
{
Console.WriteLine($"New Listener discovered: {listener.Name}");
//Subscribe to the specific DiagnosticListener of interest.
if (listener.Name == "System.Net.Http")
{
//Use lock to ensure the callback code is thread safe.
lock (allListeners)
{
if (networkSubscription != null)
{
networkSubscription.Dispose();
}
IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
networkSubscription = listener.Subscribe(iobserver);
}
}
};
//Subscribe to discover all DiagnosticListeners
IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
//When a listener is created, invoke the onNext function which calls the delegate.
listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
}
// Typically you leave the listenerSubscription subscription active forever.
// However when you no longer want your callback to be called, you can
// call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
}
이 코드는 콜백 대리자를 만들고 AllListeners.Subscribe
메서드를 사용하여 시스템의 모든 활성 DiagnosticListener
에 대해 대리자를 호출하도록 요청합니다. 수신기를 구독할지 여부는 해당 이름을 검사하여 결정됩니다. 위의 코드는 이전에 만든 'System.Net.Http' 수신기를 찾고 있습니다.
Subscribe()
에 대한 모든 호출과 마찬가지로 이 호출은 구독 자체를 나타내는 값을 IDisposable
을 반환합니다.
이 구독 개체에 대해 아무 호출도 Dispose()
를 호출하지 않으면 콜백이 계속 발생합니다.
코드 예제에서는 Dispose()
를 호출하지 않으므로 콜백을 영원히 수신합니다.
AllListeners
를 구독하면 ALL ACTIVE DiagnosticListeners
에 대한 콜백이 표시됩니다.
따라서 구독 시 모든 기존 DiagnosticListeners
에 대한 콜백이 급증하고 새 콜백이 생성되면 콜백도 수신됩니다. 구독할 수 있는 모든 항목의 전체 목록을 받게 됩니다.
DiagnosticListener 구독
DiagnosticListener
는 IObservable<KeyValuePair<string, object>>
인터페이스를 구현하므로 Subscribe()
도 호출할 수 있습니다. 다음 코드는 이전 예제를 작성하는 방법을 보여 줍니다.
IDisposable networkSubscription;
IDisposable listenerSubscription;
private readonly object allListeners = new();
public void Listening()
{
Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
{
Console.WriteLine($"Data received: {data.Key}: {data.Value}");
};
Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
{
Console.WriteLine($"New Listener discovered: {listener.Name}");
//Subscribe to the specific DiagnosticListener of interest.
if (listener.Name == "System.Net.Http")
{
//Use lock to ensure the callback code is thread safe.
lock (allListeners)
{
if (networkSubscription != null)
{
networkSubscription.Dispose();
}
IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
networkSubscription = listener.Subscribe(iobserver);
}
}
};
//Subscribe to discover all DiagnosticListeners
IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
//When a listener is created, invoke the onNext function which calls the delegate.
listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
}
이 예제에서는 'System.Net.Http' DiagnosticListener
를 찾은 후 수신기, 이벤트 및 payload.ToString()
의 이름을 출력하는 작업이 만들어집니다.
참고 항목
DiagnosticListener
는 IObservable<KeyValuePair<string, object>>
를 구현합니다. 즉, 각 콜백에서 KeyValuePair
를 가져옵니다. 이 쌍의 키는 이벤트의 이름이며 값은 페이로드 object
입니다. 예제에서는 이 정보를 콘솔에 로그하기만 하면 됩니다.
DiagnosticListener
에 대한 구독을 추적하는 것이 중요합니다. 이전 코드에서는 networkSubscription
변수가 이를 기억합니다. 다른 creation
을 구성하는 경우 이전 수신기를 구독 취소하고 새 수신기를 구독해야 합니다.
DiagnosticSource
/DiagnosticListener
코드는 스레드로부터 안전하지만 콜백 코드도 스레드로부터 안전해야 합니다. 콜백 코드가 스레드로부터 안전하도록 하기 위해 잠금이 사용됩니다. 이름이 같은 두 DiagnosticListeners
를 동시에 만들 수 있습니다. 경합 상태를 방지하기 위해 잠금을 보호하여 공유 변수의 업데이트가 수행됩니다.
이전 코드가 실행되면 다음에 'System.Net.Http' DiagnosticListener
에서 Write()
작업이 완료되면 정보가 콘솔에 로그됩니다.
구독은 서로 독립적입니다. 따라서 다른 코드는 코드 예제와 정확히 동일한 작업을 수행하고 로깅 정보의 두 개의 '파이프'를 생성할 수 있습니다.
페이로드 디코딩
KeyvaluePair
콜백에 전달되는 값에는 이벤트 이름과 페이로드가 있지만 페이로드는 object
로만 형식이 지정됩니다. 좀 더 구체적인 데이터를 가져오는 방법에는 두 가지가 있습니다.
페이로드가 잘 알려진 형식(예: string
또는 HttpMessageRequest
)인 경우 object
를 필요한 형식으로 캐스팅한 다음(잘못된 경우 예외를 발생시키지 않도록 as
연산자 사용) 필드에 액세스할 수 있습니다. 이는 매우 효율적입니다.
리플렉션 API를 사용합니다. 예를 들어 다음 메서드가 있다고 가정하겠습니다.
/// Define a shortcut method that fetches a field of a particular name.
static class PropertyExtensions
{
static object GetProperty(this object _this, string propertyName)
{
return _this.GetType().GetTypeInfo().GetDeclaredProperty(propertyName)?.GetValue(_this);
}
}
페이로드를 더 완벽하게 디코딩하려면 listener.Subscribe()
호출을 다음 코드로 바꿀 수 있습니다.
networkSubscription = listener.Subscribe(delegate(KeyValuePair<string, object> evnt) {
var eventName = evnt.Key;
var payload = evnt.Value;
if (eventName == "RequestStart")
{
var url = payload.GetProperty("Url") as string;
var request = payload.GetProperty("Request");
Console.WriteLine("Got RequestStart with URL {0} and Request {1}", url, request);
}
});
리플렉션을 사용하면 비교적 비용이 많이 듭니다. 그러나 무명 형식을 사용하여 페이로드를 생성한 경우에는 리플렉션을 사용하는 것이 유일한 옵션입니다. PropertyInfo.GetMethod.CreateDelegate() 또는 System.Reflection.Emit 네임스페이스를 사용하여 빠르고 특수한 속성 페처를 만들어 이 오버헤드를 줄일 수 있지만 이에 대한 설명은 이 문서의 범위를 벗어납니다.
(빠른 대리자 기반 속성 페처의 예는 DiagnosticSourceEventSource
에서 사용되는 PropertySpec 클래스를 참조하세요.)
필터링
이전 예제의 코드는 IObservable.Subscribe()
메서드를 사용하여 콜백을 연결합니다. 이렇게 하면 모든 이벤트가 콜백에 제공됩니다. 그러나 DiagnosticListener
에는 컨트롤러가 제공되는 이벤트를 제어할 수 있게 해 주는 Subscribe()
오버로드가 있습니다.
이전 예제의 listener.Subscribe()
호출은 다음 코드로 바꿔서 보여 줄 수 있습니다.
// Create the callback delegate.
Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) =>
Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString());
// Turn it into an observer (using the Observer<T> Class above).
Observer<KeyValuePair<string, object>> observer = new Observer<KeyValuePair<string, object>>(callback);
// Create a predicate (asks only for one kind of event).
Predicate<string> predicate = (string eventName) => eventName == "RequestStart";
// Subscribe with a filter predicate.
IDisposable subscription = listener.Subscribe(observer, predicate);
// subscription.Dispose() to stop the callbacks.
이렇게 하면 'RequestStart' 이벤트만 효율적으로 구독할 수 있습니다. 다른 모든 이벤트는 DiagnosticSource.IsEnabled()
메서드가 false
를 반환하도록 하므로 효율적으로 필터링됩니다.
참고 항목
필터링은 성능 최적화로만 설계되었습니다. 수신기가 필터를 충족하지 않는 경우에도 이벤트를 수신할 수 있습니다. 이는 일부 다른 수신기가 이벤트를 구독했거나 이벤트 원본이 이벤트를 보내기 전에 IsEnabled()를 검사하지 않았기 때문에 발생할 수 있습니다. 지정된 이벤트가 필터를 충족하는지 확인하려면 콜백 내에서 검사합니다. 예시:
Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) =>
{
if(predicate(evnt.Key)) // only print out events that satisfy our filter
{
Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString());
}
};
컨텍스트 기반 필터링
일부 시나리오에서는 확장된 컨텍스트를 기반으로 하는 고급 필터링이 필요합니다. 생산자는 다음 코드와 같이 DiagnosticSource.IsEnabled 오버로드를 호출하고 추가 이벤트 속성을 제공할 수 있습니다.
//aRequest and anActivity are the current request and activity about to be logged.
if (httpLogger.IsEnabled("RequestStart", aRequest, anActivity))
httpLogger.Write("RequestStart", new { Url="http://clr", Request=aRequest });
다음 코드 예제에서는 소비자가 이러한 속성을 사용하여 이벤트를 좀 더 정확하게 필터링할 수 있음을 보여 줍니다.
// Create a predicate (asks only for Requests for certain URIs)
Func<string, object, object, bool> predicate = (string eventName, object context, object activity) =>
{
if (eventName == "RequestStart")
{
if (context is HttpRequestMessage request)
{
return IsUriEnabled(request.RequestUri);
}
}
return false;
}
// Subscribe with a filter predicate
IDisposable subscription = listener.Subscribe(observer, predicate);
생산자는 소비자가 제공한 필터를 인식하지 않습니다. DiagnosticListener
는 제공된 필터를 호출하여 필요한 경우 추가 인수를 생략하므로 필터는 null
컨텍스트를 수신해야 합니다.
생산자가 이벤트 이름과 컨텍스트를 사용하여 IsEnabled()
를 호출하는 경우 해당 호출은 이벤트 이름만 사용하는 오버로드로 묶입니다. 소비자는 설정한 필터가 컨텍스트가 없는 이벤트를 통과하도록 허용해야 합니다.
.NET