diff --git a/README.md b/README.md index 03a705b..42b8b19 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A plugin for monitoring Microsoft SQL Server using the New Relic platform. ## Installation instructions -1. [Download the files](https://s3.amazonaws.com/new_relic_platform_plugin_binary_hosting/ms_sql_plugin/NewRelic.Microsoft.SqlServer.Plugin.zip) +1. [Download the files](https://rpm.newrelic.com/plugins/55/b100e5e011d544ba024e265887b4dff3) from New Relic. 2. Unpack them to something like `C:\Program Files\New Relic\MicrosoftSQLServerPlugin\` (we'll call this `INSTALLDIR`.) on a server that has access to the SQL server(s) you want to monitor. In general, that means the agent could run on the server hosting the SQL server or another locally connected machine which network access to the SQL server. 3. Configure the plugin. 1. Run a text editor **as administrator** and open the file `INSTALLDIR\NewRelic.Microsoft.SqlServer.Plugin.exe.config`. @@ -25,14 +25,16 @@ A plugin for monitoring Microsoft SQL Server using the New Relic platform. `connectionString="Server=tcp:zzz.database.windows.net,1433;Database=CustomerDB;User ID=NewRelic@zzz;` `Password=foobar;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;` 4. Verify the settings. - 1. Open a command prompt running **as administrator** to `INSTALLDIR`. - 2. Run the plugin in read-only mode: `NewRelic.Microsoft.SqlServer.Plugin.exe --collect-only` + 1. Open a command prompt, **not** running as administrator, to `INSTALLDIR`. + 2. Run the plugin in read-only, test mode: `NewRelic.Microsoft.SqlServer.Plugin.exe --test` 3. If there are no errors, move on to installing the service. 5. Install the plugin as a Windows service. - 1. Use the command prompt from step #4.1 or open it again. + 1. Open a new command prompt, **running as administrator**, to `INSTALLDIR`. 2. Execute: `NewRelic.Microsoft.SqlServer.Plugin.exe --install` and ensure you see the message `Service NewRelicSQLServerPlugin has been successfully installed.` - 3. Start the service: net start NewRelicSQLServerPlugin + 3. Start the service: `net start NewRelicSQLServerPlugin` + 4. Review the log file at `C:\ProgramData\New Relic\MicrosoftSQLServerPlugin\SqlMonitor.log` to confirm the service is running. Look at the end of the file for a successful startup similar to the output of step 4.2., but with `[Main] INFO - Windows Service: Yes`, indicating the Windows service is running. + 5. After about 5 minutes, data is available in your New Relic dashboard in the 'MS SQL' and/or 'Azure SQL' area. ## Configure permissions @@ -126,6 +128,7 @@ SQL Server LEFT JOIN sys.sysprocesses s ON s.dbid = d.database_id LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.spid WHERE (s.spid IS NULL OR c.session_id >= 51) + GROUP BY d.Name Azure SQL diff --git a/build/versions.targets b/build/versions.targets index 299cd85..d9b982d 100644 --- a/build/versions.targets +++ b/build/versions.targets @@ -3,7 +3,7 @@ 1 0 - 6 + 7 diff --git a/src/Common/CommonAssemblyInfo.cs b/src/Common/CommonAssemblyInfo.cs index 0e11abd..f5e5457 100644 --- a/src/Common/CommonAssemblyInfo.cs +++ b/src/Common/CommonAssemblyInfo.cs @@ -7,6 +7,6 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.0.6")] -[assembly: AssemblyFileVersion("1.0.6")] -[assembly: AssemblyInformationalVersion("1.0.6")] +[assembly: AssemblyVersion("1.0.7")] +[assembly: AssemblyFileVersion("1.0.7")] +[assembly: AssemblyInformationalVersion("1.0.7")] diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs index 6e2b5cb..be24ea3 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs @@ -1,4 +1,4 @@ -using System.Data.SqlClient; +using System; using System.Linq; using System.Reflection; using System.Security.Principal; @@ -34,7 +34,7 @@ public Settings(ISqlEndpoint[] endpoints) public int PollIntervalSeconds { get; set; } public string ServiceName { get; set; } public ISqlEndpoint[] Endpoints { get; private set; } - public bool CollectOnly { get; set; } + public bool TestMode { get; set; } public bool IsProcessElevated { get; private set; } public string Version @@ -79,11 +79,16 @@ internal static Settings FromConfigurationSection(NewRelicConfigurationSection s public void ToLog(ILog log) { + // Pending review by New Relic before adding this information + // log.Info(" New Relic Key: " + LicenseKey); + log.Info(" Version: " + Version); - log.Info(" PollIntervalSeconds: " + PollIntervalSeconds); - log.Info(" CollectOnly: " + CollectOnly); - log.Info(" RunAsAdministrator: " + IsProcessElevated); - log.Info(" TotalEndpoints: " + Endpoints.Length); + log.Info(" Test Mode: " + (TestMode ? "Yes" : "No")); + log.Info(" Windows Service: " + (Environment.UserInteractive ? "No" : "Yes")); + log.InfoFormat(@" User: {0}\{1}", Environment.UserDomainName, Environment.UserName); + log.Info(" Run as Administrator: " + (IsProcessElevated ? "Yes" : "No")); + log.Info(" Total Endpoints: " + Endpoints.Length); + log.Info(" Poll Interval Seconds: " + PollIntervalSeconds); var sqlServerEndpoints = Endpoints.OfType().ToArray(); if (sqlServerEndpoints.Any()) @@ -118,7 +123,7 @@ public void ToLog(ILog log) public override string ToString() { - return string.Format("Version: {0}, PollIntervalSeconds: {1}, CollectOnly: {2}", Version, PollIntervalSeconds, CollectOnly); + return string.Format("Version: {0}, PollIntervalSeconds: {1}, TestMode: {2}", Version, PollIntervalSeconds, TestMode); } } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlPoller.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlPoller.cs index 699a01e..73f6759 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlPoller.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlPoller.cs @@ -43,11 +43,11 @@ public void Start() var queries = new QueryLocator(new DapperWrapper()).PrepareQueries(); _settings.Endpoints.ForEach(e => e.SetQueries(queries)); - var initialDelaySeconds = _settings.CollectOnly ? 0 : _settings.PollIntervalSeconds; + var initialDelaySeconds = _settings.TestMode ? 0 : _settings.PollIntervalSeconds; var pollingThreadSettings = new PollingThreadSettings { Name = "SqlPoller", - // Set to immediate when CollectOnly for instant gratification + // Set to immediate when in TestMode for instant gratification. InitialPollDelaySeconds = initialDelaySeconds, PollIntervalSeconds = _settings.PollIntervalSeconds, PollAction = () => _metricCollector.QueryEndpoints(queries), diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/MetricCollector.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/MetricCollector.cs index 31e10e2..da5039c 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/MetricCollector.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/MetricCollector.cs @@ -72,7 +72,7 @@ internal void SendComponentDataToCollector(ISqlEndpoint endpoint) var platformData = endpoint.GeneratePlatformData(_agentData); // Allows a testing mode that does not send data to New Relic - if (_settings.CollectOnly) + if (_settings.TestMode) { return; } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj b/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj index 32b2360..0612a49 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj @@ -82,6 +82,7 @@ + diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Options.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Options.cs index df09a83..3dbd371 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Options.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Options.cs @@ -1,4 +1,6 @@ -using CommandLine; +using System; + +using CommandLine; using NewRelic.Microsoft.SqlServer.Plugin.Properties; @@ -13,27 +15,47 @@ public class Options MutuallyExclusiveSet = "mode")] public bool Install { get; set; } - [Option('u', "uninstall", HelpText = "Attempts to Stop and Uninstall the " + ServiceConstants.DisplayName + " Windows service from this machine", MutuallyExclusiveSet = "mode")] + [Option('u', "uninstall", HelpText = "Attempts to Stop and Uninstall the " + ServiceConstants.DisplayName + " Windows service from this machine.", MutuallyExclusiveSet = "mode")] public bool Uninstall { get; set; } - [Option('s', "start", HelpText = "Attempts to Start the " + ServiceConstants.DisplayName + " Windows service on this machine", MutuallyExclusiveSet = "mode")] + [Option('s', "start", HelpText = "Attempts to Start the " + ServiceConstants.DisplayName + " Windows service on this machine.", MutuallyExclusiveSet = "mode")] public bool Start { get; set; } - [Option("stop", HelpText = "Attempts to Stop the " + ServiceConstants.DisplayName + " Windows service on this machine", MutuallyExclusiveSet = "mode")] + [Option("stop", HelpText = "Attempts to Stop the " + ServiceConstants.DisplayName + " Windows service on this machine.", MutuallyExclusiveSet = "mode")] public bool Stop { get; set; } [Option("installorstart", - HelpText = "If the service was not previously installed it installs it, but does not start it. Alternatively if the service is already installed it simply starts the service", + HelpText = "If the service was not previously installed it installs it, but does not start it. Alternatively if the service is already installed it simply starts the service.", MutuallyExclusiveSet = "mode")] public bool InstallOrStart { get; set; } - [Option("service-name", HelpText = "Optional. Overrides the default service name when installing or uninstalling")] + [Option("service-name", HelpText = "Optional. Overrides the default service name when installing or uninstalling.")] public string ServiceName { get; set; } - [Option("collect-only", DefaultValue = false, HelpText = "Collect metrics, but do not send them to New Relic")] - public bool CollectOnly { get; set; } + [Option('t', "test", DefaultValue = false, HelpText = "Verifies the configuration. Collect metrics locally but does not send them to New Relic.")] + public bool TestMode { get; set; } - [Option("config-file", HelpText = "Specify that settings are in a different file than the .exe.config")] + [Option("config-file", HelpText = "Specify that settings are in a different file than the .exe.config.")] public string ConfigFile { get; set; } + + public static Options ParseArguments(string[] args) + { + // '--test' arg was once named '--collect-only'. This provides backwards compatibility. + if (args != null) + { + var index = Array.IndexOf(args, "--collect-only"); + if (index >= 0) + { + args[index] = "--test"; + } + } + + var options = new Options(); + + // If bad args were passed, will exit and print usage + Parser.Default.ParseArgumentsStrict(args, options); + + return options; + } } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Program.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Program.cs index c5ea1e0..e1de7b2 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Program.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Program.cs @@ -6,8 +6,6 @@ using System.ServiceProcess; using System.Threading; -using CommandLine; - using NewRelic.Microsoft.SqlServer.Plugin.Configuration; using NewRelic.Microsoft.SqlServer.Plugin.Core; using NewRelic.Microsoft.SqlServer.Plugin.Core.Extensions; @@ -24,12 +22,9 @@ private static int Main(string[] args) { try { - var options = new Options(); - var log = SetUpLogConfig(); - // If bad args were passed, will exit and print usage - Parser.Default.ParseArgumentsStrict(args, options); + var options = Options.ParseArguments(args); var settings = ConfigurationParser.ParseSettings(log, options.ConfigFile); Settings.Default = settings; @@ -60,8 +55,8 @@ private static int Main(string[] args) else { Thread.CurrentThread.Name = "Main"; - settings.CollectOnly = options.CollectOnly; - log.InfoFormat("New Relic® Sql Server Plugin"); + settings.TestMode = options.TestMode; + log.InfoFormat("New Relic Sql Server Plugin"); log.Info("Loaded Settings:"); settings.ToLog(log); @@ -72,7 +67,6 @@ private static int Main(string[] args) if (Environment.UserInteractive) { - Console.Out.WriteLine("Starting Interactive mode"); RunInteractive(settings); } else @@ -114,8 +108,6 @@ public static ILog SetUpLogConfig() /// private static void RunInteractive(Settings settings) { - Console.Out.WriteLine("Starting Server"); - // Start our services var poller = new SqlPoller(settings); poller.Start(); @@ -132,8 +124,6 @@ private static void RunInteractive(Settings settings) key = consoleKeyInfo.KeyChar; } while (key != 'q' && key != 'Q'); - Console.Out.WriteLine("Stopping..."); - // Stop our services poller.Stop(); diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs index b34f97c..0b0ccfd 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs @@ -287,69 +287,7 @@ private void LogErrorSummary(ILog log, Exception e, ISqlQuery query) var sqlException = e.InnerException as SqlException; if (sqlException == null) return; - var connectionString = new SqlConnectionStringBuilder(ConnectionString); - - switch (sqlException.Number) - { - case 297: // User cannot log on via Windows Auth - case 18456: // User cannot login via SQL Auth - if (connectionString.IntegratedSecurity) - { - // System.Data.SqlClient.SqlException: Login failed. The login is from an untrusted domain and cannot be used with Windows authentication. - log.ErrorFormat("The Windows service is running as user '{0}', however, the user cannot access the server '{1}'. " + - "Consider changing the connection string in the configuration file " + - "or adding permissions to your SQL Server (see readme.md).", - Environment.UserName, connectionString.DataSource); - } - else - { - // System.Data.SqlClient.SqlException: Login failed for user ''. - log.ErrorFormat("User '{0}' cannot access the server '{1}'. " + - "Consider changing the connection string in the configuration file " + - "or adding permissions to your SQL Server (see readme.md).", - connectionString.UserID, connectionString.DataSource); - } - break; - - case 4060: // Missing database user - // System.Data.SqlClient.SqlException: Cannot open database "Junk" requested by the login. The login failed. - if (connectionString.IntegratedSecurity) - { - log.ErrorFormat("The Windows service is running as user '{0}', however, the user cannot access the database '{1}'. " + - "Ensure the login has a user in the database (see readme.md).", - Environment.UserName, connectionString.InitialCatalog); - } - else - { - log.ErrorFormat("User '{0}' cannot access the database '{1}'. " + - "Ensure the login has a user in the database (see readme.md).", - connectionString.UserID, connectionString.InitialCatalog); - } - break; - - case 10060: - case 10061: - case 11001: - case 40615: - if (sqlException.Message.Contains("sp_set_firewall_rule")) - { - var relevantErrorMessage = Regex.Replace(sqlException.Message, @"change to take effect\.(.*)$", string.Empty, RegexOptions.Singleline); - log.Error("Azure SQL Error: " + relevantErrorMessage); - } - else - { - log.ErrorFormat("Timeout connecting to server at '{0}'. Verify that the connection string is correct and the server is reachable.", - connectionString.DataSource); - } - break; - - default: - // System.Data.SqlClient.SqlException: Arithmetic overflow error for data type tinyint, value = -34. - // System.Data.SqlClient.SqlException: Arithmetic overflow error converting expression to data type int. - log.ErrorFormat("Error collecting metric '{0}'. Contact New Relic support at https://support.newrelic.com/home. Details: C{1}, M{2}, S{3}", - query.QueryName, sqlException.Class, sqlException.Number, sqlException.State); - break; - } + log.LogSqlException(sqlException, query, ConnectionString); } } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlErrorReporter.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlErrorReporter.cs new file mode 100644 index 0000000..3fc3654 --- /dev/null +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlErrorReporter.cs @@ -0,0 +1,80 @@ +using System; +using System.Data.SqlClient; +using System.Text.RegularExpressions; + +using log4net; + +namespace NewRelic.Microsoft.SqlServer.Plugin +{ + public static class SqlErrorReporter + { + public static void LogSqlException(this ILog log, SqlException sqlException, ISqlQuery query, string connString) + { + var connectionString = new SqlConnectionStringBuilder(connString); + + log.Error(string.Empty); + + switch (sqlException.Number) + { + case 297: // User cannot log on via Windows Auth + case 18456: // User cannot login via SQL Auth + if (connectionString.IntegratedSecurity) + { + // System.Data.SqlClient.SqlException: Login failed. The login is from an untrusted domain and cannot be used with Windows authentication. + log.ErrorFormat("The Windows service is running as user '{0}', however, the user cannot access the server '{1}'. " + + "Consider changing the connection string in the configuration file " + + "or adding permissions to your SQL Server (see readme.md).", + Environment.UserName, connectionString.DataSource); + } + else + { + // System.Data.SqlClient.SqlException: Login failed for user ''. + log.ErrorFormat("User '{0}' cannot access the server '{1}'. " + + "Consider changing the connection string in the configuration file " + + "or adding permissions to your SQL Server (see readme.md).", + connectionString.UserID, connectionString.DataSource); + } + break; + + case 4060: // Missing database user + // System.Data.SqlClient.SqlException: Cannot open database "Junk" requested by the login. The login failed. + if (connectionString.IntegratedSecurity) + { + log.ErrorFormat("The Windows service is running as user '{0}', however, the user cannot access the database '{1}'. " + + "Ensure the login has a user in the database (see readme.md).", + Environment.UserName, connectionString.InitialCatalog); + } + else + { + log.ErrorFormat("User '{0}' cannot access the database '{1}'. " + + "Ensure the login has a user in the database (see readme.md).", + connectionString.UserID, connectionString.InitialCatalog); + } + break; + + case 10060: + case 10061: + case 11001: + case 40615: + if (sqlException.Message.Contains("sp_set_firewall_rule")) + { + var relevantErrorMessage = Regex.Replace(sqlException.Message, @"change to take effect\.(.*)$", string.Empty, RegexOptions.Singleline); + log.Error("Azure SQL Error: " + relevantErrorMessage); + } + else + { + log.ErrorFormat("Timeout connecting to server at '{0}'. Verify that the connection string is correct and the server is reachable.", + connectionString.DataSource); + } + break; + + default: + log.ErrorFormat("Error collecting metric '{0}': {1}", query.QueryName, sqlException.Message); + log.ErrorFormat("SQL Exception Details: Class {0}, Number {1}, State {2}", sqlException.Class, sqlException.Number, sqlException.State); + log.Error(@"Check the error log for more details at 'C:\ProgramData\New Relic\MicrosoftSQLServerPlugin\ErrorDetailOutput.log'"); + log.Error("For additional help, contact New Relic support at https://support.newrelic.com/home. Please paste all log messages above into the support request."); + break; + } + } + } +}