TraceEvent Importer (managed) replacement for ReadTrace#515
Open
TraceEvent Importer (managed) replacement for ReadTrace#515
Conversation
…xe dependency in the future - Add `TraceEventImporter` project: managed importer plugin reading `.trc`/`.xel` files via XELite, normalizing SQL text, computing hash IDs, and bulk-loading into the `ReadTrace` schema - Embed `CreateSchema.sql` and `PostLoadFixups.sql` as assembly resources; includes `vwBatchPartialAggsByGroupTimeInterval` view matching ReadTrace.exe output format - Use ReadTrace.exe index naming convention (no `IX_` prefix) for compatibility with `ReadTracePostProcessing.sql` index hints - Add `ProjectReference` in `sqlnexus.csproj`, register importer in `AppConfig.xml` - Set `AppendTargetFrameworkToOutputPath=false` so output goes to `bin\Release\` directly - Implement mutual exclusivity between ReadTrace and TraceEventImporter: auto-disable ReadTrace at startup when both enabled, MessageBox notification on manual toggle - Restrict `SupportedMasks` to `*pssdiag*.xel` and `*LogScout*.xel` to avoid conflicts with CustomXELImporter - Declare `ReadTracePostProcessing.sql` as post-script; update `RunPostScripts` to allow it for both importers - Fix `BulkLoadRowset.GetNewRow()` to handle schema-qualified table names (`[ReadTrace].[tblName]` instead of `[ReadTrace.tblName]`) - Fix `ImportOptions` saved options overwrite bug: add `HasOption()` method, only restore saved values when they actually exist - Skip non-boolean options (e.g., `int`) from checkbox menu to prevent `InvalidCastException` - Add `EnforceTraceImporterExclusivity()` and `IsImporterEnabled()` helpers in `fmImport.cs`
…ty, import logging, and UI fixes - Fix XEL `cpu_time` unit conversion in `XelFileReader.cs`: divide by 1000 (microseconds → milliseconds) to match ReadTrace.exe convention - Wrap all `BulkLoadRowset` usages in `BulkWriter.cs` with `try/finally` blocks to ensure `Close()` is called on exception - Add per-table row count logging in `TraceEventImporterPlugin.cs` after import for diagnostics - Close dropdown menus via `cmOptions.Close()` before showing importer conflict MessageBox in `fmImport.cs` to prevent dialog from being hidden behind menus - Pass `this` as owner to `MessageBox.Show()` for proper modal centering - Remove `EnumFiles()` call from `tsiBool_Click` to prevent `NullReferenceException` when `instances` is not yet initialized
…eadTraceManaged-pijocoder-042726
Contributor
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
…dd volatile to _cancelled field - BulkLoadRowset.cs: Wrap SqlDataAdapter in using statement for proper disposal - BulkWriter.cs: Remove redundant (object)x ?? DBNull.Value checks for non-nullable value types (int, byte, long) in WriteBatches, WriteStatements, WriteInterestingEvents, WriteProcedureNames, WriteUniqueBatches - TraceEventImporterPlugin.cs: Mark _cancelled field as volatile to ensure cross-thread visibility and fix always-false condition in inner cancellation check
EventProcessor - align sequence and connection handling with ReadTrace.exe: - BatchSeq/StmtSeq now use Starting event's global sequence (no separate counters) - Statement→Batch linking uses in-flight _curBatchStartSeq session state - ConnSeq uses global sequence namespace for both real and fake connections - Add "CONNECTED BEFORE TRACE" placeholder rows for sessions with no login event - Only create fake connection rows from batch completion (not statements) - Retain _sessionConnSeq on logout to prevent duplicate fake connection rows
- Add SpExecuteSqlExtractor to extract inner SQL query from sp_executesql, sp_prepexec, sp_prepare, and sp_cursor* RPC calls for normalization/hashing (aligns with ReadTrace.exe GetRPCParamText behavior) - SqlTextNormalizer: only normalize auto-generated RPC params (@p0, @p1, ...) to @p#; preserve named parameters (@JOB_ID, @ALL, @IS_SYSTEM) as-is (aligns with ReadTrace.exe normalization)
…e to file format header. Not a common use case
Comment on lines
+275
to
+281
| catch (Exception ex) | ||
| { | ||
| LogMessage($"TraceEventImporter: Error - {ex.Message}"); | ||
| LogMessage(ex.ToString()); | ||
| State = ImportState.Idle; | ||
| return false; | ||
| } |
Comment on lines
+338
to
+349
| foreach (string batch in batches) | ||
| { | ||
| string trimmed = batch.Trim(); | ||
| if (trimmed.Length == 0) continue; | ||
|
|
||
| using (var cmd = new SqlCommand(trimmed, conn)) | ||
| { | ||
| cmd.CommandTimeout = 0; | ||
| cmd.ExecuteNonQuery(); | ||
| executedCount++; | ||
| } | ||
| } |
…rt returning no data tblUniqueBatches.Seq and tblUniqueStatements.Seq must store the global event sequence of the first occurrence of each unique batch/statement, matching the value in tblBatches.BatchSeq / tblStatements.StmtSeq. Previously, UniqueStore used auto-incremented local counters (++_uniqueBatchSeq / ++_uniqueStmtSeq) which produced small sequential values (1, 2, 3...) that never matched the large XEL event sequence numbers stored as BatchSeq/StmtSeq in the detail tables. This caused spReporter_ExampleBatchDetails (and related report procs) to return zero rows because the join `tblUniqueBatches.Seq = tblBatches.BatchSeq` could never be satisfied. Changes: - UniqueStore.TryAddBatch: add batchSeq parameter; use it as Seq - UniqueStore.TryAddStatement: add stmtSeq parameter; use it as Seq - EventProcessor.HandleBatchCompleted: pass batchSeq to TryAddBatch - EventProcessor.HandleStatementCompleted: declare stmtSeq before use, pass it to TryAddStatement - Remove unused _uniqueBatchSeq and _uniqueStmtSeq counters from UniqueStore
Added IsCurrencySymbol() helper covering the Unicode Currency Symbols
block (\u20A0-\u20CF) plus the Latin-1 extras $, ¢, £, ¤, ¥ — all
expressed as \uXXXX escapes to avoid encoding fragility.
In the main normalization loop, a currency symbol immediately followed
by a digit is now skipped before the numeric literal handler runs, so
values like $380, €50, or ¥100 are normalized to {##} the same as a
bare number. The guard on the numeric literal section is also relaxed
to allow digits preceded by a currency symbol, covering the case where
$ is otherwise treated as an identifier character.
String/character data is unaffected: the currency handler requires
char.IsDigit on the next character, and string literals are consumed
and continued well before this code is reached.
…eadTraceManaged-pijocoder-042726
…den DLL discovery diagnostics - Add TraceEventImporter ProjectReference to sqlnexus.csproj so MSBuild copies TraceEventImporter.dll (and its dependencies) to the output directory; without this the Import menu entry was never created - Fix broken pre-existing TraceEventImporter fragment in sqlnexus.csproj (placeholder GUID, missing </ProjectReference> closing tag) that caused an MSBuild XML parse error; preserve the valid ErrorLogImporter reference that was nested inside it fmImport.cs — harden EnumImportersFromDirectory diagnostics: - Wrap GetExportedTypes() in its own try/catch; ReflectionTypeLoadException now logs each individual LoaderException (exact missing dependency name) instead of silently terminating the scan for remaining DLLs - Wrap CreateInstance/cast in try/catch so an InvalidCastException from a mismatched NexusInterfaces version is logged per-type rather than crashing - Distinguish "expected" DLLs (those sqlnexus.exe directly references at compile time via GetReferencedAssemblies()) from unrelated support libs; load failures for expected DLLs escalate to MessageOptions.Both (log + status bar) instead of Silent - Add per-DLL "loaded but registered no importers" warning for expected DLLs (catches interface version mismatches, wrong SNK key, etc.) - Add post-scan loop over referenced assemblies containing "Import" to log any expected importer DLL absent from the scanned directory (Silent) - Replace hardcoded _expectedImporterDlls string[] with a Lazy<HashSet<string>> built from Assembly.GetReferencedAssemblies() so the list stays in sync automatically when new ProjectReferences are added - Add final post-scan check in EnumImporters() that warns at Both level if TraceEventImporter or ReadTraceNexusImporter never registered, naming the exact DLL and startup path to check
| // List of option names | ||
| // Track which expected DLL names were actually seen in this directory so that | ||
| // EnumImporters() can warn about any that are present on disk but failed to register. | ||
| var seenExpected = new HashSet<string>(StringComparer.OrdinalIgnoreCase); |
Comment on lines
+645
to
+652
| catch (Exception ex) | ||
| { | ||
| MainForm.LogMessage(string.Format( | ||
| "Failed to instantiate or cast '{0}' from '{1}': {2}", | ||
| typ.FullName, Path.GetFileName(file), ex.Message), | ||
| isExpected ? MessageOptions.Both : MessageOptions.Silent); | ||
| continue; | ||
| } |
Comment on lines
+585
to
+591
| catch (Exception ex) | ||
| { | ||
| string msg = string.Format("Assembly '{0}' could not be loaded: {1}", Path.GetFileName(file), ex.Message); | ||
| // Use a visible log level when a known importer DLL fails to load | ||
| MainForm.LogMessage(msg, isExpected ? MessageOptions.Both : MessageOptions.Silent); | ||
| continue; | ||
| } |
…sing
SpExecuteSqlExtractor.cs
- Combined nested `if` into single condition for N'...' unicode literal
detection; moved comment above the block
SqlTextNormalizer.cs
- Binary literal (0xHEX): folded inner identifier-boundary guard into the
outer `if` condition; added clarifying comment
- IsHexDigit: split three-range return onto separate lines to resolve
CodeQL "complex condition: too many logical operations" warning
- IsShowplanVariable: replaced `string prefix = ""` + `+=` concatenation
with `StringBuilder` per CodeQL recommendation; eliminated intermediate
`prefix` variable by calling `.Length` and `.ToString()` directly on the
builder
XelFileReader.cs
- Replaced all four empty `catch { }` blocks in the Field/Action helpers
with `catch (Exception ex)` + `Debug.WriteLine` per CodeQL
"empty catch block" rule; `Debug.WriteLine` is used (vs `Trace`) so
messages are stripped from Release builds and always visible in the VS
Output window when debugging without requiring a configured TraceListener
Aggregator.cs
- `FindTimeInterval`: moved early-return condition to its own line for
consistency with the rest of the file's brace style
Comment on lines
+273
to
+275
| return (c >= '0' && c <= '9') | ||
| || (c >= 'a' && c <= 'f') | ||
| || (c >= 'A' && c <= 'F'); |
Comment on lines
+226
to
+229
| catch (Exception ex) | ||
| { | ||
| System.Diagnostics.Debug.WriteLine($"[XelFileReader] GetFieldString('{fieldName}'): {ex.Message}"); | ||
| } |
Comment on lines
+240
to
+243
| catch (Exception ex) | ||
| { | ||
| System.Diagnostics.Debug.WriteLine($"[XelFileReader] GetFieldInt64Nullable('{fieldName}'): {ex.Message}"); | ||
| } |
Comment on lines
+254
to
+257
| catch (Exception ex) | ||
| { | ||
| System.Diagnostics.Debug.WriteLine($"[XelFileReader] GetActionString('{actionName}'): {ex.Message}"); | ||
| } |
Comment on lines
+268
to
+271
| catch (Exception ex) | ||
| { | ||
| System.Diagnostics.Debug.WriteLine($"[XelFileReader] GetActionInt64('{actionName}'): {ex.Message}"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Introduction
Trace Event Importer is a fully managed SqlNexus importer plugin that reads SQL Server trace files (.trc and .xel) directly in C#, eliminating the dependency on the legacy native ReadTrace.exe executable. It normalizes SQL text, computes hash-based batch/statement groupings, and bulk-loads processed data into the existing ReadTrace database schema used by SqlNexus for workload analysis. It creates identical database schema and objects as those of ReadTrace to avoid having to recreate reports and queries.
Key Design Decisions
Same schema, same reports: The importer writes to the identical
ReadTrace.*tables that ReadTrace.exe creates. All existing RDLC reports work without modification.Embedded SQL scripts:
CreateSchema.sqlandPostLoadFixups.sqlare embedded resources in the DLL, eliminating external file dependencies.INexusImporter (not INexusFileImporter): The plugin handles its own file enumeration internally (same pattern as ReadTrace), receiving a file mask from the host and processing all matching files in a single
DoImport()call.XELite for XEL reading: Uses the
Microsoft.SqlServer.XEvent.XELiteNuGet package for reading Extended Events files, which handles the binary XEL format reliably.BulkLoadRowset for writing: Uses the existing
BulkLoadExlibrary (shared with other importers) for efficient bulk insert.Implementation Plan
Phase 1: Core Pipeline
TraceEvent.cs): Define a unified event structure that can represent both TRC and XEL events with fields for session, request, timestamps, duration, CPU, reads, writes, text data, etc.TrcFileReader.cs): Read SQL Trace binary format, map trace event IDs toTraceEventTypeenum, extract fields by column ID.XelFileReader.cs): Read Extended Events files via XELite, map XE event names (sql_batch_completed,rpc_completed, etc.) to the sameTraceEventTypeenum.EventProcessor.cs): Correlate Starting↔Completed events per session+request, track connections (login→logout), handle attention events.Phase 2: Normalization & Hashing
SqlTextNormalizer.cs): Replace string literals with{STR}, numeric literals with{##}, normalize whitespace, handle quoted identifiers.HashComputer.cs): Compute stable 64-bit hash IDs for normalized SQL text (must produce consistent hashes for grouping).SpecialProcDetector.cs): Identifysp_executesql,sp_prepare,sp_prepexec,sp_cursoropen, etc. and extract the inner SQL text for normalization.UniqueStore.cs): Deduplicate batches, statements, app names, login names, procedure names; assign sequential IDs.Phase 3: Aggregation & Database Writing
Aggregator.cs): Partition events into configurable time intervals (default 60 seconds), compute per-HashID partial aggregates (min/max/total for duration, reads, writes, CPU; counts for starting/completed/attention events).BulkWriter.cs): Write all ReadTrace tables viaBulkLoadRowset— metadata, reference data, unique text, fact tables, and aggregation tables.CreateSchema.sql): CREATE SCHEMA, all tables matching the ReadTrace.exe layout, and thevwBatchPartialAggsByGroupTimeIntervalview.PostLoadFixups.sql): Create primary keys, nonclustered indexes (using ReadTrace.exe naming convention for index hint compatibility), and reconcileConnSeq/BatchSeq/ParentStmtSeqforeign key relationships.Phase 4: Host Integration
ProjectReferenceinsqlnexus.csproj, register inAppConfig.xml.ReadTraceschema, only one can be active at a time.MessageBoxinforming the user when toggling between importers at runtimeSaveImportOptions*pssdiag*.xeland*LogScout*.xelmasks (not*.xel) to avoid picking up system health, AlwaysOn, or SQLDiag XEL files that belong to theCustomXELImporter.ReadTracePostProcessing.sqlas a post-script, and update the host'sRunPostScriptsto allow it for both ReadTrace and TraceEventImporter."Aggregation interval (seconds)"gracefully in the UI (skip from checkbox menu).false) when no saved options exist yet.Phase 5: Schema Compatibility
The following objects must match what
ReadTracePostProcessing.sqland the RDLC reports expect:ReadTrace.vwBatchPartialAggsByGroupTimeInterval— must return exactly 10 columns (StartTime,EndTime,TimeInterval,StartingEvents,CompletedEvents,Attentions,Duration,Reads,Writes,CPU) aggregated across all HashIDs per time interval.tblBatches_HashIDnotIX_tblBatches_HashID) becauseReadTracePostProcessing.sqlusesWITH (INDEX(...))hints that reference these exact names.ReadTracePostProcessing.sqlstored procedures and the RDLC report datasets expect.Phase 6: BulkLoadRowset Fix
BulkLoadRowset.GetNewRow()wraps the table name in[brackets], which breaks for schema-qualified names likeReadTrace.tblBatches(produces[ReadTrace.tblBatches]instead of[ReadTrace].[tblBatches]). Fix to split on.and bracket each part separately.Configuration
truetrue60Testing Plan
1. Build & Deploy Verification
TraceEventImporter.dllis present insqlnexus\bin\Release\CreateSchema.sql,PostLoadFixups.sql)2. Importer Discovery & UI
Enabledchecked"Aggregation interval (seconds)"option does NOT appear as a checkbox (non-boolean options are hidden)3. Basic Import (XEL files)
*LogScout*.xelfilesReadTraceschema and all tables are createdIX_prefix)ReadTrace.vwBatchPartialAggsByGroupTimeIntervalview existstblBatches,tblStatements,tblConnections,tblUniqueBatches, etc.4. Basic Import (TRC files)
*sp_trace*.trcfiles5. Report Validation
6. Comparative Testing (TraceEventImporter vs ReadTrace)
This is the critical validation — import the same trace files with both importers and compare results.
Setup
.xeland.trcif possible)sqlnexus_managedandsqlnexus_readtrace)sqlnexus_managedsqlnexus_readtraceComparison Queries
More test queries:
Expected Differences
Some minor differences are acceptable:
NormTextinstead{STR}vsN'{STR}')Red Flags (investigate if seen)
7. File Exclusion
log_1.trc,log_2.trc, and*_blk.trcfiles alongside valid trace files*pssdiag*and*LogScout*patterns)8. Re-import & Drop Tables
9. Disabled Importer
10. Cancellation