/ Performance,Windows / 43浏览

如何通过 C++ 实时监听 ETW 事件

引入

ETW(Event Tracing for Windows)是 Windows 平台上非常强大的事件跟踪机制,广泛用于调试、性能分析以及日志记录等场景。相较于传统的日志方式,ETW 在内核态和用户态都有提供良好的事件跟踪接口,可以帮助开发者更灵活地收集关键信息。

在许多应用场景下,我们不仅要监听某个 Provider(事件提供者)产生的所有事件,还需要进一步通过 Event ID 或关键字(Keyword)等进行过滤。ETW 本身提供了非常高效的内核态过滤功能,可以在事件触发时就直接按需过滤,减少数据传输和处理的开销。

步骤

以下是实现实时监听并根据 Event ID 进行过滤的核心步骤解析。

1. 启动 ETW Session

  • EVENT_TRACE_PROPERTIES 结构
    这是配置 ETW 会话属性的核心结构,除了常用的缓冲区大小、标记位(WNODE_FLAG_TRACED_GUID)等,还需要指定 LogFileModeEVENT_TRACE_REAL_TIME_MODE,才能开启实时模式。
  • StartTrace
    通过 StartTrace 来启动一个新的 ETW Session,并返回一个 TRACEHANDLE 会话句柄。在调用时,我们需要将 EVENT_TRACE_PROPERTIES 与会话名称一起传入。
ULONG status = StartTrace(&hSession, sessionName, pSessionProperties);

如果调用成功,hSession 会被赋值,用于后续操作(如 EnableTraceEx2ControlTrace 等)。

2. 配置内核态过滤器(EVENT_FILTER_EVENT_ID)

  • EVENT_FILTER_EVENT_ID 结构
    这是一个用于表明我们想要捕获(或排除)哪些事件 ID 的结构。其主要成员包括:
    • FilterInTRUE 表示“包含”指定的事件 ID;若为 FALSE,则表示排除。
    • Count:要过滤的事件 ID 个数。
    • Events:指向实际的事件 ID 数组。
  • EVENT_FILTER_DESCRIPTOR
    这个结构用于向 ETW API 传递过滤器的元信息,它包括指向过滤器数据的指针、数据大小以及过滤器的类型。
    此处我们将 Type 设置为 EVENT_FILTER_TYPE_EVENT_ID,表示按事件 ID 进行过滤。
  • ENABLE_TRACE_PARAMETERS
    在调用 EnableTraceEx2 时,需要通过该结构把过滤器数组传进去(实际上可以传入多个过滤器)。
    我们在示例中只用到一个过滤器,因此 FilterDescCount = 1

3. 启用 Provider 并应用过滤规则

  • EnableTraceEx2
    该函数是新的、较灵活的 Provider 启用接口。相比 EnableTrace,它支持传入更多参数,比如过滤描述符。
    其中比较关键的参数有:
    • EVENT_CONTROL_CODE_ENABLE_PROVIDER:表示启用 Provider。
    • TRACE_LEVEL_VERBOSE:设置事件级别为最高级别。
    • MatchAnyKeyword = 0xFFFFFFFFMatchAllKeyword = 0:如果对关键字无特殊需求,可以这样表示“不限制”。
    • enableParams:我们前面构造好的过滤参数。
status = EnableTraceEx2(
    hSession,
    &providerGuid,
    EVENT_CONTROL_CODE_ENABLE_PROVIDER,
    TRACE_LEVEL_VERBOSE,
    0xFFFFFFFF,
    0,
    0,
    &enableParams
);

4. 打开 Trace 并实时消费事件

  • OpenTrace
    打开指定会话的 Trace,用于后续调用 ProcessTrace 读取事件数据。
    我们通过设置 logFile.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD 来指定实时模式和 EventRecord 回调类型。
  • ProcessTrace
    开始循环接收事件,直到会话停止或遇到其他终止条件。
    在循环过程中,每个新到的事件都会调用我们在 logFile.EventRecordCallback 中指定的回调函
EVENT_TRACE_LOGFILEW logFile = {};
logFile.LoggerName          = (LPWSTR)sessionName; 
logFile.ProcessTraceMode    = PROCESS_TRACE_MODE_REAL_TIME 
                            | PROCESS_TRACE_MODE_EVENT_RECORD;
logFile.EventRecordCallback = (PEVENT_RECORD_CALLBACK)EventRecordCallback;

5. 回调函数:EventRecordCallback

该函数的签名固定,入参是 PEVENT_RECORD。通过 EventRecord->EventHeader.EventDescriptor,可以拿到事件的 ID、Level 等信息(在内核态已经过滤了指定 ID,因此进入回调的事件一定符合过滤条件)。

若需要进一步获取其他字段(如自定义字段、Payload 数据等),可以使用以下 TDH(Tracing Data Helper)API:

  • TdhGetEventInformation():获取事件元数据。
  • TdhFormatProperty():获取并格式化指定属性。

6. 停止会话 & 资源清理

在不再需要监听时,通过 ControlTrace 并指定 EVENT_TRACE_CONTROL_STOP 来停止会话。随后可以调用 CloseTrace 关闭之前打开的 Trace。最后释放掉分配的内存等资源。

常见问题与注意事项

  1. 权限问题
    启动 ETW Session 需要管理员权限,建议在“以管理员身份”运行时执行,否则可能会返回权限不足的错误。
  2. 重复启动会话
    当会话名相同且已存在时,StartTrace 可能会失败或返回特定错误,需注意清理或改名。
  3. 过滤规则的上限
    不同版本的 Windows 对过滤器支持可能略有差异,如果需要更复杂的过滤(例如同时根据多个关键字和事件级别过滤),需要仔细阅读相关文档,并在 ENABLE_TRACE_PARAMETERS 中传入更多过滤描述符。
  4. 实时模式下的负载
    如果流量巨大,实时模式可能会消耗较多 CPU 和内存。实际生产环境中要结合性能需求,必要时采用文件日志或分段模式进行持久化。
  5. 回调函数中的处理
    在回调函数中尽量保证轻量级的操作,因为它是由系统在内部线程中同步回调。如果回调过于耗时,可能会导致事件堆积或丢失。

完整代码

#include <windows.h>
#include <evntrace.h>
#include <tdh.h>         // 包含 TdhGetEventInformation 等
#include <iostream>
#include <string>

#pragma comment(lib, "tdh.lib")
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "ole32.lib")

//---------------------------------------------------------------------------
// 全局回调,用于处理已通过内核态过滤后的事件
//---------------------------------------------------------------------------
static VOID WINAPI EventRecordCallback(PEVENT_RECORD pEventRecord)
{
    // 这里的事件一定是 Event ID = 100 或 200(因为已在内核态过滤)
    USHORT eventId = pEventRecord->EventHeader.EventDescriptor.Id;
    std::wcout << L"Received ETW event, ID = " << eventId << L"\n";

    // 如果需要获取更多字段,可使用 TdhGetEventInformation()、TdhFormatProperty() 等
}

//---------------------------------------------------------------------------
// BufferCallback:可选,用于监测缓冲区情况,一般直接返回 TRUE
//---------------------------------------------------------------------------
static ULONG WINAPI BufferCallback(EVENT_TRACE_LOGFILEW* /*pLogFile*/)
{
    return TRUE; // 返回 TRUE 继续处理
}

//---------------------------------------------------------------------------
// 主函数
//---------------------------------------------------------------------------
int wmain()
{
    //------------------------------------------------------------------------
    // 0. Provider GUID & Event ID
    //------------------------------------------------------------------------
    // 目标 Provider 的 GUID: {cfc18ec0-96b1-4eba-961b-622caee05b0a}
    GUID providerGuid = 
    { 0xcfc18ec0, 0x96b1, 0x4eba, {0x96, 0x1b, 0x62, 0x2c, 0xae, 0xe0, 0x5b, 0x0a} };

    // 只想捕获的事件 ID 列表
    USHORT desiredEvents[] = { 100, 200 };

    //------------------------------------------------------------------------
    // 1. 准备并启动 ETW Session(实时模式)
    //------------------------------------------------------------------------
    // 1.1 分配 EVENT_TRACE_PROPERTIES
    size_t bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + 2 * MAX_PATH * sizeof(WCHAR);
    EVENT_TRACE_PROPERTIES* pSessionProperties = 
        (EVENT_TRACE_PROPERTIES*)malloc(bufferSize);
    if (!pSessionProperties)
    {
        std::wcerr << L"Failed to allocate memory for session properties.\n";
        return 1;
    }
    ZeroMemory(pSessionProperties, bufferSize);

    pSessionProperties->Wnode.BufferSize = (ULONG)bufferSize;
    pSessionProperties->Wnode.Flags      = WNODE_FLAG_TRACED_GUID;
    pSessionProperties->LogFileMode      = EVENT_TRACE_REAL_TIME_MODE;
    pSessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);

    LPCWSTR sessionName = L"MyCppEtwSession";
    TRACEHANDLE hSession = 0;

    // 1.2 调用 StartTrace
    ULONG status = StartTrace(&hSession, sessionName, pSessionProperties);
    if (status != ERROR_SUCCESS)
    {
        std::wcerr << L"StartTrace failed, error: " << status << L"\n";
        free(pSessionProperties);
        return 1;
    }

    //------------------------------------------------------------------------
    // 2. 准备用于 “内核态” 过滤 Event ID 的数据结构
    //    关键在 EnableTraceEx2 + EVENT_FILTER_DESCRIPTOR
    //------------------------------------------------------------------------
    // 2.1 填充 EVENT_FILTER_EVENT_ID 结构(声明捕获的 Event ID 列表)
    EVENT_FILTER_EVENT_ID eventIdFilter = {};
    eventIdFilter.FilterIn = TRUE; // TRUE 表示 "只" 捕获这些事件(而不是排除)
    eventIdFilter.Count    = (USHORT)_countof(desiredEvents);
    eventIdFilter.Events   = desiredEvents;

    // 2.2 包装成 EVENT_FILTER_DESCRIPTOR
    EVENT_FILTER_DESCRIPTOR filterDesc = {};
    ZeroMemory(&filterDesc, sizeof(filterDesc));

    // 计算大小:EVENT_FILTER_EVENT_ID 本身 + (Count-1)*sizeof(USHORT)
    filterDesc.Ptr  = (ULONG_PTR)&eventIdFilter;
    filterDesc.Size = sizeof(EVENT_FILTER_EVENT_ID) 
                    + (eventIdFilter.Count - 1) * sizeof(USHORT);
    filterDesc.Type = EVENT_FILTER_TYPE_EVENT_ID; // 按 Event ID 过滤

    // 2.3 构造 ENABLE_TRACE_PARAMETERS,用于 EnableTraceEx2
    ENABLE_TRACE_PARAMETERS enableParams;
    ZeroMemory(&enableParams, sizeof(enableParams));
    enableParams.Version          = ENABLE_TRACE_PARAMETERS_VERSION_2;
    enableParams.EnableProperty   = 0;      // 不需要特殊属性
    enableParams.ControlFlags     = 0;
    enableParams.SourceId         = 0;
    enableParams.FilterDescCount  = 1;      // 我们的过滤器个数
    enableParams.EnableFilterDesc = &filterDesc;

    //------------------------------------------------------------------------
    // 3. 调用 EnableTraceEx2,传入过滤规则
    //------------------------------------------------------------------------
    status = EnableTraceEx2(
        hSession,                      // 第一步 StartTrace 返回的会话句柄
        &providerGuid,                 // 要启用的 Provider
        EVENT_CONTROL_CODE_ENABLE_PROVIDER,
        TRACE_LEVEL_VERBOSE,           // 事件级别
        0xFFFFFFFF,                    // MatchAnyKeyword
        0,                             // MatchAllKeyword
        0,                             // 超时时间(0 表示默认)
        &enableParams                  // 我们的过滤参数
    );
    if (status != ERROR_SUCCESS)
    {
        std::wcerr << L"EnableTraceEx2 failed, error: " << status << L"\n";
        goto Cleanup;
    }

    //------------------------------------------------------------------------
    // 4. OpenTrace + ProcessTrace(实时消费事件)
    //------------------------------------------------------------------------
    EVENT_TRACE_LOGFILEW logFile = {};
    logFile.LoggerName          = (LPWSTR)sessionName; 
    logFile.ProcessTraceMode    = PROCESS_TRACE_MODE_REAL_TIME 
                                | PROCESS_TRACE_MODE_EVENT_RECORD;
    logFile.EventRecordCallback = (PEVENT_RECORD_CALLBACK)EventRecordCallback;
    logFile.BufferCallback      = (PEVENT_TRACE_BUFFER_CALLBACKW)BufferCallback;

    TRACEHANDLE hTrace = OpenTraceW(&logFile);
    if (hTrace == INVALID_PROCESSTRACE_HANDLE)
    {
        std::wcerr << L"OpenTrace failed.\n";
        goto Cleanup;
    }

    std::wcout << L"Start capturing ETW events (only EventID=100 or 200)...\n";
    status = ProcessTrace(&hTrace, 1, NULL, NULL);
    if (status != ERROR_SUCCESS)
    {
        std::wcerr << L"ProcessTrace failed, error: " << status << L"\n";
    }

    //------------------------------------------------------------------------
    // 5. 清理:停止会话 & 关闭 Trace
    //------------------------------------------------------------------------
    status = ControlTrace(hSession, sessionName, pSessionProperties, EVENT_TRACE_CONTROL_STOP);
    if (status != ERROR_SUCCESS)
    {
        std::wcerr << L"ControlTrace (Stop) failed, error: " << status << L"\n";
    }
    CloseTrace(hTrace);

Cleanup:
    if (pSessionProperties)
    {
        free(pSessionProperties);
        pSessionProperties = nullptr;
    }
    std::wcout << L"ETW capturing finished.\n";
    return 0;
}
【译】ETW 堆跟踪 – 每个分配都被记录
【译】ETW 堆跟踪 – 每个分配都被记录
【译】Wait Analysis – 寻找空闲时间
【译】Wait Analysis – 寻找空闲时间
如何通过 ETW Provider 来记录应用日志
如何通过 ETW Provider 来记录应用日志
【译】调查并确定 Windows 运行速度变慢问题
【译】调查并确定 Windows 运行速度变慢问题
【译】丢失的 WPA 文档 —— 磁盘使用
【译】丢失的 WPA 文档 —— 磁盘使用
【译】丢失的 WPA 文档 —— CPU 调度
【译】丢失的 WPA 文档 —— CPU 调度

1

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注