Event Tracing for Windows, or ETW is an efficient facility for applications to write event messages.
You can write the messages to a file or to the real time consumers.
It has a very low overhead, if disabled, and quite efficient if enabled as well. Can be enabled/disabled at runtime.
The Event Tracing API is broken into three distinct components:
Controllers
They configure, start/stop an event tracing session and enable/disable providers. They define size and location of the log file, manage buffer pools and so on. See Controlling Event Tracing Sessions.
Providers
They provide the events. After provider registers itself, a controller can enable/disable it to collect the events it produces. There are different ways a provider can report events (MOF providers, WPP providers, Manifest-based providers, TraceLogging providers), but they all eventually call EventWrite/EventWriteEx APIs at the lower level. See Providing Events.
Consumers
Consumers consume the events. In real time, or by reading a file with events.
Controllers/providers/consumers could be different applications, or could be components used within one application.
Let’s make a simple data logger with ETW
Our application will be writing its events to a file, for later consumption by other apps offline.
Our logging app will be both a controller and a provider.
To start with, we’ll need to include event tracing API headers: #include <evntrace.h> for the controller and #include <evntprov.h> for the provider.
So, as event as provider we need to register one:
// Initialized to random GUID with CoCreateGuid()
GUID providerId;
::CoCreateGuid(&providerId);
// Provider's handle. Unregister with EventUnregister() later.
REGHANDLE providerHandle;
// Registering the provider
::EventRegister(&providerId, nullptr, nullptr, &providerHandle);
providerId is also used in the event controller code below, but does not need to be visible outside our logging app, so can be randomly generated internally.
handle is used in EventWrite calls to log the events.
As an event controller, we need to setup and start a tracing session with:
const char* sessionName = "My unique log session name";
// Event session information
EVENT_TRACE_PROPERTIES properties;
// properties initialization code (skipped, see below) ...
// Stop later with: ControlTrace with
// ControlCode = EVENT_TRACE_CONTROL_STOP.
TRACEHANDLE sessionHandle;
// Starts the trace, copies sessionName into buffer
// whose offset is specified in properties's structure.
::StartTraceA(&sessionHandle, sessionName, &properties)};
sessionName must be unique, otherwise the code will be affecting existing sessions. On the other hand, the sessions are a limited resource, so if your session name is already in use, and you’ve started it, then stop it, and reuse the name.
EVENT_TRACE_PROPERTIES sets up session parameters for subsequent calls of APIs such as StartTrace, ControlTrace, QueryTrace. You only need to set fields needed for that specific call.
The tricky part of EVENT_TRACE_PROPERTIES is that it expects that there will be extra buffers located in memory after it – for the log file name and for the session name. LogFileNameOffset and LoggerNameOffset properties are offsets from the beginning of the EVENT_TRACE_PROPERTIES to those buffers.
The important fields for us to start the trace are:
Wnode.BufferSize– size ofEVENT_TRACE_PROPERTIES+ those buffers allocated after it.Wnode.Guid– GUID for the session. Can be left empty, and the system will generate a new one. But we will be creating a private session, and in this case, we’ll need to assignproviderIdto it.Wnode.ClientContext– clock resolution for the session. We’ll use 1 for QueryPerformanceCounter (see Wnode documentation).Wnode.Flags– must containWNODE_FLAG_TRACED_GUIDLogFileMode– how the file is being logged. We’ll useEVENT_TRACE_PRIVATE_LOGGER_MODE|EVENT_TRACE_PRIVATE_IN_PROCvalues to limit the session to smallest possible scope. Microsoft warns that cross-process event tracing sessions are a limited system resource. And we don’t need that capability. We’ll also useEVENT_TRACE_FILE_MODE_SEQUENTIAL, although I’m not entirely sure what happens if we don’t. See documentations on logging mode constants for details.LogFileNameOffset– offset from beginning ofEVENT_TRACE_PROPERTIESto the log buffer.LoggerNameOffset– offset from beginning ofEVENT_TRACE_PROPERTIESto the event session name buffer.
To simplify the construction of this tricky structure, I made a wrapper. You can construct it, then use its Properties member:
struct EventTracePropertiesWithBuffers {
EventTracePropertiesWithBuffers(const GUID& sessionId, std::string_view logFilePath) {
::ZeroMemory(this, sizeof(EventTracePropertiesWithBuffers));
Properties.Wnode.BufferSize = sizeof(EventTracePropertiesWithBuffers);
Properties.LoggerNameOffset = offsetof(EventTracePropertiesWithBuffers, SessionName);
Properties.LogFileNameOffset = offsetof(EventTracePropertiesWithBuffers, LogFilePath);
Properties.Wnode.Flags = WNODE_FLAG_TRACED_GUID;
Properties.Wnode.ClientContext = 1; //QPC clock resolution
// For private session, use the Provider's id instead of a unique session ID.
Properties.Wnode.Guid = sessionId;
// See: https://docs.microsoft.com/en-us/windows/win32/etw/logging-mode-constants
Properties.LogFileMode =
EVENT_TRACE_FILE_MODE_SEQUENTIAL
| EVENT_TRACE_PRIVATE_LOGGER_MODE
| EVENT_TRACE_PRIVATE_IN_PROC;
SetLogFilePath(logFilePath);
}
void SetLogFilePath(std::string_view logFilePath) {
assert(logFilePath.size() <= std::extent<decltype(LogFilePath)>::value);
std::copy(logFilePath.begin(), logFilePath.end(), std::begin(LogFilePath));
}
EVENT_TRACE_PROPERTIES Properties;
char SessionName[256]; // Arbitrary max size for the buffer, but 1024 is the system limit.
char LogFilePath[1024]; // Max supported filename length is 1024
};
After the session has been started, we need to enable the provider in it with EnableTraceEx2 (some other APIs can be used as well):
::EnableTraceEx2(
sessionHandle,
&providerId,
EVENT_CONTROL_CODE_ENABLE_PROVIDER,
TRACE_LEVEL_INFORMATION,
0,
0,
0,
NULL
);
After this, we can start writing events to the provider, and they should start appearing in our log file:
// Our message
std::span<const std::byte> message;
// Event descriptor.
// We don't really use any of the fields here.
constexpr static const EVENT_DESCRIPTOR c_descriptor = {
0x1, // Id
0x1, // Version
0x0, // Channel
0x0, // LevelSeverity
0x0, // Opcode
0x0, // Task
0x0, // Keyword
};
EVENT_DATA_DESCRIPTOR eventDataDescriptors[1];
EventDataDescCreate(&eventDataDescriptors[0], message.data(), static_cast<ULONG>(message.size()));
::EventWrite(providerHandle, &c_descriptor, 1, eventDataDescriptors);
After we’re done, we’ll need to disable provider, stop the session, unregister the provider.
::EnableTraceEx2(
sessionHandle,
&providerId,
EVENT_CONTROL_CODE_DISABLE_PROVIDER,
TRACE_LEVEL_INFORMATION,
0,
0,
0,
NULL
);
::ControlTraceA(sessionHandle, sessionName, &properties, EVENT_TRACE_CONTROL_STOP);
::EventUnregister(providerHandle);
After the file is written, it can be read via OpenTrace + ProcessTrace APIs. You can see this being used in my test code (see ReadRecords method).
The whole logging class with error handling and some more comments can be found here.