EventSource 이벤트를 만드는 계측 코드
이 문서는 .NET Core 3.1 이상 버전 .NET Framework 4.5 이상 버전에 ✔️ 적용됩니다 ✔️.
시작 가이드 최소한의 EventSource를 만들고 추적 파일에서 이벤트를 수집하는 방법을 보여 줍니다. 이 자습서에서는 System.Diagnostics.Tracing.EventSource사용하여 이벤트를 만드는 방법에 대해 자세히 설명합니다.
최소 이벤트 소스
[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
}
파생 EventSource의 기본 구조는 항상 동일합니다. 특히:
- 클래스는 System.Diagnostics.Tracing.EventSource 상속합니다.
- 생성하려는 각 이벤트 유형에 대해 메서드를 정의해야 합니다. 이 메서드는 생성되는 이벤트의 이름을 사용하여 이름을 지정해야 합니다. 이벤트에 추가 데이터가 있는 경우 인수를 사용하여 전달해야 합니다. 이러한 이벤트 인수는 특정 형식 만 허용되도록 serialize해야 합니다.
- 각 메서드에는 Id(이벤트를 나타내는 숫자 값) 및 이벤트 메서드의 인수를 전달하는 WriteEvent를 호출하는 본문이 있습니다. ID는 EventSource 내에서 고유해야 합니다. ID는 System.Diagnostics.Tracing.EventAttribute 사용하여 명시적으로 할당됩니다.
- EventSources는 싱글톤 인스턴스로 사용됩니다. 따라서 이 싱글톤을 나타내는
Log
이라는 규칙에 따라 정적 변수를 정의하는 것이 편리합니다.
이벤트 메서드 정의 규칙
- EventSource 클래스에 정의된 가상이 아닌 void 반환 메서드는 기본적으로 이벤트 로깅 메서드입니다.
- 가상 또는 비 void 반환 메서드는 System.Diagnostics.Tracing.EventAttribute 표시된 경우에만 포함됩니다.
- 한정 메서드를 비로그로 표시하려면 System.Diagnostics.Tracing.NonEventAttribute을(를) 사용하여 장식해야 합니다.
- 이벤트 로깅 메서드에는 이벤트 ID가 연결되어 있습니다. 이 작업은 메서드를 System.Diagnostics.Tracing.EventAttribute 데코레이팅하거나 클래스에 있는 메서드의 서수로 암시적으로 수행할 수 있습니다. 예를 들어 클래스의 첫 번째 메서드에 암시적 번호 매기기를 사용하면 ID가 1이고, 두 번째 메서드에는 ID 2가 있습니다.
- 이벤트 로깅 메서드는 WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId 또는 WriteEventWithRelatedActivityIdCore 오버로드를 호출해야 합니다.
- 암시적이든 명시적이든 이벤트 ID는 호출하는 WriteEvent* API에 전달된 첫 번째 인수와 일치해야 합니다.
- EventSource 메서드에 전달된 인수의 수, 형식 및 순서는 WriteEvent* API에 전달되는 방식과 일치해야 합니다. WriteEvent의 경우 인수는 이벤트 ID를 따릅니다. WriteEventWithRelatedActivityId의 경우 인수는 relatedActivityId를 따릅니다. WriteEvent*Core 메서드의 경우 인수를
data
매개 변수로 수동으로 직렬화해야 합니다. - 이벤트 이름에는
<
또는>
문자를 포함할 수 없습니다. 사용자 정의 메서드도 이러한 문자를 포함할 수 없지만async
메서드를 포함하도록 컴파일러에서 다시 작성합니다. EventSource에서 모든 비 이벤트 메서드를 NonEventAttribute로 표시하세요, 생성된 메서드가 이벤트가 되지 않도록 합니다.
모범 사례
- EventSource에서 파생되는 형식은 일반적으로 계층 구조에 중간 형식이 없거나 인터페이스를 구현하지 않습니다. 유용할 수 있는 몇 가지 예외는 아래 고급 사용자 지정을 참조하세요.
- 일반적으로 EventSource 클래스의 이름은 EventSource의 잘못된 공용 이름입니다. 로깅 구성 및 로그 뷰어에 표시되는 공용 이름은 전역적으로 고유해야 합니다. 따라서 System.Diagnostics.Tracing.EventSourceAttribute사용하여 EventSource에 공용 이름을 지정하는 것이 좋습니다. 위에서 사용한 "데모"라는 이름은 짧고 고유하지 않을 수 있으므로 프로덕션 용도로는 적합하지 않습니다. 일반적인 규칙은 "MyCompany-Samples-Demo"와 같이
.
또는-
있는 계층적 이름을 구분 기호로 사용하거나 EventSource에서 이벤트를 제공하는 어셈블리 또는 네임스페이스의 이름을 사용하는 것입니다. 공용 이름의 일부로 "EventSource"를 포함하지 않는 것이 좋습니다. - 이벤트 ID를 명시적으로 할당합니다. 이렇게 하면 소스 클래스의 코드를 다시 정렬하거나 중간에 메서드를 추가하는 등의 무해한 변경 내용이 각 메서드와 연결된 이벤트 ID를 변경하지 않습니다.
- 작업 단위의 시작과 끝을 나타내는 이벤트를 작성할 때 규칙에 따라 이러한 메서드의 이름은 접미사 'Start' 및 'Stop'으로 지정됩니다. 예를 들어 'RequestStart' 및 'RequestStop'입니다.
- 이전 버전과의 호환성을 위해 필요한 경우가 아니면 EventSourceAttribute의 Guid 속성에 명시적 값을 지정하지 마세요. 기본 Guid 값은 원본 이름에서 파생되므로 도구에서 사람이 읽을 수 있는 이름을 더 많이 수락하고 동일한 Guid를 파생할 수 있습니다.
- 이벤트 발생과 관련된 리소스 집약적 작업을 수행하기 전에 IsEnabled() 호출합니다(예: 이벤트가 비활성화된 경우 필요하지 않은 값비싼 이벤트 인수 계산).
- EventSource 개체를 다시 호환되도록 유지하고 적절하게 버전을 지정합니다. 이벤트의 기본 버전은 0입니다. EventAttribute.Version설정하여 버전을 변경할 수 있습니다. 이벤트와 함께 serialize된 데이터를 변경할 때마다 이벤트 버전을 변경합니다. 항상 이벤트 선언의 끝에 새 직렬화된 데이터를 추가합니다. 즉, 메서드 매개 변수 목록의 끝에 추가합니다. 가능하지 않은 경우 새 ID를 사용하여 새 이벤트를 만들어 이전 이벤트를 바꿉니다.
- 이벤트 메서드를 선언할 때 고정 크기의 데이터 앞에 고정 크기 페이로드 데이터를 지정합니다.
- null 문자를 포함하는 문자열을 사용하지 마세요. ETW EventSource에 대한 매니페스트를 생성할 때, C# 문자열에 null 문자가 포함될 수 있음에도 불구하고 모든 문자열을 null로 종료된 것으로 선언합니다. 문자열에 null 문자가 포함된 경우 전체 문자열이 이벤트 페이로드에 기록되지만 모든 파서는 첫 번째 null 문자를 문자열의 끝으로 처리합니다. 문자열 뒤의 페이로드 인수가 있는 경우 문자열의 나머지 부분에서는 의도한 값 대신 구문 분석됩니다.
일반적인 이벤트 사용자 지정
이벤트 세부 정보 표시 수준 설정
각 이벤트에는 세부 정보 표시 수준이 있으며 이벤트 구독자는 EventSource의 모든 이벤트를 특정 세부 정보 수준까지 사용하도록 설정하는 경우가 많습니다. 이벤트는 Level 속성을 사용하여 세부 정보 표시 수준을 선언합니다. 예를 들어 이 EventSource에서 정보 수준 이하의 이벤트를 요청하는 구독자는 Verbose DebugMessage 이벤트를 기록하지 않습니다.
[EventSource(Name = "MyCompany-Samples-Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1, Level = EventLevel.Informational)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
[Event(2, Level = EventLevel.Verbose)]
public void DebugMessage(string message) => WriteEvent(2, message);
}
EventAttribute에서 이벤트의 세부 정보 수준을 지정하지 않으면 기본적으로 Informational로 설정됩니다.
모범 사례
비교적 드문 경고 또는 오류의 경우 Informational보다 작은 수준을 사용합니다. 확신이 서지 않을 때는 "Informational"의 기본 설정을 따르고, 초당 1000개 이상의 이벤트가 발생하는 경우에는 "Verbose"를 사용합니다.
이벤트 키워드 설정
일부 이벤트 추적 시스템은 추가 필터링 메커니즘으로 키워드를 지원합니다. 세부 수준별로 이벤트를 분류하는 세부 정보 표시와 달리 키워드는 코드 기능 영역과 같은 다른 기준에 따라 이벤트를 분류하거나 특정 문제를 진단하는 데 유용합니다. 키워드의 이름은 비트 플래그이며 각 이벤트에는 모든 키워드 조합이 적용될 수 있습니다. 예를 들어 아래 EventSource는 요청 처리와 관련된 일부 이벤트 및 시작과 관련된 다른 이벤트를 정의합니다. 개발자가 시작의 성능을 분석하려는 경우 시작 키워드로 표시된 이벤트만 로깅하도록 설정할 수 있습니다.
[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1, Keywords = Keywords.Startup)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
[Event(2, Keywords = Keywords.Requests)]
public void RequestStart(int requestId) => WriteEvent(2, requestId);
[Event(3, Keywords = Keywords.Requests)]
public void RequestStop(int requestId) => WriteEvent(3, requestId);
public class Keywords // This is a bitvector
{
public const EventKeywords Startup = (EventKeywords)0x0001;
public const EventKeywords Requests = (EventKeywords)0x0002;
}
}
키워드는 Keywords
라는 중첩 클래스를 사용하여 정의해야 하며 각 개별 키워드는 형식화된 멤버 public const EventKeywords
의해 정의됩니다.
모범 사례
키워드는 대용량 이벤트를 구분할 때 더 중요합니다. 이를 통해 이벤트 소비자는 세부 정보를 높은 수준으로 높일 수 있지만 이벤트의 좁은 하위 집합만 사용하도록 설정하여 성능 오버헤드 및 로그 크기를 관리할 수 있습니다. 1,000초 이상 트리거되는 이벤트는 고유한 키워드에 적합한 후보입니다.
지원되는 매개 변수 형식
EventSource를 사용하려면 제한된 형식 집합만 허용하도록 모든 이벤트 매개 변수를 serialize할 수 있어야 합니다. 다음과 같습니다.
- 기본 데이터 형식: bool, byte(바이트), sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr 및 UIntPtr, Guid, decimal, string, DateTime, DateTimeOffset, TimeSpan
- 열거형
- System.Diagnostics.Tracing.EventDataAttribute특성이 부여된 구조체입니다. 직렬화 가능한 형식을 가진 공용 인스턴스 속성만 serialize됩니다.
- 모든 public 속성이 직렬화 가능한 형식인 익명 형식
- 직렬화 가능한 형식의 배열
- Nullable<T> 여기서 T는 직렬화할 수 있는 형식입니다.
- KeyValuePair<T, U> 여기서 T와 U는 모두 직렬화 가능한 형식입니다.
- 정확히 하나의 형식 T에 대해 IEnumerable<T> 구현하는 형식이며 여기서 T는 직렬화 가능한 형식입니다.
문제 해결
EventSource 클래스는 기본적으로 Exception을 throw하지 않도록 설계되었습니다. 로깅은 종종 선택 사항으로 처리되므로 이 속성은 유용한 속성이며, 일반적으로 애플리케이션이 실패하도록 로그 메시지를 작성하는 동안 오류가 발생하지 않도록 합니다. 그러나 이로 인해 EventSource에서 실수를 찾기가 어려워집니다. 다음은 문제를 해결하는 데 도움이 되는 몇 가지 기술입니다.
- EventSource 생성자에는 EventSourceSettings을(를) 받는 오버로드가 있습니다. ThrowOnEventWriteErrors 플래그를 일시적으로 사용해 보십시오.
- EventSource.ConstructionException 속성은 이벤트 로깅 메서드의 유효성을 검사할 때 생성된 모든 Exception을 저장합니다. 이렇게 하면 다양한 작성 오류가 표시 될 수 있습니다.
- EventSource는 이벤트 ID 0을 사용하여 오류를 기록하며, 이 오류 이벤트에는 오류를 설명하는 문자열이 있습니다.
- 디버깅할 때 동일한 오류 문자열도 Debug.WriteLine()을 사용하여 기록되고 디버그 출력 창에 표시됩니다.
- EventSource는 내부적으로 예외를 발생시키고 오류가 발생할 때 이를 처리합니다. 이러한 예외가 발생하는 시기를 관찰하려면 디버거에서 첫 번째 기회 예외를 사용하도록 설정하거나 .NET 런타임의 예외 이벤트가 활성화된 채로 이벤트 추적을 사용합니다.
고급 사용자 지정
OpCode 및 작업 설정
ETW에는 이벤트 태그 지정 및 필터링을 위한 추가 메커니즘인 작업 및 OpCodes개념이 있습니다. Task 및 Opcode 속성을 사용하여 이벤트를 특정 작업 및 opcode와 연결할 수 있습니다. 예제는 다음과 같습니다.
[EventSource(Name = "Samples-EventSourceDemos-Customized")]
public sealed class CustomizedEventSource : EventSource
{
static public CustomizedEventSource Log { get; } = new CustomizedEventSource();
[Event(1, Task = Tasks.Request, Opcode=EventOpcode.Start)]
public void RequestStart(int RequestID, string Url)
{
WriteEvent(1, RequestID, Url);
}
[Event(2, Task = Tasks.Request, Opcode=EventOpcode.Info)]
public void RequestPhase(int RequestID, string PhaseName)
{
WriteEvent(2, RequestID, PhaseName);
}
[Event(3, Keywords = Keywords.Requests,
Task = Tasks.Request, Opcode=EventOpcode.Stop)]
public void RequestStop(int RequestID)
{
WriteEvent(3, RequestID);
}
public class Tasks
{
public const EventTask Request = (EventTask)0x1;
}
}
EventName>Start 및 <EventName>Stop을 <명명 패턴이 있는 후속 이벤트 ID를 사용하여 두 개의 이벤트 메서드를 선언하여 EventTask 개체를 암시적으로 만들 수 있습니다. 이러한 이벤트는 클래스 정의에서 나란히 선언되어야 하며 <EventName>Start 메서드가 먼저 와야 합니다.
자체 설명(추적 로깅) 및 매니페스트 이벤트 형식
이 개념은 ETW에서 EventSource를 구독하는 경우에만 중요합니다. ETW에는 이벤트, 매니페스트 형식 및 자체 설명(추적 로깅이라고도 함) 형식을 기록할 수 있는 두 가지 방법이 있습니다. 매니페스트 기반 EventSource 개체는 초기화 시 클래스에 정의된 이벤트를 나타내는 XML 문서를 생성하고 기록합니다. 이렇게 하려면 EventSource가 자체에 대해 반영하여 공급자 및 이벤트 메타데이터를 생성해야 합니다. 각 이벤트에 대한 자체 설명 포맷 메타데이터는 선제적인 전송이 아닌, 이벤트 데이터와 함께 인라인으로 전송됩니다. 자체 설명 방법은 미리 정의된 이벤트 로깅 메서드를 만들지 않고도 임의 이벤트를 보낼 수 있는 보다 유연한 Write 메서드를 지원합니다. 리플렉션을 서둘러 실행하지 않으므로 시작 시 약간 더 빠릅니다. 그러나 각 이벤트와 함께 내보내는 추가 메타데이터는 작은 성능 오버헤드를 추가하므로 많은 양의 이벤트를 보낼 때는 바람직하지 않을 수 있습니다.
자체 설명 이벤트 형식을 사용하려면 EventSource(String) 생성자, EventSource(String, EventSourceSettings) 생성자를 사용하거나 EventSourceSettings에서 EtwSelfDescribingEventFormat 플래그를 설정하여 EventSource를 생성합니다.
인터페이스를 구현하는 EventSource 형식
EventSource 형식은 인터페이스를 사용하여 공통 로깅 대상을 정의하는 다양한 고급 로깅 시스템에서 원활하게 통합하기 위해 인터페이스를 구현할 수 있습니다. 사용 가능한 예는 다음과 같습니다.
public interface IMyLogging
{
void Error(int errorCode, string msg);
void Warning(string msg);
}
[EventSource(Name = "Samples-EventSourceDemos-MyComponentLogging")]
public sealed class MyLoggingEventSource : EventSource, IMyLogging
{
public static MyLoggingEventSource Log { get; } = new MyLoggingEventSource();
[Event(1)]
public void Error(int errorCode, string msg)
{ WriteEvent(1, errorCode, msg); }
[Event(2)]
public void Warning(string msg)
{ WriteEvent(2, msg); }
}
인터페이스 메서드에서 EventAttribute를 지정해야 합니다. 그렇지 않으면(호환성 이유로) 메서드가 로깅 메서드로 처리되지 않습니다. 명명 충돌을 방지하기 위해 명시적 인터페이스 메서드 구현이 허용되지 않습니다.
EventSource 클래스 계층 구조
대부분의 경우 EventSource 클래스에서 직접 파생되는 형식을 작성할 수 있습니다. 그러나 사용자 지정된 WriteEvent 오버로드와 같은 여러 파생 EventSource 형식에서 공유되는 기능을 정의하는 것이 유용한 경우도 있습니다(아래 대용량 이벤트에 대한 성능 최적화 참조).
추상 기본 클래스는 키워드, 작업, opcode, 채널 또는 이벤트를 정의하지 않는 한 사용할 수 있습니다. 다음은 UtilBaseEventSource 클래스가 동일한 구성 요소의 여러 파생 EventSources에 필요한 최적화된 WriteEvent 오버로드를 정의하는 예제입니다. 이러한 파생 형식 중 하나는 아래에 OptimizedEventSource로 설명되어 있습니다.
public abstract class UtilBaseEventSource : EventSource
{
protected UtilBaseEventSource()
: base()
{ }
protected UtilBaseEventSource(bool throwOnEventWriteErrors)
: base(throwOnEventWriteErrors)
{ }
protected unsafe void WriteEvent(int eventId, int arg1, short arg2, long arg3)
{
if (IsEnabled())
{
EventSource.EventData* descrs = stackalloc EventSource.EventData[2];
descrs[0].DataPointer = (IntPtr)(&arg1);
descrs[0].Size = 4;
descrs[1].DataPointer = (IntPtr)(&arg2);
descrs[1].Size = 2;
descrs[2].DataPointer = (IntPtr)(&arg3);
descrs[2].Size = 8;
WriteEventCore(eventId, 3, descrs);
}
}
}
[EventSource(Name = "OptimizedEventSource")]
public sealed class OptimizedEventSource : UtilBaseEventSource
{
public static OptimizedEventSource Log { get; } = new OptimizedEventSource();
[Event(1, Keywords = Keywords.Kwd1, Level = EventLevel.Informational,
Message = "LogElements called {0}/{1}/{2}.")]
public void LogElements(int n, short sh, long l)
{
WriteEvent(1, n, sh, l); // Calls UtilBaseEventSource.WriteEvent
}
#region Keywords / Tasks /Opcodes / Channels
public static class Keywords
{
public const EventKeywords Kwd1 = (EventKeywords)1;
}
#endregion
}
대용량 이벤트에 대한 성능 최적화
EventSource 클래스에는 가변 인수 수에 대한 오버로드를 포함하여 WriteEvent에 대한 여러 오버로드가 있습니다. 다른 오버로드가 일치하지 않으면 params 메서드가 호출됩니다. 불행히도 매개 변수 오버로드는 상대적으로 비쌉니다. 특히 다음과 같습니다.
- 변수 인수를 저장할 배열을 할당합니다.
- 각 매개 변수를 개체로 캐스팅하여 값 형식에 대한 할당을 발생합니다.
- 이러한 개체를 배열에 할당합니다.
- 함수를 호출합니다.
- 각 배열 요소의 형식을 파악하여 직렬화하는 방법을 결정합니다.
이는 특수 형식보다 10~20배 많은 비용이 들 것입니다. 이는 볼륨이 적은 경우는 별로 중요하지 않지만 대용량 이벤트의 경우 중요할 수 있습니다. 매개 변수 오버로드가 사용되지 않도록 보장하는 두 가지 중요한 사례가 있습니다.
- 열거형 형식이 빠른 오버로드 중 하나와 일치하도록 'int'로 캐스팅되었는지 확인합니다.
- 대용량 페이로드에 대한 새로운 빠른 WriteEvent 오버로드를 만듭니다.
다음은 4개의 정수 인수를 사용하는 WriteEvent 오버로드를 추가하는 예제입니다.
[NonEvent]
public unsafe void WriteEvent(int eventId, int arg1, int arg2,
int arg3, int arg4)
{
EventData* descrs = stackalloc EventProvider.EventData[4];
descrs[0].DataPointer = (IntPtr)(&arg1);
descrs[0].Size = 4;
descrs[1].DataPointer = (IntPtr)(&arg2);
descrs[1].Size = 4;
descrs[2].DataPointer = (IntPtr)(&arg3);
descrs[2].Size = 4;
descrs[3].DataPointer = (IntPtr)(&arg4);
descrs[3].Size = 4;
WriteEventCore(eventId, 4, (IntPtr)descrs);
}
.NET