SQL Server Extended Events session driver + event XML parser. Builds the CREATE EVENT SESSION DDL, reads the session's event files incrementally, turns the payload into typed events.
Maintained by Tirion. Used by Calliper. MIT, zero deps.
1.0. Stable API.
Three things every SQL Server XEvent tool ends up rewriting:
- The
CREATE EVENT SESSIONDDL (with theADD EVENTlist, anevent_filetarget in SQL Server's log folder, dispatch latency). - The poll loop reading the event files with
sys.fn_xe_file_target_read_filefrom the last block read, delivering each event once by itspackage0.event_sequence(every event carries it), and counting events missing from that sequence. Reads are bounded (max_events_per_poll, whole file blocks), optionallyCOMPRESSed (gunzip), and, when the host can open more connections (open_reader), spread over up tomax_readersconnections while the reader is behind the server; those connections are excluded from the session's events by application name. - The XML parser.
<data>vs<action>, numeric character refs, escaped-text vs CDATA vs nested-markup values (xml-typed fields such asshowplan_xmlarrive as nested markup and are kept verbatim), per-event-type column dispatch.
sp_statement_completedsql_batch_completedrpc_completedquery_post_execution_showplansqlos.wait_info(filtered by duration)login, added only withsp_statement_completedplus a request completion: it follows a new connection's logon-trigger statements, which complete at nest level 1 before its first request.
XeSessionOptions toggles events on/off and sets dispatch latency.
Completion and showplan events capture request IDs and package0.event_sequence
for conservative consumer-side association. Unknown identities remain -1.
Wait durations are exposed in microseconds like other events, converting the
server's millisecond wait_info.duration; duration predicates use server units.
Enum display text (including CDATA) supplies wait names and Begin/End opcodes.
Server filters (XeSessionOptions::filters, ServerFilter) are emitted inside
each event definition: AND across filters, OR within In / NotIn, text compared
case-insensitively as N'…' literals with quotes doubled and LIKE wildcards
escaped. A filter applies to the events that carry its field and lets the
others through (Profiler's column-filter rule; see ServerFilter).
set_filters() changes them on a running session by dropping and re-adding
its events (SQL Server cannot alter a predicate in place); the session keeps
running and event_sequence continues. server_filter_error() says why a
filter cannot run.
Every event, including wait_info, also excludes the starting connection
(sqlserver.session_id <> <spid>), so the session never records its own polling.
Sessions are server-wide, so each is named owner_prefix plus <spid>_<login time> of the connection that starts it; name() returns it. The prefix has no
default: the caller supplies one unique to its application (letters, digits and
_), and start() rejects an empty or invalid one. start() first drops
sessions with that prefix whose owning connection no longer exists (orphans of
crashed hosts) — only with VIEW SERVER STATE, since without it other connections
are invisible. A session that is created but fails to start is dropped before
start() rethrows. session_started_utc(conn, name) reports when a named
session (for example one left by an older version of the caller) started,
without touching it. orphan_sweep_sql(prefix) exposes the sweep for tests.
Events go to event_file files in SQL Server's log folder, shared by the
captures of one login: event_file_base(prefix, login, slot) names a family
per capture running at the same time (<prefix><login>_<hash>_s<slot>, the
login lowercased to [a-z0-9_], the hash keeping distinct logins apart).
start() takes the lowest slot no defined session uses, then checks again
before starting: of two captures that chose the same slot at once, the one
with the lower session name keeps it and the other moves on. Each capture
reuses its family, and SQL Server's rollover (max_file_size = 32 MB,
max_rollover_files = 4) counts the files left by earlier captures, so a
family never holds more than about 128 MB. A running capture's open file is
never rolled away by another session. poll() reads only the files this
session created (named after its start) and continues from the last file
offset read, returning each event once in event_sequence order; the
sequence also filters any overlap, for example when the file read last was
rolled away and the read restarts. Holes in the sequence are events dropped
under load or rolled away unread; events_lost() counts them (cumulative
since start()). finish() stops the session, which flushes SQL Server's
buffers to the files, returns the events not yet read and drops the session;
stop() abandons it. The files stay after the session is dropped (T-SQL
cannot delete them) until the login's next capture rolls them over;
event_file_pattern() names them so the caller can tell the user.
Reading the files needs VIEW SERVER STATE. File targets in Azure SQL Managed Instance must be blob URLs and are not supported.
#include <xesession/xesession.hpp>
class MyConn : public xesession::IConn {
public:
explicit MyConn(MyOdbcWrapper& c) : conn_(c) {}
void exec(const std::string& sql) override { conn_.exec(sql); }
std::string exec_scalar_text(const std::string& sql) override {
return conn_.exec_scalar_text(sql);
}
std::vector<std::string> exec_text_rows(const std::string& sql) override {
return conn_.exec_text_rows(sql); // every column of every row, row-major
}
private:
MyOdbcWrapper& conn_;
};
MyConn ax{my_odbc};
xesession::XeSessionOptions options;
options.owner_prefix = "myapp_live_";
// Optional: on SQL Server 2016+ event data is then read COMPRESS()ed
// (about 8x less traffic). Any RFC 1952 decoder will do.
options.gunzip = [](std::string_view gz) { return my_gunzip(gz); };
xesession::XeSession session{ax, options};
session.start();
while (running) {
auto events = session.poll();
for (const auto& e : events) {
if (e.name == "sp_statement_completed") {
log("stmt {} ms, {} reads, in {}",
e.duration_us / 1000, e.logical_reads, e.object_name);
}
}
if (session.events_lost() > 0) warn("{} events lost", session.events_lost());
// A backlog comes in batches of options.max_events_per_poll: read on at once.
if (!session.backlog()) std::this_thread::sleep_for(std::chrono::seconds(2));
}
// The events flushed at stop (one batch at most; a reader still behind the
// server leaves the rest, counted by session.events_unread()).
for (const auto& e : session.finish()) handle(e);
// session.event_file_pattern() names the files SQL Server keeps.cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build # checks run in Release too (no assert/NDEBUG)CMake integration:
add_subdirectory(path/to/libxesession)
target_link_libraries(my_app PRIVATE xesession::xesession)All in include/xesession/xesession.hpp:
| Type | Purpose |
|---|---|
xesession::IConn |
Abstract host-provided connection (three methods). |
xesession::XeEvent |
One parsed event. |
xesession::XeSessionOptions |
Session knobs, including filters. |
xesession::ServerFilter |
One server-side filter (field, operator, values). |
xesession::XeSession |
RAII session + poll loop. |
xesession::parse_event_xml(xml) |
Free function. Test + offline replay. |
xesession::session_started_utc(conn, name) |
Free function. Creation time of a running session, read-only. |
xesession::event_file_base(prefix, login, slot) |
Free function. A login's event file family name. |
xesession::server_filter_error(filter) |
Free function. Why a filter cannot run at the server; empty when it can. |
- Drive the connection. Bring your own.
- Delete its event files (T-SQL cannot); each login's files stay in SQL Server's log folder, capped by rollover.
- Predicate trees: filters AND together, with OR only inside In / NotIn.
- History beyond one
XeSessionlifetime.
MIT. Issues at github.com/tirion-tools/libxesession.