/ Performance,Windows / 207浏览

如何通过 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;
}
Windows是如何区分互联网下载文件和本地文件的
Windows是如何区分互联网下载文件和本地文件的
如何低成本的获取到应用卡顿情况
如何低成本的获取到应用卡顿情况
【译】ETW 堆跟踪 – 每个分配都被记录
【译】ETW 堆跟踪 – 每个分配都被记录
【译】Wait Analysis – 寻找空闲时间
【译】Wait Analysis – 寻找空闲时间
如何通过 ETW Provider 来记录应用日志
如何通过 ETW Provider 来记录应用日志
【译】调查并确定 Windows 运行速度变慢问题
【译】调查并确定 Windows 运行速度变慢问题

2

发表回复

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