다음을 통해 공유


EventSource 이벤트를 만드는 계측 코드

이 문서의 적용 대상: ✔️ .NET Core 3.1 이상 버전 ✔️ .NET Framework 4.5 이상 버전

시작 가이드에서는 최소한의 EventSource를 만들고 추적 파일에서 이벤트를 수집하는 방법을 보여 줍니다. 이 자습서에서는 System.Diagnostics.Tracing.EventSource를 사용하여 이벤트를 만드는 방법에 대해 자세히 설명합니다.

최소 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에서 상속합니다.
  • 생성하려는 각 이벤트 유형에 대해 메서드를 정의해야 합니다. 이 메서드의 이름은 생성되는 이벤트의 이름을 사용하여 지정해야 합니다. 이벤트에 추가 데이터가 있는 경우 인수를 사용하여 전달해야 합니다. 이러한 이벤트 인수는 특정 형식만 허용되도록 직렬화되어야 합니다.
  • 각 메서드에는 ID(이벤트를 나타내는 숫자 값)와 이벤트 메서드의 인수를 전달하는 WriteEvent를 호출하는 본문이 있습니다. ID는 EventSource 내에서 고유해야 합니다. ID는 System.Diagnostics.Tracing.EventAttribute를 사용하여 명시적으로 할당됩니다.
  • EventSources는 싱글톤 인스턴스로 사용됩니다. 따라서 이 싱글톤을 나타내는 Log라는 규칙에 따라 정적 변수를 정의하는 것이 편리합니다.

이벤트 메서드 정의 규칙

  1. EventSource 클래스에 정의된 가상이 아닌 void 반환 메서드는 기본적으로 이벤트 로깅 메서드입니다.
  2. 가상 또는 비 void 반환 메서드는 System.Diagnostics.Tracing.EventAttribute로 표시된 경우에만 포함됩니다.
  3. 정규화 메서드를 비로깅으로 표시하려면 System.Diagnostics.Tracing.NonEventAttribute를 사용하여 데코레이트해야 합니다.
  4. 이벤트 로깅 메서드에는 이벤트 ID가 연결되어 있습니다. 이 작업은 메서드를 System.Diagnostics.Tracing.EventAttribute로 데코레이팅하거나 클래스에 있는 메서드의 서수로 암시적으로 수행할 수 있습니다. 예를 들어 클래스의 첫 번째 메서드에 암시적 번호 매기기를 사용하면 ID 1이 있고 두 번째 메서드에는 ID 2가 있습니다.
  5. 이벤트 로깅 메서드는 WriteEvent, WriteEventCore, WriteEventWithRelatedActivityId 또는 WriteEventWithRelatedActivityIdCore 오버로드를 호출해야 합니다.
  6. 암시적이든 명시적이든 이벤트 ID는 호출하는 WriteEvent* API에 전달된 첫 번째 인수와 일치해야 합니다.
  7. EventSource 메서드에 전달된 인수의 수, 형식 및 순서는 WriteEvent* API에 전달되는 방식과 일치해야 합니다. WriteEvent의 경우 인수는 이벤트 ID를 따릅니다. WriteEventWithRelatedActivityId의 경우 인수는 relatedActivityId를 따릅니다. WriteEvent*Core 메서드의 경우 인수를 data 매개 변수에 수동으로 직렬화해야 합니다.
  8. 이벤트 이름에는 < 또는 > 문자를 포함할 수 없습니다. 사용자 정의 메서드도 이러한 문자를 포함할 수 없지만 async 메서드는 이러한 문자를 포함하도록 컴파일러에서 다시 작성합니다. 이렇게 생성된 메서드가 이벤트가 되지 않도록 하려면 EventSource에서 모든 비 이벤트 메서드를 NonEventAttribute로 표시합니다.

모범 사례

  1. EventSource에서 파생되는 형식은 일반적으로 계층 구조에 중간 형식이 없거나 인터페이스를 구현하지 않습니다. 유용할 수 있는 몇 가지 예외는 아래 고급 사용자 지정을 참조하세요.
  2. 일반적으로 EventSource 클래스의 이름은 EventSource의 잘못된 공용 이름입니다. 로깅 구성 및 로그 뷰어에 표시되는 공용 이름은 전역적으로 고유해야 합니다. 따라서 System.Diagnostics.Tracing.EventSourceAttribute를 사용하여 EventSource에 공용 이름을 지정하는 것이 좋습니다. 위에서 사용한 "Demo"라는 이름은 짧고 고유하지 않을 수 있으므로 프로덕션 용도로는 적합하지 않습니다. 일반적인 규칙은 "MyCompany-Samples-Demo"와 같이 . 또는 -를 구분 기호로 사용하는 계층적 이름이나 EventSource에서 이벤트를 제공하는 어셈블리 또는 네임스페이스의 이름을 사용하는 것입니다. 공용 이름의 일부로 "EventSource"를 포함하지 않는 것이 좋습니다.
  3. 이벤트 ID를 명시적으로 할당합니다. 이렇게 하면 소스 클래스의 코드를 다시 정렬하거나 중간에 메서드를 추가하는 등의 무해한 변경 내용이 각 메서드와 연결된 이벤트 ID를 변경하지 않습니다.
  4. 작업 단위의 시작과 끝을 나타내는 이벤트를 작성할 때 규칙에 따라 이러한 메서드의 이름은 접미사 'Start' 및 'Stop'으로 지정됩니다. 예를 들어 'RequestStart' 및 'RequestStop'입니다.
  5. 이전 버전과의 호환성을 위해 필요한 경우가 아니면 EventSourceAttribute의 Guid 속성에 명시적 값을 지정하지 마세요. 기본 Guid 값은 소스 이름에서 파생됩니다. 이를 통해 도구는 사람이 더 읽기 쉬운 이름을 받아들이고 동일한 Guid를 파생할 수 있습니다.
  6. 이벤트를 사용하지 않도록 설정한 경우 필요하지 않은 값비싼 이벤트 인수를 계산하는 등 이벤트 발생과 관련된 리소스 집약적인 작업을 수행하기 전에 IsEnabled()를 호출합니다.
  7. EventSource 개체를 다시 호환되도록 유지하고 적절하게 버전을 지정합니다. 이벤트의 기본 버전은 0입니다. EventAttribute.Version를 설정하여 버전을 변경할 수 있습니다. 이벤트와 함께 직렬화된 데이터를 변경할 때마다 이벤트 버전을 변경합니다. 항상 이벤트 선언의 끝에 새 직렬화된 데이터를 추가합니다. 즉, 메서드 매개 변수 목록의 끝에 추가합니다. 이렇게 할 수 없는 경우 새 ID를 사용하여 새 이벤트를 만들어 이전 이벤트를 바꿉니다.
  8. 이벤트 메서드를 선언할 때 가변 크기의 데이터 앞에 고정 크기 페이로드 데이터를 지정합니다.
  9. 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보다 낮은 수준을 사용합니다. 의심스러운 경우 정보 기본값을 고수하고 1000개 이벤트/초보다 자주 발생하는 이벤트에 자세한 정보를 사용합니다.

이벤트 키워드 설정

일부 이벤트 추적 시스템은 추가 필터링 메커니즘으로 키워드를 지원합니다. 세부 수준별로 이벤트를 분류하는 세부 정보 표시와 달리 키워드는 코드 기능 영역과 같은 다른 기준에 따라 이벤트를 분류하거나 특정 문제를 진단하는 데 유용합니다. 키워드의 이름은 비트 플래그로 지정되며 각 이벤트에는 모든 키워드 조합이 적용될 수 있습니다. 예를 들어 아래 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를 사용하려면 제한된 형식 세트만 허용하도록 모든 이벤트 매개 변수를 직렬화할 수 있어야 합니다. 이는 다음과 같습니다.

  • 기본 형식: bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, IntPtr 및 UIntPtr, Guid decimal, string, DateTime, DateTimeOffset, TimeSpan
  • 열거형
  • System.Diagnostics.Tracing.EventDataAttribute로 특성이 지정된 구조체입니다. 직렬화 가능한 형식을 가진 공용 인스턴스 속성만 직렬화됩니다.
  • 모든 공용 속성이 직렬화 가능한 형식인 익명 형식
  • 직렬화 가능한 형식의 배열
  • T가 직렬화 가능한 형식인 Nullable<T>
  • T 및 U는 모두 직렬화 가능한 형식인 KeyValuePair<T, U>
  • 정확히 하나의 형식 T에 대해 IEnumerable<T>를 구현하는 형식이며 여기서 T는 직렬화 가능한 형식입니다.

문제 해결

EventSource 클래스는 기본적으로 예외를 throw하지 않도록 설계되었습니다. 로깅은 선택 사항으로 처리되는 경우가 많으며 일반적으로 로그 메시지 작성 오류로 인해 애플리케이션이 실패하는 것을 원하지 않으므로 이는 유용한 속성입니다. 그러나 이로 인해 EventSource에서 실수를 찾기가 어렵습니다. 다음은 문제를 해결하는 데 도움이 되는 몇 가지 기술입니다.

  1. EventSource 생성자에는 EventSourceSettings를 수행하는 오버로드가 있습니다. ThrowOnEventWriteErrors 플래그를 일시적으로 사용하도록 설정합니다.
  2. EventSource.ConstructionException 속성은 이벤트 로깅 메서드의 유효성을 검사할 때 생성된 모든 예외를 저장합니다. 이렇게 하면 다양한 작성 오류가 표시될 수 있습니다.
  3. EventSource는 이벤트 ID 0을 사용하여 오류를 기록하며, 이 오류 이벤트에는 오류를 설명하는 문자열이 있습니다.
  4. 디버깅할 때 동일한 오류 문자열도 Debug.WriteLine()을 사용하여 기록되고 디버그 출력 창에 표시됩니다.
  5. EventSource는 내부적으로 throw한 다음, 오류가 발생할 때 예외를 catch합니다. 이러한 예외가 발생하는 시기를 관찰하려면 디버거에서 첫 번째 예외를 사용하도록 설정하거나 .NET 런타임의 예외 이벤트가 활성화된 이벤트 추적을 사용합니다.

고급 사용자 지정

OpCode 및 작업 설정

ETW에는 이벤트 태그 지정 및 필터링을 위한 추가 메커니즘인 작업 및 OpCode 개념이 있습니다. TaskOpcode 속성을 사용하여 이벤트를 특정 작업 및 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 메서드가 호출됩니다. 불행히도 매개 변수 오버로드는 상대적으로 비쌉니다. 특히 다음과 같습니다.

  1. 변수 인수를 저장할 배열을 할당합니다.
  2. 각 매개 변수를 개체로 캐스팅하여 값 형식에 대한 할당을 발생시킵니다.
  3. 이러한 개체를 배열에 할당합니다.
  4. 함수를 호출합니다.
  5. 각 배열 요소의 형식을 파악하여 직렬화하는 방법을 결정합니다.

이는 특수 형식보다 10~20배 더 비쌀 수 있습니다. 이는 볼륨이 적은 경우는 별로 중요하지 않지만 대용량 이벤트의 경우에는 중요할 수 있습니다. 매개 변수 오버로드가 사용되지 않도록 하는 두 가지 중요한 사례가 있습니다.

  1. 열거형 형식이 빠른 오버로드 중 하나와 일치하도록 'int'로 캐스팅되었는지 확인합니다.
  2. 대용량 페이로드에 대한 새로운 빠른 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);
}