diff --git a/README.md b/README.md index eef0aae..c2e13cb 100644 --- a/README.md +++ b/README.md @@ -101,56 +101,26 @@ In a new connection to each individual Azure SQL Database: ## Proxy Support ## -When the plugin is installed behind a proxy, there are a few configuration options. - -**Default Proxy** - -Assuming the proxy is configured at the system level (Internet Options in Control Panel), just uncomment the default configuration so that it reads as follows: - - - - - - - -This configuration may be used with the username and password in the next section. - -If you are upgrading from version 1.0.8 or earlier, you'll need to add that snippet to the config. - -**Authenticated Proxy** - -Ensure the above `defaultProxy` configuration is in place before setting the credentials. The New Relic .NET SDK requires that you uncomment the default configuration and set the username and password as below: - - - - - - -When specifying the username and password, you *must* configure the default proxy seen immediately above. The domain name before the user is optional. - -If you are upgrading from version 1.0.8 or earlier, you'll need to add that snippet to the config. - -**Authenticated Proxy with Proxy URL** - -The authenticated proxy with specified URL is a bit of a mix of the previous two settings. It is also mutually exclusive to the previous proxy setting. First, ensure the `` contain the proxy URL, username, and password: - - - - - - - - -Then the proxy handler must be configured. This is *similar* to the previous proxy config above: - - - - - - - -If you have any questions regarding the expected look of the config file, please review the [latest source on GitHub](https://github.com/newrelic-platform/newrelic_microsoft_sqlserver_plugin/blob/develop/src/NewRelic.Microsoft.SqlServer.Plugin/app.config). - +For installations behind a proxy, the details are set in the config file. + + + + + + +If you are upgrading from version 1.0.9 or earlier, you'll need to replace the previous proxy settings with this new snippet in the config. ## Logging diff --git a/build/versions.targets b/build/versions.targets index 4bed4ba..d95248b 100644 --- a/build/versions.targets +++ b/build/versions.targets @@ -3,7 +3,7 @@ 1 0 - 9 + 10 diff --git a/lib/NewRelic.Platform.Binding.DotNET.dll b/lib/NewRelic.Platform.Binding.DotNET.dll index f7b1bb6..6a88243 100644 Binary files a/lib/NewRelic.Platform.Binding.DotNET.dll and b/lib/NewRelic.Platform.Binding.DotNET.dll differ diff --git a/lib/NewRelic.Platform.Binding.DotNET.pdb b/lib/NewRelic.Platform.Binding.DotNET.pdb deleted file mode 100644 index abfead9..0000000 Binary files a/lib/NewRelic.Platform.Binding.DotNET.pdb and /dev/null differ diff --git a/src/Common/CommonAssemblyInfo.cs b/src/Common/CommonAssemblyInfo.cs index 6681236..658dfce 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.9")] -[assembly: AssemblyFileVersion("1.0.9")] -[assembly: AssemblyInformationalVersion("1.0.9")] +[assembly: AssemblyVersion("1.0.10")] +[assembly: AssemblyFileVersion("1.0.10")] +[assembly: AssemblyInformationalVersion("1.0.10")] diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlEndpointTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlEndpointTests.cs index ac54f24..24fdd7b 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlEndpointTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlEndpointTests.cs @@ -27,6 +27,29 @@ public IEnumerable ComponentGuidTestCases } } + //Tests fix for issue where Plugin gets 400's after server is unavailable for a time https://support.newrelic.com/tickets/55385 + [Test] + [TestCase(1, TestName = "A Minute Since Success")] + [TestCase(5, TestName = "5 Minutes Since Success")] + [TestCase(30, TestName = "30 Minutes Since Success")] + [TestCase(60, TestName = "An Hour Since Success")] + [TestCase(120, TestName = "Two Hours Since Success")] + [TestCase(1440, TestName = "A Day Since Success")] + [TestCase(2880, TestName = "Two Days Since Success")] + public void Assert_duration_does_not_exceed_allowed_max(int minutesSinceLastSuccessful) + { + var endpoint = new SqlServerEndpoint("Foo",".",false); + endpoint.MetricReportSuccessful(DateTime.Now.AddMinutes(minutesSinceLastSuccessful * -1)); + + const int thirtyMinutesInSeconds = 30 * 60; + Assert.That(endpoint.Duration, Is.LessThanOrEqualTo(thirtyMinutesInSeconds), "Duration should never be longer than 30 minutes, regardless of last succssful reported time"); + + endpoint.MetricReportSuccessful(DateTime.Now.AddMinutes(-.5)); + Assert.That(endpoint.Duration, Is.LessThanOrEqualTo(31), "Duration should reset to be around 30 seconds regardless of previous value"); + + } + + [Test] public void Assert_endpoint_appropriately_massages_duplicated_data() { var endpoint = Substitute.For("", ""); diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/NewRelicConfigurationSection.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/NewRelicConfigurationSection.cs index 8529132..580a1e0 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/NewRelicConfigurationSection.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/NewRelicConfigurationSection.cs @@ -4,12 +4,18 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration { internal class NewRelicConfigurationSection : ConfigurationSection { - [ConfigurationProperty("service")] + [ConfigurationProperty("service", IsRequired = true)] public ServiceElement Service { get { return ((ServiceElement) (base["service"])); } } + [ConfigurationProperty("proxy")] + public ProxyElement Proxy + { + get { return ((ProxyElement)(base["proxy"])); } + } + [ConfigurationProperty("sqlServers")] public SqlServerCollection SqlServers { diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ProxyElement.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ProxyElement.cs new file mode 100644 index 0000000..97373d4 --- /dev/null +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ProxyElement.cs @@ -0,0 +1,62 @@ +using System.Configuration; + +namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration +{ + internal class ProxyElement : ConfigurationElement + { + /// + /// The proxy server host name. Required. + /// + [ConfigurationProperty("host", DefaultValue = "", IsKey = false, IsRequired = true)] + public string Host + { + get { return ((string)(base["host"])); } + set { base["host"] = value; } + } + /// + /// The proxy server port (optional - defaults to 8080). + /// + [ConfigurationProperty("port", DefaultValue = "8080", IsKey = false, IsRequired = false)] + public string Port + { + get { return ((string)(base["port"])); } + set { base["port"] = value; } + } + /// + /// The username used to authenticate with the proxy server (optional). + /// + [ConfigurationProperty("user", DefaultValue = "", IsKey = false, IsRequired = false)] + public string User + { + get { return ((string)(base["user"])); } + set { base["user"] = value; } + } + /// + /// The password used to authenticate with the proxy server (optional). + /// + [ConfigurationProperty("password", DefaultValue = "", IsKey = false, IsRequired = false)] + public string Password + { + get { return ((string)(base["password"])); } + set { base["password"] = value; } + } + /// + /// The domain used to authenticate with the proxy server (optional). + /// + [ConfigurationProperty("domain", DefaultValue = "", IsKey = false, IsRequired = false)] + public string Domain + { + get { return ((string)(base["domain"])); } + set { base["domain"] = value; } + } + /// + /// 'true' or 'false. Uses the credentials of the account running the plugin (optional - defaults to false). + /// + [ConfigurationProperty("useDefaultCredentials", DefaultValue = "false", IsKey = false, IsRequired = false)] + public string UseDefaultCredentials + { + get { return ((string)(base["useDefaultCredentials"])); } + set { base["useDefaultCredentials"] = value; } + } + } +} \ No newline at end of file diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs index cc9b7cc..e709986 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net; using System.Reflection; using System.Security.Principal; using System.Text.RegularExpressions; @@ -13,6 +14,7 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration public class Settings { private string _version; + private static string _ProxyDetails; public Settings(ISqlEndpoint[] endpoints) { @@ -74,9 +76,72 @@ internal static Settings FromConfigurationSection(NewRelicConfigurationSection s settings.ServiceName = service.ServiceName; } + var webProxy = GetWebProxy(section, log); + if (webProxy != null) + { + WebRequest.DefaultWebProxy = webProxy; + } + return settings; } + private static IWebProxy GetWebProxy(NewRelicConfigurationSection section, ILog log) + { + var proxyElement = section.Proxy; + if (proxyElement == null || !proxyElement.ElementInformation.IsPresent) return null; + + Uri uri; + if (!Uri.TryCreate(proxyElement.Host, UriKind.RelativeOrAbsolute, out uri)) + { + log.ErrorFormat("Proxy host '{0}' is not a valid URI, skipping proxy.", proxyElement.Host); + return null; + } + + int port; + if (!int.TryParse(proxyElement.Port, out port)) + { + log.ErrorFormat("Unable to parse proxy port from '{0}', skipping proxy. Expecting a number from 1-65535.", proxyElement.Port); + return null; + } + + WebProxy webProxy; + try + { + webProxy = new WebProxy(proxyElement.Host, port); + } + catch (Exception e) + { + log.ErrorFormat("Proxy settings are invalid. {0}", e.Message); + return null; + } + + if ("true".Equals(proxyElement.UseDefaultCredentials, StringComparison.InvariantCultureIgnoreCase)) + { + webProxy.UseDefaultCredentials = true; + webProxy.Credentials = CredentialCache.DefaultCredentials; + _ProxyDetails = string.Format("Proxy Server: {0}:{1} with default credentials", proxyElement.Host, port); + } + else if (!string.IsNullOrEmpty(proxyElement.User)) + { + if (string.IsNullOrEmpty(proxyElement.Domain)) + { + webProxy.Credentials = new NetworkCredential(proxyElement.User, proxyElement.Password); + _ProxyDetails = string.Format("Proxy Server: {0}@{1}:{2}", proxyElement.User, proxyElement.Host, port); + } + else + { + webProxy.Credentials = new NetworkCredential(proxyElement.User, proxyElement.Password, proxyElement.Domain); + _ProxyDetails = string.Format("Proxy Server: {0}\\{1}@{2}:{3}", proxyElement.Domain, proxyElement.User, proxyElement.Host, port); + } + } + else + { + _ProxyDetails = string.Format("Proxy Server: {0}:{1}", proxyElement.Host, port); + } + + return webProxy; + } + public void ToLog(ILog log) { // Pending review by New Relic before adding this information @@ -87,6 +152,12 @@ public void ToLog(ILog log) 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")); + + if (_ProxyDetails != null) + { + log.Info(" " + _ProxyDetails); + } + log.Info(" Total Endpoints: " + Endpoints.Length); log.Info(" Poll Interval Seconds: " + PollIntervalSeconds); 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 83196fe..e766740 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj @@ -73,6 +73,7 @@ + diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Queries/DatabaseDetails.SqlServer.sql b/src/NewRelic.Microsoft.SqlServer.Plugin/Queries/DatabaseDetails.SqlServer.sql index 6da32c4..03f52d1 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Queries/DatabaseDetails.SqlServer.sql +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Queries/DatabaseDetails.SqlServer.sql @@ -2,9 +2,9 @@ -- Retrieves relevant data about each database -- Assists support when a user reports a problem - -SELECT - d.[name] AS DatabaseName, +SELECT d.[name] AS DatabaseName, + * -- Use * as differnt databases seem to have different columns. This isn't critical data so we must fail gracefully. +/* d.[database_id], d.[source_database_id], d.[create_date], @@ -70,5 +70,6 @@ SELECT d.[containment], d.[containment_desc], d.[target_recovery_time_in_seconds] +*/ FROM sys.databases d /*{WHERE}*/ \ No newline at end of file diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs index d158dac..30da068 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs @@ -4,18 +4,23 @@ using System.Linq; using System.Text; +using log4net; + using NewRelic.Microsoft.SqlServer.Plugin.Configuration; using NewRelic.Microsoft.SqlServer.Plugin.Core.Extensions; using NewRelic.Microsoft.SqlServer.Plugin.Properties; using NewRelic.Microsoft.SqlServer.Plugin.QueryTypes; using NewRelic.Platform.Binding.DotNET; -using log4net; - namespace NewRelic.Microsoft.SqlServer.Plugin { public abstract class SqlEndpointBase : ISqlEndpoint { + /// + /// Metrics with a Duration greater than this value will be rejected by the server (400) + /// + private const int MaximumAllowedDuration = 1800; + private static readonly ILog _ErrorDetailOutputLogger = LogManager.GetLogger(Constants.ErrorDetailLogger); private static readonly ILog _VerboseSqlOutputLogger = LogManager.GetLogger(Constants.VerboseSqlLogger); @@ -54,7 +59,14 @@ public string[] IncludedDatabaseNames public int Duration { - get { return (int) DateTime.Now.Subtract(_lastSuccessfulReportTime).TotalSeconds; } + get + { + var secondsSinceLastSuccessfulReport = (int) DateTime.Now.Subtract(_lastSuccessfulReportTime).TotalSeconds; + + return secondsSinceLastSuccessfulReport <= MaximumAllowedDuration + ? secondsSinceLastSuccessfulReport + : MaximumAllowedDuration; + } } public void SetQueries(IEnumerable queries) @@ -77,7 +89,7 @@ public void UpdateHistory(IQueryContext[] queryContexts) { queryContexts.ForEach(queryContext => { - var queryHistory = QueryHistory.GetOrCreate(queryContext.QueryName); + Queue queryHistory = QueryHistory.GetOrCreate(queryContext.QueryName); if (queryHistory.Count >= 2) //Only track up to last run of this query { queryHistory.Dequeue(); @@ -90,8 +102,8 @@ public PlatformData GeneratePlatformData(AgentData agentData) { var platformData = new PlatformData(agentData); - var pendingComponentData = QueryHistory.Select(qh => ComponentDataRetriever.GetData(qh.Value.ToArray())) - .Where(c => c != null).ToArray(); + ComponentData[] pendingComponentData = QueryHistory.Select(qh => ComponentDataRetriever.GetData(qh.Value.ToArray())) + .Where(c => c != null).ToArray(); pendingComponentData.ForEach(platformData.AddComponent); @@ -110,12 +122,13 @@ public virtual void ToLog(ILog log) log.InfoFormat(" {0}: {1}", Name, safeConnectionString); // Validate that connection string do not provide both Trusted Security AND user/password - var hasUserCreds = !string.IsNullOrEmpty(safeConnectionString.UserID) || !string.IsNullOrEmpty(safeConnectionString.Password); + bool hasUserCreds = !string.IsNullOrEmpty(safeConnectionString.UserID) || !string.IsNullOrEmpty(safeConnectionString.Password); if (safeConnectionString.IntegratedSecurity == hasUserCreds) { log.Error("=================================================="); log.ErrorFormat("Connection string for '{0}' may not contain both Integrated Security and User ID/Password credentials. " + - "Review the readme.md and update the config file.", safeConnectionString.DataSource); + "Review the readme.md and update the config file.", + safeConnectionString.DataSource); log.Error("=================================================="); } } @@ -133,7 +146,7 @@ protected IEnumerable ExecuteQueries(SqlQuery[] queries, string c using (var conn = new SqlConnection(connectionString)) { - foreach (var query in queries) + foreach (SqlQuery query in queries) { object[] results; try @@ -160,11 +173,11 @@ protected static void LogVerboseSqlResults(ISqlQuery query, IEnumerable { // This could be slow, so only proceed if it actually gets logged if (!_VerboseSqlOutputLogger.IsInfoEnabled) return; - + var verboseLogging = new StringBuilder(); verboseLogging.AppendFormat("Executed {0}", query.ResourceName).AppendLine(); - foreach (var result in results) + foreach (object result in results) { verboseLogging.AppendLine(result.ToString()); } @@ -197,7 +210,7 @@ internal object[] CalculateSqlDmlActivityIncrease(object[] inputResults, ILog lo return inputResults; } - var sqlDmlActivities = inputResults.OfType().ToArray(); + SqlDmlActivity[] sqlDmlActivities = inputResults.OfType().ToArray(); if (!sqlDmlActivities.Any()) { @@ -205,7 +218,7 @@ internal object[] CalculateSqlDmlActivityIncrease(object[] inputResults, ILog lo return inputResults; } - var currentValues = sqlDmlActivities + Dictionary currentValues = sqlDmlActivities .GroupBy(a => string.Format("{0}:{1}:{2}:{3}", BitConverter.ToString(a.PlanHandle), BitConverter.ToString(a.SqlStatementHash), a.CreationTime.Ticks, a.QueryType)) .Select(a => new { diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/app.config b/src/NewRelic.Microsoft.SqlServer.Plugin/app.config index 71b3c71..3f1486d 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/app.config +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/app.config @@ -5,26 +5,6 @@
- - - - + + + + + + - - - - - - + \ No newline at end of file diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/app.deploy.config b/src/NewRelic.Microsoft.SqlServer.Plugin/app.deploy.config index d60139d..7b2f8bf 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/app.deploy.config +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/app.deploy.config @@ -5,26 +5,6 @@
- - - - + + + + + + - - - - - - - \ No newline at end of file