From bc48d3d691e537cc68f116905371bf56ce469c34 Mon Sep 17 00:00:00 2001 From: JosephPilov-MSFT <23519517+PiJoCoder@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:42:55 -0500 Subject: [PATCH 1/6] #510 Initial implementation of ERRORLOG importer --- ErrorLogImporter/ErrorLogImporter.cs | 367 ++++++++++++++++++++ ErrorLogImporter/ErrorLogImporter.csproj | 202 +++++++++++ ErrorLogImporter/Properties/AssemblyInfo.cs | 15 + ErrorLogImporter/SqlNexus.snk | Bin 0 -> 596 bytes ErrorLogImporter/packages.config | 43 +++ sqlnexus.sln | 52 +++ sqlnexus/AppConfig.xml | 2 + sqlnexus/Utilities.cs | 4 + sqlnexus/fmImport.cs | 6 +- sqlnexus/sqlnexus.csproj | 6 +- 10 files changed, 693 insertions(+), 4 deletions(-) create mode 100644 ErrorLogImporter/ErrorLogImporter.cs create mode 100644 ErrorLogImporter/ErrorLogImporter.csproj create mode 100644 ErrorLogImporter/Properties/AssemblyInfo.cs create mode 100644 ErrorLogImporter/SqlNexus.snk create mode 100644 ErrorLogImporter/packages.config diff --git a/ErrorLogImporter/ErrorLogImporter.cs b/ErrorLogImporter/ErrorLogImporter.cs new file mode 100644 index 00000000..570490f6 --- /dev/null +++ b/ErrorLogImporter/ErrorLogImporter.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using Microsoft.Data.SqlClient; +using NexusInterfaces; +using BulkLoadEx; + +namespace ErrorLogImporter +{ + public class ErrorLogImporter : INexusFileImporter + { + private const string TABLE_NAME = "tbl_ERRORLOG"; + private const string OPTION_DROP_EXISTING = "Drop existing tables (ERRORLOG)"; + private const string OPTION_ENABLED = "Enabled"; + + // Regex to match ERRORLOG lines: datetime, process, message + // Example: "2026-04-14 22:37:11.55 Server Microsoft SQL Server 2022..." + private static readonly Regex LogLineRegex = new Regex( + @"^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{2})\s+(\S+)\s+(.*)", + RegexOptions.Compiled); + + private string fileMask = ""; + private string connStr = ""; + private string server = ""; + private string database = ""; + private bool useWindowsAuth = true; + private string sqlLogin = ""; + private string sqlPassword = ""; + private ILogger logger; + + private ImportState state = ImportState.Idle; + private bool cancelled = false; + private long totalRowsInserted = 0; + private long totalLinesProcessed = 0; + private long fileSize = 0; + private long currentPosition = 0; + private bool hasDroppedTable = false; + private ArrayList knownRowsets = new ArrayList(); + private Dictionary options = new Dictionary(); + + public ErrorLogImporter() + { + options.Add(OPTION_DROP_EXISTING, true); + options.Add(OPTION_ENABLED, true); + } + + private void LogMessage(string msg) + { + if (null == logger) + Trace.WriteLine(msg); + else + logger.LogMessage(msg); + } + + #region INexusImporter Members + + public Guid ID + { + get { return new Guid("D7E8F9A0-B1C2-4D3E-5F6A-7B8C9D0E1F2A"); } + } + + public void Initialize(string Filemask, string connString, string Server, bool UseWindowsAuth, string SQLLogin, string SQLPassword, string DatabaseName, ILogger Logger) + { + this.fileMask = Filemask; + this.connStr = connString; + this.server = Server; + this.useWindowsAuth = UseWindowsAuth; + this.sqlLogin = SQLLogin; + this.sqlPassword = SQLPassword; + this.database = DatabaseName; + this.logger = Logger; + this.state = ImportState.Idle; + this.cancelled = false; + this.totalRowsInserted = 0; + this.totalLinesProcessed = 0; + } + + public Dictionary Options + { + get { return options; } + } + + public Form OptionsDialog + { + get { return null; } + } + + public string[] SupportedMasks + { + get { return new string[] { "*_ERRORLOG*" }; } + } + + public string[] PreScripts + { + get { return new string[] { }; } + } + + public string[] PostScripts + { + get { return new string[] { }; } + } + + public ImportState State + { + get { return state; } + private set + { + state = value; + OnStatusChanged(EventArgs.Empty); + } + } + + public bool Cancelled + { + get { return cancelled; } + private set { cancelled = value; } + } + + public ArrayList KnownRowsets + { + get { return knownRowsets; } + } + + public long TotalRowsInserted + { + get { return totalRowsInserted; } + } + + public long TotalLinesProcessed + { + get { return totalLinesProcessed; } + } + + public string Name + { + get { return "ERRORLOG Importer"; } + } + + public bool DoImport() + { + try + { + string dir = Path.GetDirectoryName(fileMask); + string mask = Path.GetFileName(fileMask); + + if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) + { + LogMessage("ErrorLogImporter: Directory not found: " + dir); + return false; + } + + string[] files = Directory.GetFiles(dir, mask); + if (files.Length == 0) + { + State = ImportState.NoFiles; + LogMessage("ErrorLogImporter: No ERRORLOG files found matching " + fileMask); + return true; + } + + State = ImportState.OpeningDatabaseConnection; + + if ((bool)options[OPTION_DROP_EXISTING] && !hasDroppedTable) + { + DropExistingTable(); + hasDroppedTable = true; + } + + CreateTable(); + + State = ImportState.Importing; + + foreach (string file in files) + { + if (Cancelled) + break; + + LogMessage("ErrorLogImporter: Importing file " + file); + ImportFile(file); + } + + State = ImportState.Idle; + LogMessage("ErrorLogImporter: Import complete. Total rows inserted: " + totalRowsInserted); + return true; + } + catch (Exception ex) + { + LogMessage("ErrorLogImporter: Error - " + ex.ToString()); + State = ImportState.Idle; + return false; + } + } + + public void Cancel() + { + Cancelled = true; + State = ImportState.Canceling; + LogMessage("ErrorLogImporter: Received cancel request"); + } + + public event EventHandler StatusChanged; + + public void OnStatusChanged(EventArgs e) + { + StatusChanged?.Invoke(this, e); + } + + #endregion + + #region INexusProgressReporter Members + + public long CurrentPosition + { + get { return currentPosition; } + } + + public event EventHandler ProgressChanged; + + public void OnProgressChanged(EventArgs e) + { + ProgressChanged?.Invoke(this, e); + } + + #endregion + + #region INexusFileSizeReporter Members + + public long FileSize + { + get { return fileSize; } + } + + #endregion + + #region Private Methods + + private void DropExistingTable() + { + using (SqlConnection cn = new SqlConnection(connStr)) + { + cn.Open(); + using (SqlCommand cmd = new SqlCommand()) + { + cmd.Connection = cn; + cmd.CommandTimeout = 0; + cmd.CommandText = "IF OBJECT_ID ('" + TABLE_NAME + "', 'U') IS NOT NULL DROP TABLE [" + TABLE_NAME + "]"; + cmd.ExecuteNonQuery(); + } + } + } + + private void CreateTable() + { + using (SqlConnection cn = new SqlConnection(connStr)) + { + cn.Open(); + using (SqlCommand cmd = new SqlCommand()) + { + cmd.Connection = cn; + cmd.CommandTimeout = 0; + cmd.CommandText = @" + IF OBJECT_ID ('" + TABLE_NAME + @"', 'U') IS NULL + BEGIN + CREATE TABLE [" + TABLE_NAME + @"] ( + [LogDateTime] datetime NULL, + [Process] varchar(50) NULL, + [Message] varchar(max) NULL, + [FileName] varchar(256) NULL + ) + END"; + cmd.ExecuteNonQuery(); + } + } + } + + private void ImportFile(string filePath) + { + FileInfo fi = new FileInfo(filePath); + fileSize = fi.Length; + currentPosition = 0; + string shortFileName = Path.GetFileName(filePath); + + BulkLoadRowset bulkLoad = new BulkLoadRowset(TABLE_NAME, connStr); + + try + { + DateTime? pendingDateTime = null; + string pendingProcess = null; + string pendingMessage = null; + + using (StreamReader reader = new StreamReader(filePath)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + if (Cancelled) + break; + + totalLinesProcessed++; + currentPosition += System.Text.Encoding.UTF8.GetByteCount(line) + 2; // +2 for \r\n + OnProgressChanged(EventArgs.Empty); + + Match match = LogLineRegex.Match(line); + if (match.Success) + { + // Flush the previous pending entry + if (pendingDateTime.HasValue) + { + InsertRow(bulkLoad, pendingDateTime.Value, pendingProcess, pendingMessage, shortFileName); + } + + // Parse the new entry + string dateStr = match.Groups[1].Value; + pendingProcess = match.Groups[2].Value; + pendingMessage = match.Groups[3].Value; + + if (DateTime.TryParseExact(dateStr, "yyyy-MM-dd HH:mm:ss.ff", + CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsedDate)) + { + pendingDateTime = parsedDate; + } + else + { + pendingDateTime = null; + } + } + else + { + // Continuation line - append to current message + if (pendingMessage != null) + { + pendingMessage += Environment.NewLine + line; + } + } + } + + // Flush the last pending entry + if (pendingDateTime.HasValue) + { + InsertRow(bulkLoad, pendingDateTime.Value, pendingProcess, pendingMessage, shortFileName); + } + } + } + finally + { + bulkLoad.Close(); + } + } + + private void InsertRow(BulkLoadRowset bulkLoad, DateTime logDateTime, string process, string message, string fileName) + { + System.Data.DataRow row = bulkLoad.GetNewRow(); + row["LogDateTime"] = logDateTime; + row["Process"] = process != null && process.Length > 50 ? process.Substring(0, 50) : process; + row["Message"] = message ?? ""; + row["FileName"] = fileName != null && fileName.Length > 256 ? fileName.Substring(0, 256) : fileName; + bulkLoad.InsertRow(row); + totalRowsInserted++; + } + + #endregion + } +} diff --git a/ErrorLogImporter/ErrorLogImporter.csproj b/ErrorLogImporter/ErrorLogImporter.csproj new file mode 100644 index 00000000..8dbba1e1 --- /dev/null +++ b/ErrorLogImporter/ErrorLogImporter.csproj @@ -0,0 +1,202 @@ + + + + + Debug + AnyCPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D} + Library + Properties + ErrorLogImporter + ErrorLogImporter + v4.8 + 512 + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + + + SqlNexus.snk + + + + ..\packages\Azure.Core.1.44.1\lib\net472\Azure.Core.dll + + + ..\packages\Azure.Identity.1.12.1\lib\netstandard2.0\Azure.Identity.dll + + + ..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + True + + + ..\packages\Microsoft.Bcl.TimeProvider.8.0.1\lib\net462\Microsoft.Bcl.TimeProvider.dll + + + ..\packages\Microsoft.Data.SqlClient.5.2.2\lib\net462\Microsoft.Data.SqlClient.dll + + + ..\packages\Microsoft.Identity.Client.4.65.0\lib\net472\Microsoft.Identity.Client.dll + + + ..\packages\Microsoft.Identity.Client.Extensions.Msal.4.65.0\lib\netstandard2.0\Microsoft.Identity.Client.Extensions.Msal.dll + + + ..\packages\Microsoft.IdentityModel.Abstractions.8.1.2\lib\net472\Microsoft.IdentityModel.Abstractions.dll + + + ..\packages\Microsoft.IdentityModel.JsonWebTokens.8.1.2\lib\net472\Microsoft.IdentityModel.JsonWebTokens.dll + + + ..\packages\Microsoft.IdentityModel.Logging.8.1.2\lib\net472\Microsoft.IdentityModel.Logging.dll + + + ..\packages\Microsoft.IdentityModel.Protocols.8.1.2\lib\net472\Microsoft.IdentityModel.Protocols.dll + + + ..\packages\Microsoft.IdentityModel.Protocols.OpenIdConnect.8.1.2\lib\net472\Microsoft.IdentityModel.Protocols.OpenIdConnect.dll + + + ..\packages\Microsoft.IdentityModel.Tokens.8.1.2\lib\net472\Microsoft.IdentityModel.Tokens.dll + + + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + ..\packages\System.ClientModel.1.2.1\lib\netstandard2.0\System.ClientModel.dll + + + ..\packages\System.Configuration.ConfigurationManager.8.0.1\lib\net462\System.Configuration.ConfigurationManager.dll + + + + ..\packages\System.Diagnostics.DiagnosticSource.8.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\System.IdentityModel.Tokens.Jwt.8.1.2\lib\net472\System.IdentityModel.Tokens.Jwt.dll + + + ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll + True + + + ..\packages\System.IO.FileSystem.AccessControl.5.0.0\lib\net461\System.IO.FileSystem.AccessControl.dll + + + ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + ..\packages\System.Memory.Data.8.0.1\lib\net462\System.Memory.Data.dll + + + ..\packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll + True + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll + True + + + ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True + + + ..\packages\System.Security.AccessControl.6.0.1\lib\net461\System.Security.AccessControl.dll + + + ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll + True + + + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + True + + + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + True + + + ..\packages\System.Security.Cryptography.ProtectedData.8.0.0\lib\net462\System.Security.Cryptography.ProtectedData.dll + + + ..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll + True + + + ..\packages\System.Security.Permissions.8.0.0\lib\net462\System.Security.Permissions.dll + + + ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll + + + ..\packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll + True + + + ..\packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + + + + + + + + + + {f9069c48-39a6-4fb9-b4b8-d8d0034709a3} + BulkLoadEx + + + {2217e9ca-442e-46c5-ab6c-7a46ae41a22c} + NexusInterfaces + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + diff --git a/ErrorLogImporter/Properties/AssemblyInfo.cs b/ErrorLogImporter/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..98374987 --- /dev/null +++ b/ErrorLogImporter/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("ErrorLogImporter")] +[assembly: AssemblyDescription("SQL Server ERRORLOG Importer for SQL Nexus")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("ErrorLogImporter")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2026")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ErrorLogImporter/SqlNexus.snk b/ErrorLogImporter/SqlNexus.snk new file mode 100644 index 0000000000000000000000000000000000000000..4760abbc51c6a568589aef9dc014ee27c6f38163 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096s;x?OfQwj&Ckq)FAmRn7#SG3KN@(n&G zt49pm_r{EzF-<@ONCzFqF)n4}C>scy`pNt&C4fl82G}2f!I6JJOb6xx~TJwsBhbG zq$sBpk~k8U3fL-6Up$8Rlg5>yUkxJ*F>mmn9AZ$Z)RH}pWJi5*f5`OfXz|u=tnJ}@ zQ@(#`Vu*IdLOt&VY|N-@7o;mlCEfL<&3PJx73y;G&oAH85Y!U>L%d}BY-6&ybbT#R zwT47py!{s_z1-eGnE(#EI1HrLI?w9Sp{&dF2`ew_@5Z=(agvXUeN@FlTu>vQcs6uk z2$yQT4)v@!h?6cg75Q0Ilv%W`bE+1i<)elAn{}eoaNR*YO0xz+bc`3}4tv!!4iX&W zb|MkovRXNjkcEf>i&@E`iJV?)S_KP8cPR0xb2_%*l3fw5qaDUI$FA>I4w2}`F==d2 z)x3%$czY5hK4Q@W>Zqdrp@ZmRg6kHftfI`;+Z+Uz9||BEMieED;Y1Prl0FdK8ig-q z_L_9~HYsco*>~jV+Fhw|zxNjs@NBQzU6@M%G>2js`u7(^*Bd;Ai0t^n;AG1V(rVxJS}RhF-}Ed i685zQvm3us$JK!;I3Cl(Z)#yz@>Os>27e8pb*L01TNk?k literal 0 HcmV?d00001 diff --git a/ErrorLogImporter/packages.config b/ErrorLogImporter/packages.config new file mode 100644 index 00000000..2583c8d1 --- /dev/null +++ b/ErrorLogImporter/packages.config @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sqlnexus.sln b/sqlnexus.sln index 504c5120..45f9171d 100644 --- a/sqlnexus.sln +++ b/sqlnexus.sln @@ -28,6 +28,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Setup-Related\SetupSQLNexusPrereq.ps1 = Setup-Related\SetupSQLNexusPrereq.ps1 EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErrorLogImporter", "ErrorLogImporter\ErrorLogImporter.csproj", "{B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -395,6 +397,56 @@ Global {CCF72846-1645-4548-8E66-A2FFCE83692D}.Release|x64.Build.0 = Release|Any CPU {CCF72846-1645-4548-8E66-A2FFCE83692D}.Release|x86.ActiveCfg = Release|Any CPU {CCF72846-1645-4548-8E66-A2FFCE83692D}.Release|x86.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|Win32.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|Win32.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|x64.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|x64.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|x86.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug|x86.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|Any CPU.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|Any CPU.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|Mixed Platforms.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|Win32.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|Win32.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|x64.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|x64.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|x86.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Debug64|x86.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|Any CPU.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|Any CPU.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|Mixed Platforms.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|Win32.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|Win32.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|x64.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|x64.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|x86.ActiveCfg = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.DebugLocal|x86.Build.0 = Debug|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|Any CPU.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|Any CPU.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|Mixed Platforms.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|Mixed Platforms.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|Win32.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|Win32.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|x64.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|x64.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|x86.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Production|x86.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|Any CPU.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|Win32.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|Win32.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|x64.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|x64.Build.0 = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|x86.ActiveCfg = Release|Any CPU + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/sqlnexus/AppConfig.xml b/sqlnexus/AppConfig.xml index 27eb501d..036b6499 100644 --- a/sqlnexus/AppConfig.xml +++ b/sqlnexus/AppConfig.xml @@ -21,6 +21,8 @@ + + diff --git a/sqlnexus/Utilities.cs b/sqlnexus/Utilities.cs index 497aa58a..9e68606c 100644 --- a/sqlnexus/Utilities.cs +++ b/sqlnexus/Utilities.cs @@ -35,6 +35,10 @@ public static bool IsEnabled(String option) { return m_options.ContainsKey(option) && m_options[option]; } + public static bool HasOption(String option) + { + return m_options.ContainsKey(option); + } public static void Set(String option, bool Enable ) { if (m_options.ContainsKey(option)) diff --git a/sqlnexus/fmImport.cs b/sqlnexus/fmImport.cs index cb1f47ea..392c3c92 100644 --- a/sqlnexus/fmImport.cs +++ b/sqlnexus/fmImport.cs @@ -597,11 +597,11 @@ private void EnumImportersFromDirectory(string importerDirectory) subtsi.Tag = prod; subtsi.CheckOnClick = true; - bool UserSaved = ImportOptions.IsEnabled(String.Format("{0}.{1}", prod.Name, subtsi.Text)); + string savedKey = String.Format("{0}.{1}", prod.Name, subtsi.Text); MainForm.LogMessage("load: " + String.Format("{0}->{1}", prod.Name, option), MessageOptions.Silent); - if (ImportOptions.IsEnabled("SaveImportOptions")) - subtsi.Checked = UserSaved; + if (ImportOptions.IsEnabled("SaveImportOptions") && ImportOptions.HasOption(savedKey)) + subtsi.Checked = ImportOptions.IsEnabled(savedKey); else subtsi.Checked = (bool)prod.Options[option]; diff --git a/sqlnexus/sqlnexus.csproj b/sqlnexus/sqlnexus.csproj index 687ce72f..72b4479b 100644 --- a/sqlnexus/sqlnexus.csproj +++ b/sqlnexus/sqlnexus.csproj @@ -886,6 +886,10 @@ {8DB02D0C-68F3-4762-9BC9-AA3DF2B91F4B} RowsetImportEngine + + {B3A4C5D6-E7F8-4A9B-0C1D-2E3F4A5B6C7D} + ErrorLogImporter + @@ -1673,4 +1677,4 @@ - \ No newline at end of file + From 4663074d7e6aaf4bcb0a965d31f7f0c3a861c613 Mon Sep 17 00:00:00 2001 From: JosephPilov-MSFT <23519517+PiJoCoder@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:18:10 -0500 Subject: [PATCH 2/6] #510 ErrorLogImporter refinements: remove unused credential fields, add RowNum identity column with composite index, and remove obsolete tblErrorlog rowset definition - Remove unused sqlLogin, sqlPassword, server, database, useWindowsAuth fields from ErrorLogImporter to avoid storing credentials unnecessarily in memory - Add RowNum bigint IDENTITY(1,1) column to tbl_ERRORLOG for deterministic row ordering - Add composite nonclustered index IX_tbl_ERRORLOG_LogDateTime_RowNum on (LogDateTime, RowNum) for efficient time-range queries - Remove obsolete tblErrorlog rowset definition from TextRowsets.xml (both copies) since ERRORLOG import is now handled by the dedicated ErrorLogImporter plugin --- ErrorLogImporter/ErrorLogImporter.cs | 12 ++---------- RowsetImportEngine/TextRowsets.xml | 10 ---------- sqlnexus/TextRowsets.xml | 10 ---------- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/ErrorLogImporter/ErrorLogImporter.cs b/ErrorLogImporter/ErrorLogImporter.cs index 570490f6..fcaad228 100644 --- a/ErrorLogImporter/ErrorLogImporter.cs +++ b/ErrorLogImporter/ErrorLogImporter.cs @@ -26,11 +26,6 @@ public class ErrorLogImporter : INexusFileImporter private string fileMask = ""; private string connStr = ""; - private string server = ""; - private string database = ""; - private bool useWindowsAuth = true; - private string sqlLogin = ""; - private string sqlPassword = ""; private ILogger logger; private ImportState state = ImportState.Idle; @@ -68,11 +63,6 @@ public void Initialize(string Filemask, string connString, string Server, bool U { this.fileMask = Filemask; this.connStr = connString; - this.server = Server; - this.useWindowsAuth = UseWindowsAuth; - this.sqlLogin = SQLLogin; - this.sqlPassword = SQLPassword; - this.database = DatabaseName; this.logger = Logger; this.state = ImportState.Idle; this.cancelled = false; @@ -266,11 +256,13 @@ private void CreateTable() IF OBJECT_ID ('" + TABLE_NAME + @"', 'U') IS NULL BEGIN CREATE TABLE [" + TABLE_NAME + @"] ( + [RowNum] bigint IDENTITY(1,1) NOT NULL, [LogDateTime] datetime NULL, [Process] varchar(50) NULL, [Message] varchar(max) NULL, [FileName] varchar(256) NULL ) + CREATE NONCLUSTERED INDEX [IX_" + TABLE_NAME + @"_LogDateTime_RowNum] ON [" + TABLE_NAME + @"] ([LogDateTime], [RowNum]) END"; cmd.ExecuteNonQuery(); } diff --git a/RowsetImportEngine/TextRowsets.xml b/RowsetImportEngine/TextRowsets.xml index bb5e8645..d5594af0 100644 --- a/RowsetImportEngine/TextRowsets.xml +++ b/RowsetImportEngine/TextRowsets.xml @@ -724,16 +724,6 @@ - - - - - - - - - - diff --git a/sqlnexus/TextRowsets.xml b/sqlnexus/TextRowsets.xml index bb5e8645..d5594af0 100644 --- a/sqlnexus/TextRowsets.xml +++ b/sqlnexus/TextRowsets.xml @@ -724,16 +724,6 @@ - - - - - - - - - - From 02969ff04928e6a9640d68a40f214a045ce88872 Mon Sep 17 00:00:00 2001 From: JosephPilov-MSFT <23519517+PiJoCoder@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:37:53 -0500 Subject: [PATCH 3/6] Finding 'CodeQL / Missed 'readonly' opportunity' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- ErrorLogImporter/ErrorLogImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ErrorLogImporter/ErrorLogImporter.cs b/ErrorLogImporter/ErrorLogImporter.cs index fcaad228..f5ba7889 100644 --- a/ErrorLogImporter/ErrorLogImporter.cs +++ b/ErrorLogImporter/ErrorLogImporter.cs @@ -35,7 +35,7 @@ public class ErrorLogImporter : INexusFileImporter private long fileSize = 0; private long currentPosition = 0; private bool hasDroppedTable = false; - private ArrayList knownRowsets = new ArrayList(); + private readonly ArrayList knownRowsets = new ArrayList(); private Dictionary options = new Dictionary(); public ErrorLogImporter() From cec637be33ddc6c7abb12a02a408f98cb7a71568 Mon Sep 17 00:00:00 2001 From: JosephPilov-MSFT <23519517+PiJoCoder@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:39:53 -0500 Subject: [PATCH 4/6] 'CodeQL / Redundant ToString() call' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- ErrorLogImporter/ErrorLogImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ErrorLogImporter/ErrorLogImporter.cs b/ErrorLogImporter/ErrorLogImporter.cs index f5ba7889..e29236f6 100644 --- a/ErrorLogImporter/ErrorLogImporter.cs +++ b/ErrorLogImporter/ErrorLogImporter.cs @@ -179,7 +179,7 @@ public bool DoImport() } catch (Exception ex) { - LogMessage("ErrorLogImporter: Error - " + ex.ToString()); + LogMessage("ErrorLogImporter: Error - " + ex); State = ImportState.Idle; return false; } From dd2448f6b18f3d32d08c53406e9c747669c1f350 Mon Sep 17 00:00:00 2001 From: JosephPilov-MSFT <23519517+PiJoCoder@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:04:10 -0500 Subject: [PATCH 5/6] #510 CodeQL fixes: use StringBuilder for loop concatenation, add readonly to collection fields, remove dead variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace string concatenation in loop (`pendingMessage += ...`) with `StringBuilder` to avoid O(n²) allocations on multi-line ERRORLOG entries - Mark `knownRowsets` and `options` fields as `readonly` since they are never reassigned - Remove unused `pendingMessage` variable, pass regex group value directly to `StringBuilder` constructor - Condense `pendingDateTime` assignment to ternary expression --- ErrorLogImporter/ErrorLogImporter.cs | 34 ++++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ErrorLogImporter/ErrorLogImporter.cs b/ErrorLogImporter/ErrorLogImporter.cs index fcaad228..1c161235 100644 --- a/ErrorLogImporter/ErrorLogImporter.cs +++ b/ErrorLogImporter/ErrorLogImporter.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Text; using System.Text.RegularExpressions; using System.Windows.Forms; using Microsoft.Data.SqlClient; @@ -35,8 +36,8 @@ public class ErrorLogImporter : INexusFileImporter private long fileSize = 0; private long currentPosition = 0; private bool hasDroppedTable = false; - private ArrayList knownRowsets = new ArrayList(); - private Dictionary options = new Dictionary(); + private readonly ArrayList knownRowsets = new ArrayList(); + private readonly Dictionary options = new Dictionary(); public ErrorLogImporter() { @@ -282,7 +283,7 @@ private void ImportFile(string filePath) { DateTime? pendingDateTime = null; string pendingProcess = null; - string pendingMessage = null; + StringBuilder pendingMessageBuilder = null; using (StreamReader reader = new StreamReader(filePath)) { @@ -302,30 +303,29 @@ private void ImportFile(string filePath) // Flush the previous pending entry if (pendingDateTime.HasValue) { - InsertRow(bulkLoad, pendingDateTime.Value, pendingProcess, pendingMessage, shortFileName); + InsertRow(bulkLoad, pendingDateTime.Value, pendingProcess, pendingMessageBuilder?.ToString(), shortFileName); } + // Parse the new entry string dateStr = match.Groups[1].Value; pendingProcess = match.Groups[2].Value; - pendingMessage = match.Groups[3].Value; + pendingMessageBuilder = new StringBuilder(match.Groups[3].Value); - if (DateTime.TryParseExact(dateStr, "yyyy-MM-dd HH:mm:ss.ff", - CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsedDate)) - { - pendingDateTime = parsedDate; - } - else - { - pendingDateTime = null; - } + + pendingDateTime = DateTime.TryParseExact(dateStr, "yyyy-MM-dd HH:mm:ss.ff", + CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsedDate) + ? parsedDate + : (DateTime?)null; + } else { // Continuation line - append to current message - if (pendingMessage != null) + if (pendingMessageBuilder != null) { - pendingMessage += Environment.NewLine + line; + pendingMessageBuilder.Append(Environment.NewLine); + pendingMessageBuilder.Append(line); } } } @@ -333,7 +333,7 @@ private void ImportFile(string filePath) // Flush the last pending entry if (pendingDateTime.HasValue) { - InsertRow(bulkLoad, pendingDateTime.Value, pendingProcess, pendingMessage, shortFileName); + InsertRow(bulkLoad, pendingDateTime.Value, pendingProcess, pendingMessageBuilder?.ToString(), shortFileName); } } } From 6feaa30fec5df0019c0d8f6275b666ed0c7e3d4d Mon Sep 17 00:00:00 2001 From: JosephPilov-MSFT <23519517+PiJoCoder@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:33:57 -0500 Subject: [PATCH 6/6] #510 Add ErrorNumber and State columns to tbl_ERRORLOG for structured error extraction - Add nullable `ErrorNumber` (int) and `State` (int) columns to `tbl_ERRORLOG` - Add compiled regex to extract error number and state from messages matching `Error: NNNNN, Severity: NN, State: NN.` - Parse error/state client-side at insert time; columns remain NULL for non-error log entries --- ErrorLogImporter/ErrorLogImporter.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ErrorLogImporter/ErrorLogImporter.cs b/ErrorLogImporter/ErrorLogImporter.cs index 0b432c83..69aafa24 100644 --- a/ErrorLogImporter/ErrorLogImporter.cs +++ b/ErrorLogImporter/ErrorLogImporter.cs @@ -25,6 +25,11 @@ public class ErrorLogImporter : INexusFileImporter @"^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{2})\s+(\S+)\s+(.*)", RegexOptions.Compiled); + // Regex to extract Error number and State from messages like "Error: 18456, Severity: 14, State: 38." + private static readonly Regex ErrorStateRegex = new Regex( + @"Error:\s*(\d+),\s*Severity:\s*\d+,\s*State:\s*(\d+)", + RegexOptions.Compiled); + private string fileMask = ""; private string connStr = ""; private ILogger logger; @@ -262,6 +267,8 @@ [RowNum] bigint IDENTITY(1,1) NOT NULL, [LogDateTime] datetime NULL, [Process] varchar(50) NULL, [Message] varchar(max) NULL, + [ErrorNumber] int NULL, + [State] int NULL, [FileName] varchar(256) NULL ) CREATE NONCLUSTERED INDEX [IX_" + TABLE_NAME + @"_LogDateTime_RowNum] ON [" + TABLE_NAME + @"] ([LogDateTime], [RowNum]) @@ -350,6 +357,18 @@ private void InsertRow(BulkLoadRowset bulkLoad, DateTime logDateTime, string pro row["LogDateTime"] = logDateTime; row["Process"] = process != null && process.Length > 50 ? process.Substring(0, 50) : process; row["Message"] = message ?? ""; + + // Extract Error number and State from message if present + if (message != null) + { + Match errorMatch = ErrorStateRegex.Match(message); + if (errorMatch.Success) + { + row["ErrorNumber"] = int.Parse(errorMatch.Groups[1].Value); + row["State"] = int.Parse(errorMatch.Groups[2].Value); + } + } + row["FileName"] = fileName != null && fileName.Length > 256 ? fileName.Substring(0, 256) : fileName; bulkLoad.InsertRow(row); totalRowsInserted++;